diff --git a/.gitignore b/.gitignore index d1f5078cb4..e58d146cd5 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,4 @@ docs/docfx.zip *.received.* mono_crash.*.json test_output/ +test/**/*.apk diff --git a/CHANGELOG.md b/CHANGELOG.md index bd6822d151..c3862e7f95 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## Unreleased + +### Features + +- Add support for Portable PDB format ([#2050](https://github.com/getsentry/sentry-dotnet/pull/2050)) + ## 3.24.1 ### Fixes diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 791af3c670..be8848bbf4 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,9 +1,9 @@ # Contributing -We love receiving PRs from the community with features and fixed. +We love receiving PRs from the community with features and fixed. For big feature it's advised to raise an issue to discuss it first. -## TLDR: +## TLDR * Install the .NET SDKs * To quickly get up and running, you can just run `dotnet build` @@ -68,7 +68,7 @@ Note that the MSBuild version should always be `17.0` but will change paths base We'd love for users to update the SDK everytime and as soon as we make a new release. But in reality most users rarely update the SDK. To help users see value in updating the SDK, we maintain a changelog file with entries split between two headings: -1. `### Features` +1. `### Features` 2. `### Fixes` We add the heading in the first PR that's adding either a feature or fixes in the current release. @@ -86,3 +86,15 @@ Below that, you'll add the heading 3 mentioned above. For example, if you're add ``` There's a GitHub action check to verify if an entry was added. If the entry isn't a user-facing change, you can skip the verification with `#skip-changelog` written to the PR description. The bot writes a comment in the PR with a suggestion entry to the changelog based on the PR title. + +## Verify tests + +Some tests use [Verify](https://github.com/VerifyTests/Verify) to check returned objects against snapshots that are part of the repo. +In case you're making code changes that produce many (intended) changes in those snapshots, you can use [accept-verifier-changes.ps1](./scripts/accept-verifier-changes.ps1) like this: + +```shell-script +dotnet test +pwsh ./scripts/accept-verifier-changes.ps1 +``` + +You may need to run this multiple times because `dotnet test` stops after a certain number of failures. \ No newline at end of file diff --git a/Sentry-CI-Build-Linux.slnf b/Sentry-CI-Build-Linux.slnf index 59cfce806d..fecdbe694d 100644 --- a/Sentry-CI-Build-Linux.slnf +++ b/Sentry-CI-Build-Linux.slnf @@ -34,6 +34,7 @@ "src\\Sentry.NLog\\Sentry.NLog.csproj", "src\\Sentry.Serilog\\Sentry.Serilog.csproj", "src\\Sentry\\Sentry.csproj", + "test\\Sentry.Android.Tests\\Sentry.Android.Tests.csproj", "test\\Sentry.AspNet.Tests\\Sentry.AspNet.Tests.csproj", "test\\Sentry.AspNetCore.Grpc.Tests\\Sentry.AspNetCore.Grpc.Tests.csproj", "test\\Sentry.AspNetCore.Tests\\Sentry.AspNetCore.Tests.csproj", diff --git a/Sentry-CI-Build-Windows.slnf b/Sentry-CI-Build-Windows.slnf index c9d482552f..ffa209f86e 100644 --- a/Sentry-CI-Build-Windows.slnf +++ b/Sentry-CI-Build-Windows.slnf @@ -33,6 +33,7 @@ "src\\Sentry.NLog\\Sentry.NLog.csproj", "src\\Sentry.Serilog\\Sentry.Serilog.csproj", "src\\Sentry\\Sentry.csproj", + "test\\Sentry.Android.Tests\\Sentry.Android.Tests.csproj", "test\\Sentry.AspNet.Tests\\Sentry.AspNet.Tests.csproj", "test\\Sentry.AspNetCore.Grpc.Tests\\Sentry.AspNetCore.Grpc.Tests.csproj", "test\\Sentry.AspNetCore.Tests\\Sentry.AspNetCore.Tests.csproj", diff --git a/Sentry-CI-Build-macOS.slnf b/Sentry-CI-Build-macOS.slnf index 1aac621a54..27fc5556e9 100644 --- a/Sentry-CI-Build-macOS.slnf +++ b/Sentry-CI-Build-macOS.slnf @@ -36,6 +36,7 @@ "src\\Sentry.NLog\\Sentry.NLog.csproj", "src\\Sentry.Serilog\\Sentry.Serilog.csproj", "src\\Sentry\\Sentry.csproj", + "test\\Sentry.Android.Tests\\Sentry.Android.Tests.csproj", "test\\Sentry.AspNet.Tests\\Sentry.AspNet.Tests.csproj", "test\\Sentry.AspNetCore.Grpc.Tests\\Sentry.AspNetCore.Grpc.Tests.csproj", "test\\Sentry.AspNetCore.Tests\\Sentry.AspNetCore.Tests.csproj", diff --git a/Sentry-CI-Test.slnf b/Sentry-CI-Test.slnf index 0397bdb917..735524c45d 100644 --- a/Sentry-CI-Test.slnf +++ b/Sentry-CI-Test.slnf @@ -2,6 +2,7 @@ "solution": { "path": "Sentry.sln", "projects": [ + "test\\Sentry.Android.Tests\\Sentry.Android.Tests.csproj", "test\\Sentry.AspNet.Tests\\Sentry.AspNet.Tests.csproj", "test\\Sentry.AspNetCore.Grpc.Tests\\Sentry.AspNetCore.Grpc.Tests.csproj", "test\\Sentry.AspNetCore.Tests\\Sentry.AspNetCore.Tests.csproj", diff --git a/Sentry.sln b/Sentry.sln index 7c65c68c8f..f1ad245e96 100644 --- a/Sentry.sln +++ b/Sentry.sln @@ -166,6 +166,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sentry.Bindings.Android", " EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sentry.Bindings.Cocoa", "src\Sentry.Bindings.Cocoa\Sentry.Bindings.Cocoa.csproj", "{F533FD6D-1E10-4F06-BE3A-3B3C713A75A6}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sentry.Android.Tests", "test\Sentry.Android.Tests\Sentry.Android.Tests.csproj", "{5E12E053-22AF-4184-8581-3FCFD225617D}" +EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sentry.AspNetCore.TestUtils", "test\Sentry.AspNetCore.TestUtils\Sentry.AspNetCore.TestUtils.csproj", "{C96CB65D-3D2D-404E-85C0-69A3FC03B48F}" EndProject Global @@ -399,6 +401,10 @@ Global {F533FD6D-1E10-4F06-BE3A-3B3C713A75A6}.Debug|Any CPU.Build.0 = Debug|Any CPU {F533FD6D-1E10-4F06-BE3A-3B3C713A75A6}.Release|Any CPU.ActiveCfg = Release|Any CPU {F533FD6D-1E10-4F06-BE3A-3B3C713A75A6}.Release|Any CPU.Build.0 = Release|Any CPU + {5E12E053-22AF-4184-8581-3FCFD225617D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5E12E053-22AF-4184-8581-3FCFD225617D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5E12E053-22AF-4184-8581-3FCFD225617D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5E12E053-22AF-4184-8581-3FCFD225617D}.Release|Any CPU.Build.0 = Release|Any CPU {C96CB65D-3D2D-404E-85C0-69A3FC03B48F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {C96CB65D-3D2D-404E-85C0-69A3FC03B48F}.Debug|Any CPU.Build.0 = Debug|Any CPU {C96CB65D-3D2D-404E-85C0-69A3FC03B48F}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -466,6 +472,7 @@ Global {70066C6C-0A18-4322-A02A-9A0DFE59C02B} = {77454495-55EE-4B40-A089-71B9E8F82E89} {9A23DA57-3F0B-4646-A37A-71A4CE7C94CB} = {AF6AF4C7-8AA2-4D59-8064-2D79560904EB} {F533FD6D-1E10-4F06-BE3A-3B3C713A75A6} = {AF6AF4C7-8AA2-4D59-8064-2D79560904EB} + {5E12E053-22AF-4184-8581-3FCFD225617D} = {83263231-1A2A-4733-B759-EEFF14E8C5D5} {C96CB65D-3D2D-404E-85C0-69A3FC03B48F} = {83263231-1A2A-4733-B759-EEFF14E8C5D5} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution diff --git a/SentryMaui.slnf b/SentryMaui.slnf index 4d7e7c9aa7..f671d8c3a5 100644 --- a/SentryMaui.slnf +++ b/SentryMaui.slnf @@ -12,6 +12,7 @@ "test\\MauiTestUtils\\DeviceTests.Runners.SourceGen\\TestUtils.DeviceTests.Runners.SourceGen.csproj", "test\\MauiTestUtils\\DeviceTests.Runners\\TestUtils.DeviceTests.Runners.csproj", "test\\MauiTestUtils\\DeviceTests\\TestUtils.DeviceTests.csproj", + "test\\Sentry.Android.Tests\\Sentry.Android.Tests.csproj", "test\\Sentry.Extensions.Logging.Tests\\Sentry.Extensions.Logging.Tests.csproj", "test\\Sentry.Maui.Device.TestApp\\Sentry.Maui.Device.TestApp.csproj", "test\\Sentry.Maui.Tests\\Sentry.Maui.Tests.csproj", diff --git a/SentryNoSamples.slnf b/SentryNoSamples.slnf index 857b6791f6..41592da47a 100644 --- a/SentryNoSamples.slnf +++ b/SentryNoSamples.slnf @@ -15,6 +15,7 @@ "src\\Sentry.NLog\\Sentry.NLog.csproj", "src\\Sentry.Serilog\\Sentry.Serilog.csproj", "src\\Sentry\\Sentry.csproj", + "test\\Sentry.Android.Tests\\Sentry.Android.Tests.csproj", "test\\Sentry.AspNet.Tests\\Sentry.AspNet.Tests.csproj", "test\\Sentry.AspNetCore.Grpc.Tests\\Sentry.AspNetCore.Grpc.Tests.csproj", "test\\Sentry.AspNetCore.Tests\\Sentry.AspNetCore.Tests.csproj", diff --git a/scripts/accept-verifier-changes.ps1 b/scripts/accept-verifier-changes.ps1 new file mode 100644 index 0000000000..06a5a4b2af --- /dev/null +++ b/scripts/accept-verifier-changes.ps1 @@ -0,0 +1,23 @@ +# Similar to `dotnet verify accept` but doesn't create new runtime-specific ".verified" files if a common one exists. +param([switch] $DryRun) + +Set-StrictMode -Version Latest +$ErrorActionPreference = "Stop" + +$receivedFiles = Get-ChildItem . -Recurse -File -Include "*.received.txt" + +$receivedFiles | ForEach-Object { + $nameWithRuntime = ($_.BaseName.Split(".") | Select-Object -SkipLast 1) -join "." + $nameWithoutRuntime = ($_.BaseName.Split(".") | Select-Object -SkipLast 2) -join "." + $ext = ".verified.txt" + + $targetFile = (Test-Path "$($_.Directory)/$nameWithoutRuntime$ext") ` + ? "$($_.Directory)/$nameWithoutRuntime$ext" ` + : "$($_.Directory)/$nameWithRuntime$ext" + + Write-Host "Updating $($targetFile.Replace((Get-Item .).FullName, ''))" + if (-not ($DryRun)) + { + Move-Item $_ $targetFile -Force + } +} diff --git a/scripts/update-test-apks.ps1 b/scripts/update-test-apks.ps1 new file mode 100644 index 0000000000..0423b00ebb --- /dev/null +++ b/scripts/update-test-apks.ps1 @@ -0,0 +1,52 @@ +param([switch] $IfNotExist) + +Set-StrictMode -Version Latest +$ErrorActionPreference = "Stop" + +$repoRoot = "$PSScriptRoot/.." +$apkDir = "$repoRoot/test/Sentry.Android.Tests/TestAPKs" + +New-Item -ItemType Directory -Force -Path $apkDir | Out-Null +$apkDir = Resolve-Path $apkDir + +function BuildAndroidSample([bool] $UseAssemblyStore, [bool] $UseAssemblyCompression) +{ + $testAppDir = "$repoRoot/test/AndroidTestApp" + $outputApk = "$apkDir/android-Store=$UseAssemblyStore-Compressed=$UseAssemblyCompression.apk" + + if ($IfNotExist -and (Test-Path $outputApk)) + { + Write-Host "$outputApk already exists, skipping build" + return + } + + Push-Location -Verbose $testAppDir + try + { + # Need to do a clean build otherwise some DLLs would end up being compressed even if it's disabled on this run. + dotnet build --configuration Release --no-incremental ` + --property:AndroidUseAssemblyStore=$UseAssemblyStore ` + --property:AndroidEnableAssemblyCompression=$UseAssemblyCompression ` + | Out-Host + if ($LASTEXITCODE -ne 0) + { + exit $LASTEXITCODE + } + } + finally + { + Pop-Location + } + + Move-Item "$testAppDir/bin/Release/*/*-signed.apk" $outputApk +} + +if (!$IfNotExist) +{ + Remove-Item "$apkDir/*.apk" +} + +BuildAndroidSample $true $true +BuildAndroidSample $true $false +BuildAndroidSample $false $true +BuildAndroidSample $false $false diff --git a/src/Sentry/DebugImage.cs b/src/Sentry/DebugImage.cs index 56a0748fc4..00c633aa23 100644 --- a/src/Sentry/DebugImage.cs +++ b/src/Sentry/DebugImage.cs @@ -31,6 +31,11 @@ public sealed class DebugImage : IJsonSerializable /// public string? DebugId { get; set; } + /// + /// Checksum of the companion debug file. + /// + public string? DebugChecksum { get; set; } + /// /// Path and name of the debug companion file. /// @@ -47,6 +52,8 @@ public sealed class DebugImage : IJsonSerializable /// public string? CodeFile { get; set; } + internal Guid? ModuleVersionId { get; set; } + /// public void WriteTo(Utf8JsonWriter writer, IDiagnosticLogger? logger) { @@ -56,6 +63,7 @@ public void WriteTo(Utf8JsonWriter writer, IDiagnosticLogger? logger) writer.WriteStringIfNotWhiteSpace("image_addr", ImageAddress); writer.WriteNumberIfNotNull("image_size", ImageSize); writer.WriteStringIfNotWhiteSpace("debug_id", DebugId); + writer.WriteStringIfNotWhiteSpace("debug_checksum", DebugChecksum); writer.WriteStringIfNotWhiteSpace("debug_file", DebugFile); writer.WriteStringIfNotWhiteSpace("code_id", CodeId); writer.WriteStringIfNotWhiteSpace("code_file", CodeFile); @@ -72,6 +80,7 @@ public static DebugImage FromJson(JsonElement json) var imageAddress = json.GetPropertyOrNull("image_addr")?.GetString(); var imageSize = json.GetPropertyOrNull("image_size")?.GetInt64(); var debugId = json.GetPropertyOrNull("debug_id")?.GetString(); + var debugChecksum = json.GetPropertyOrNull("debug_checksum")?.GetString(); var debugFile = json.GetPropertyOrNull("debug_file")?.GetString(); var codeId = json.GetPropertyOrNull("code_id")?.GetString(); var codeFile = json.GetPropertyOrNull("code_file")?.GetString(); @@ -82,6 +91,7 @@ public static DebugImage FromJson(JsonElement json) ImageAddress = imageAddress, ImageSize = imageSize, DebugId = debugId, + DebugChecksum = debugChecksum, DebugFile = debugFile, CodeId = codeId, CodeFile = codeFile, diff --git a/src/Sentry/Extensibility/SentryStackTraceFactory.cs b/src/Sentry/Extensibility/SentryStackTraceFactory.cs index 3cde66e3dd..62bdf6f1f7 100644 --- a/src/Sentry/Extensibility/SentryStackTraceFactory.cs +++ b/src/Sentry/Extensibility/SentryStackTraceFactory.cs @@ -1,29 +1,14 @@ -using Sentry.Internal.Extensions; +using Sentry.Internal; namespace Sentry.Extensibility; /// /// Default factory to from an . /// -public class SentryStackTraceFactory : ISentryStackTraceFactory +public sealed class SentryStackTraceFactory : ISentryStackTraceFactory { private readonly SentryOptions _options; - /* - * NOTE: While we could improve these regexes, doing so might break exception grouping on the backend. - * Specifically, RegexAsyncFunctionName would be better as: @"^(.*)\+<(\w*|<\w*>b__\d*)>d(?:__\d*)?$" - * But we cannot make this change without consequences of ignored events coming back to life in Sentry. - */ - - private static readonly Regex RegexAsyncFunctionName = new(@"^(.*)\+<(\w*)>d__\d*$", - RegexOptions.Compiled | RegexOptions.CultureInvariant); - - private static readonly Regex RegexAnonymousFunction = new(@"^<(\w*)>b__\w+$", - RegexOptions.Compiled | RegexOptions.CultureInvariant); - - private static readonly Regex RegexAsyncReturn = new(@"^(.+`[0-9]+)\[\[", - RegexOptions.Compiled | RegexOptions.CultureInvariant); - /// /// Creates an instance of . /// @@ -34,289 +19,20 @@ public class SentryStackTraceFactory : ISentryStackTraceFactory /// /// The exception to create the stacktrace from. /// A Sentry stack trace. - public virtual SentryStackTrace? Create(Exception? exception = null) + public SentryStackTrace? Create(Exception? exception = null) { - var isCurrentStackTrace = exception == null && _options.AttachStacktrace; - - if (exception == null && !isCurrentStackTrace) + if (exception == null && !_options.AttachStacktrace) { _options.LogDebug("No Exception and AttachStacktrace is off. No stack trace will be collected."); return null; } + var isCurrentStackTrace = exception == null && _options.AttachStacktrace; _options.LogDebug("Creating SentryStackTrace. isCurrentStackTrace: {0}.", isCurrentStackTrace); - return Create(CreateStackTrace(exception), isCurrentStackTrace); - } - - /// - /// Creates a s from the . - /// - /// The exception. - /// A StackTrace. - protected virtual StackTrace CreateStackTrace(Exception? exception) => - exception is null - ? new StackTrace(true) - : new StackTrace(exception, true); - - /// - /// Creates a from the . - /// - /// The stack trace. - /// Whether this is the current stack trace. - /// SentryStackTrace - internal SentryStackTrace? Create(StackTrace stackTrace, bool isCurrentStackTrace) - { - var frames = CreateFrames(stackTrace, isCurrentStackTrace) - // Sentry expects the frames to be sent in reversed order - .Reverse(); - - var stacktrace = new SentryStackTrace(); - - foreach (var frame in frames) - { - stacktrace.Frames.Add(frame); - } - - return stacktrace.Frames.Count != 0 - ? stacktrace - : null; - } - - /// - /// Creates an enumerator of from a . - /// - internal IEnumerable CreateFrames(StackTrace stackTrace, bool isCurrentStackTrace) - { - var frames = _options.StackTraceMode switch - { - StackTraceMode.Enhanced => EnhancedStackTrace.GetFrames(stackTrace).Select(p => p as StackFrame), - _ => stackTrace.GetFrames() - // error CS8619: Nullability of reference types in value of type 'StackFrame?[]' doesn't match target type 'IEnumerable'. -#if NETCOREAPP3_0 - .Where(f => f is not null) -#endif - }; - - // Not to throw on code that ignores nullability warnings. - if (frames.IsNull()) - { - _options.LogDebug("No stack frames found. AttachStacktrace: '{0}', isCurrentStackTrace: '{1}'", - _options.AttachStacktrace, isCurrentStackTrace); - - yield break; - } - - Debug.Assert(frames != null); - - var firstFrame = true; - foreach (var stackFrame in frames) - { - -#if !NET5_0_OR_GREATER - if (stackFrame is null) - { - continue; - } -#endif - - // Remove the frames until the call for capture with the SDK - if (firstFrame - && isCurrentStackTrace - && stackFrame.GetMethod() is { } method - && method.DeclaringType?.AssemblyQualifiedName?.StartsWith("Sentry") == true) - { - continue; - } - - firstFrame = false; - - yield return CreateFrame(stackFrame, isCurrentStackTrace); - } - } - - internal SentryStackFrame CreateFrame(StackFrame stackFrame) => InternalCreateFrame(stackFrame, true); - - /// - /// Create a from a . - /// - protected virtual SentryStackFrame CreateFrame(StackFrame stackFrame, bool isCurrentStackTrace) => InternalCreateFrame(stackFrame, true); - - /// - /// Default the implementation of CreateFrame. - /// - protected SentryStackFrame InternalCreateFrame(StackFrame stackFrame, bool demangle) - { - const string unknownRequiredField = "(unknown)"; - string? projectPath = null; - var frame = new SentryStackFrame(); - if (GetMethod(stackFrame) is { } method) - { - frame.Module = method.DeclaringType?.FullName ?? unknownRequiredField; - frame.Package = method.DeclaringType?.Assembly.FullName; - - if (_options.StackTraceMode == StackTraceMode.Enhanced && - stackFrame is EnhancedStackFrame enhancedStackFrame) - { - var stringBuilder = new StringBuilder(); - frame.Function = enhancedStackFrame.MethodInfo.Append(stringBuilder, false).ToString(); - - if (enhancedStackFrame.MethodInfo.DeclaringType is { } declaringType) - { - stringBuilder.Clear(); - stringBuilder.AppendTypeDisplayName(declaringType); - frame.Module = stringBuilder.ToString(); - } - } - else - { - frame.Function = method.Name; - } - - // Originally we didn't skip methods from dynamic assemblies, so not to break compatibility: - if (_options.StackTraceMode != StackTraceMode.Original && method.Module.Assembly.IsDynamic) - { - frame.InApp = false; - } - - AttributeReader.TryGetProjectDirectory(method.Module.Assembly, out projectPath); - } - - frame.ConfigureAppFrame(_options); - - var frameFileName = stackFrame.GetFileName(); - if (projectPath != null && frameFileName != null) - { - if (frameFileName.StartsWith(projectPath, StringComparison.OrdinalIgnoreCase)) - { - frameFileName = frameFileName.Substring(projectPath.Length); - } - } - - frame.FileName = frameFileName; - - // stackFrame.HasILOffset() throws NotImplemented on Mono 5.12 - var ilOffset = stackFrame.GetILOffset(); - if (ilOffset != StackFrame.OFFSET_UNKNOWN) - { - frame.InstructionOffset = ilOffset; - } - - var lineNo = stackFrame.GetFileLineNumber(); - if (lineNo > 0) - { - frame.LineNumber = lineNo; - } - - var colNo = stackFrame.GetFileColumnNumber(); - if (lineNo > 0) - { - frame.ColumnNumber = colNo; - } - - if (demangle && _options.StackTraceMode != StackTraceMode.Enhanced) - { - DemangleAsyncFunctionName(frame); - DemangleAnonymousFunction(frame); - DemangleLambdaReturnType(frame); - } - - if (_options.StackTraceMode == StackTraceMode.Enhanced) - { - // 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 - frame.Module = null; - } - - return frame; - } - - /// - /// Get a from . - /// - /// The . - protected virtual MethodBase? GetMethod(StackFrame stackFrame) - => stackFrame.GetMethod(); - - /// - /// Clean up function and module names produced from `async` state machine calls. - /// - /// - /// When the Microsoft cs.exe compiler compiles some modern C# features, - /// such as async/await calls, it can create synthetic function names that - /// do not match the function names in the original source code. Here we - /// reverse some of these transformations, so that the function and module - /// names that appears in the Sentry UI will match the function and module - /// names in the original source-code. - /// - private static void DemangleAsyncFunctionName(SentryStackFrame frame) - { - if (frame.Module == null || frame.Function != "MoveNext") - { - return; - } - - // Search for the function name in angle brackets followed by d__. - // - // Change: - // RemotePrinterService+d__24 in MoveNext at line 457:13 - // to: - // RemotePrinterService in UpdateNotification at line 457:13 - - var match = RegexAsyncFunctionName.Match(frame.Module); - if (match.Success && match.Groups.Count == 3) - { - frame.Module = match.Groups[1].Value; - frame.Function = match.Groups[2].Value; - } - } - - /// - /// Clean up function names for anonymous lambda calls. - /// - internal static void DemangleAnonymousFunction(SentryStackFrame frame) - { - if (frame.Function == null) - { - return; - } - - // Search for the function name in angle brackets followed by b__. - // - // Change: - // b__36 - // to: - // BeginInvokeAsynchronousActionMethod { } - - var match = RegexAnonymousFunction.Match(frame.Function); - if (match.Success && match.Groups.Count == 2) - { - frame.Function = match.Groups[1].Value + " { }"; - } - } - - /// - /// Remove return type from module in a Task with a Lambda with a return value. - /// This was seen in Unity, see https://github.com/getsentry/sentry-unity/issues/845 - /// - internal static void DemangleLambdaReturnType(SentryStackFrame frame) - { - if (frame.Module == null) - { - return; - } - - // Change: - // System.Threading.Tasks.Task`1[[System.Int32, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]] in InnerInvoke - // or System.Collections.Generic.List`1[[UnityEngine.Events.PersistentCall, UnityEngine.CoreModule, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null]] in get_Item - // to: - // System.Threading.Tasks.Task`1 in InnerInvoke` - // or System.Collections.Generic.List`1 in get_Item - var match = RegexAsyncReturn.Match(frame.Module); - if (match.Success && match.Groups.Count == 2) - { - frame.Module = match.Groups[1].Value; - } + var stackTrace = exception is null ? new StackTrace(true) : new StackTrace(exception, true); + var result = DebugStackTrace.Create(_options, stackTrace, isCurrentStackTrace); + _options.LogDebug("Created {0} with {1} frames.", nameof(DebugStackTrace), result.Frames.Count); + return result.Frames.Count != 0 ? result : null; } } diff --git a/src/Sentry/Internal/DebugStackTrace.cs b/src/Sentry/Internal/DebugStackTrace.cs new file mode 100644 index 0000000000..5b7f4ab706 --- /dev/null +++ b/src/Sentry/Internal/DebugStackTrace.cs @@ -0,0 +1,494 @@ +using System.Reflection.PortableExecutable; +using Sentry.Internal.Extensions; +using Sentry.Extensibility; + +#if ANDROID +using Sentry.Android; +using Sentry.Android.AssemblyReader; +#endif + +namespace Sentry.Internal; + +/// +/// Sentry Stacktrace with debug images. +/// +internal class DebugStackTrace : SentryStackTrace +{ + private readonly SentryOptions _options; + + // Debug images referenced by frames in this StackTrace + private readonly Dictionary _debugImageIndexByModule = new(); + private const int DebugImageMissing = -1; + private bool _debugImagesMerged; + + /* + * NOTE: While we could improve these regexes, doing so might break exception grouping on the backend. + * Specifically, RegexAsyncFunctionName would be better as: @"^(.*)\+<(\w*|<\w*>b__\d*)>d(?:__\d*)?$" + * But we cannot make this change without consequences of ignored events coming back to life in Sentry. + */ + + private static readonly Regex RegexAsyncFunctionName = new(@"^(.*)\+<(\w*)>d__\d*$", + RegexOptions.Compiled | RegexOptions.CultureInvariant); + + private static readonly Regex RegexAnonymousFunction = new(@"^<(\w*)>b__\w+$", + RegexOptions.Compiled | RegexOptions.CultureInvariant); + + private static readonly Regex RegexAsyncReturn = new(@"^(.+`[0-9]+)\[\[", + RegexOptions.Compiled | RegexOptions.CultureInvariant); + +#if ANDROID + private readonly Lazy _assemblyReader; +#endif + + internal DebugStackTrace(SentryOptions options) + { + _options = options; +#if ANDROID + _assemblyReader = new Lazy(() => GetAndroidAssemblyReader(options)); +#endif + } + + protected List DebugImages { get; } = new(); + + internal static DebugStackTrace Create(SentryOptions options, StackTrace stackTrace, bool isCurrentStackTrace) + { + var result = new DebugStackTrace(options); + + var frames = result.CreateFrames(stackTrace, isCurrentStackTrace) + .Reverse(); // Sentry expects the frames to be sent in reversed order + + foreach (var frame in frames) + { + result.Frames.Add(frame); + } + + return result; + } + + internal void MergeDebugImagesInto(SentryEvent @event) + { + // This operation may only be run once because it is destructive to the object state; + // Frame indexes may be changed as well as _debugImageIndexByModule becoming invalid. + if (_debugImagesMerged) + { + throw new InvalidOperationException("Cannot call MergeDebugImagesInto multiple times"); + } + _debugImagesMerged = true; + + _options.LogDebug("Merging {0} debug images from stacktrace.", DebugImages.Count); + if (DebugImages.Count == 0) + { + return; + } + + @event.DebugImages ??= new(); + + if (@event.DebugImages.Count == 0) + { + // Default case when there's just a single stacktrace (i.e. no inner exceptions). + @event.DebugImages.AddRange(DebugImages); + return; + } + + // Otherwise, we must merge new images into an existing list, which means their relative indexes change. + // Therefore, we must also update indices specified in frame.AddressMode for each affected frame. + // We could just append _debugImages to @event.DebugImages and shift all indexes but merging is simple too. + var originalCount = @event.DebugImages.Count; + Dictionary relocations = new(); + for (var i = 0; i < DebugImages.Count; i++) + { + // First check if the image is already present on the event. Simple lookup should be faster than + // constructing a map first, assuming there are normally just a few debug images affected. + var found = false; + for (var j = 0; j < originalCount; j++) + { + if (DebugImages[i].ModuleVersionId == @event.DebugImages[j].ModuleVersionId) + { + if (i != j) + { + relocations.Add(DebugStackTrace.GetRelativeAddressMode(i), DebugStackTrace.GetRelativeAddressMode(j)); + } + found = true; + } + } + + if (!found) + { + relocations.Add(DebugStackTrace.GetRelativeAddressMode(i), DebugStackTrace.GetRelativeAddressMode(@event.DebugImages.Count)); + @event.DebugImages.Add(DebugImages[i]); + } + } + + foreach (var frame in Frames) + { + if (frame.AddressMode is not null && relocations.TryGetValue(frame.AddressMode, out var newAddressMode)) + { + frame.AddressMode = newAddressMode; + } + } + } + + /// + /// Creates an enumerator of from a . + /// + private IEnumerable CreateFrames(StackTrace stackTrace, bool isCurrentStackTrace) + { + var frames = _options.StackTraceMode switch + { + StackTraceMode.Enhanced => EnhancedStackTrace.GetFrames(stackTrace).Select(p => p as StackFrame), + _ => stackTrace.GetFrames() + // error CS8619: Nullability of reference types in value of type 'StackFrame?[]' doesn't match target type 'IEnumerable'. +#if NETCOREAPP3_0 + .Where(f => f is not null) +#endif + }; + + // Not to throw on code that ignores nullability warnings. + if (frames.IsNull()) + { + _options.LogDebug("No stack frames found. AttachStacktrace: '{0}', isCurrentStackTrace: '{1}'", + _options.AttachStacktrace, isCurrentStackTrace); + + yield break; + } + + Debug.Assert(frames != null); + + var firstFrame = true; + foreach (var stackFrame in frames) + { +#if !NET5_0_OR_GREATER + if (stackFrame is null) + { + continue; + } +#endif + + // Remove the frames until the call for capture with the SDK + if (firstFrame + && isCurrentStackTrace + && stackFrame.GetMethod() is { } method + && method.DeclaringType?.AssemblyQualifiedName?.StartsWith("Sentry") == true) + { + _options.LogDebug("Skipping initial stack frame '{0}'", method.Name); + continue; + } + + firstFrame = false; + + yield return CreateFrame(stackFrame); + } + } + + /// + /// Create a from a . + /// + internal SentryStackFrame CreateFrame(StackFrame stackFrame) => InternalCreateFrame(stackFrame, true); + + /// + /// Default the implementation of CreateFrame. + /// + private SentryStackFrame InternalCreateFrame(StackFrame stackFrame, bool demangle) + { + const string unknownRequiredField = "(unknown)"; + string? projectPath = null; + var frame = new SentryStackFrame(); + if (stackFrame.GetMethod() is { } method) + { + frame.Module = method.DeclaringType?.FullName ?? unknownRequiredField; + frame.Package = method.DeclaringType?.Assembly.FullName; + + if (_options.StackTraceMode == StackTraceMode.Enhanced && + stackFrame is EnhancedStackFrame enhancedStackFrame) + { + var stringBuilder = new StringBuilder(); + frame.Function = enhancedStackFrame.MethodInfo.Append(stringBuilder, false).ToString(); + + if (enhancedStackFrame.MethodInfo.DeclaringType is { } declaringType) + { + stringBuilder.Clear(); + stringBuilder.AppendTypeDisplayName(declaringType); + frame.Module = stringBuilder.ToString(); + } + } + else + { + frame.Function = method.Name; + } + + // Originally we didn't skip methods from dynamic assemblies, so not to break compatibility: + if (_options.StackTraceMode != StackTraceMode.Original && method.Module.Assembly.IsDynamic) + { + frame.InApp = false; + } + + AttributeReader.TryGetProjectDirectory(method.Module.Assembly, out projectPath); + + if (AddDebugImage(method.Module) is { } moduleIdx && moduleIdx != DebugImageMissing) + { + frame.AddressMode = DebugStackTrace.GetRelativeAddressMode(moduleIdx); + + try + { + var token = method.MetadataToken; + // The top byte is the token type, the lower three bytes are the record id. + // See: https://docs.microsoft.com/en-us/previous-versions/dotnet/netframework-4.0/ms404456(v=vs.100)#metadata-token-structure + var tokenType = token & 0xff000000; + // See https://docs.microsoft.com/en-us/dotnet/framework/unmanaged-api/metadata/cortokentype-enumeration + if (tokenType == 0x06000000) // CorTokenType.mdtMethodDef + { + var recordId = token & 0x00ffffff; + frame.FunctionId = $"0x{recordId:x}"; + } + } + catch (InvalidOperationException) + { + // method.MetadataToken may throw + // see https://learn.microsoft.com/en-us/dotnet/api/system.reflection.memberinfo.metadatatoken?view=net-6.0 + _options.LogDebug("Could not get MetadataToken for stack frame {0} from {1}", frame.Function, method.Module.Name); + } + } + } + + frame.ConfigureAppFrame(_options); + + if (stackFrame.GetFileName() is { } frameFileName) + { + if (projectPath != null && frameFileName.StartsWith(projectPath, StringComparison.OrdinalIgnoreCase)) + { + frameFileName = frameFileName.Substring(projectPath.Length); + } + frame.FileName = frameFileName; + } + + // stackFrame.HasILOffset() throws NotImplemented on Mono 5.12 + var ilOffset = stackFrame.GetILOffset(); + if (ilOffset != StackFrame.OFFSET_UNKNOWN) + { + frame.InstructionAddress = $"0x{ilOffset:x}"; + } + + var lineNo = stackFrame.GetFileLineNumber(); + if (lineNo > 0) + { + frame.LineNumber = lineNo; + } + + var colNo = stackFrame.GetFileColumnNumber(); + if (lineNo > 0) + { + frame.ColumnNumber = colNo; + } + + if (demangle && _options.StackTraceMode != StackTraceMode.Enhanced) + { + DemangleAsyncFunctionName(frame); + DemangleAnonymousFunction(frame); + DemangleLambdaReturnType(frame); + } + + if (_options.StackTraceMode == StackTraceMode.Enhanced) + { + // 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 + frame.Module = null; + } + + return frame; + } + + private static string GetRelativeAddressMode(int moduleIndex) => $"rel:{moduleIndex}"; + + /// + /// Clean up function and module names produced from `async` state machine calls. + /// + /// + /// When the Microsoft cs.exe compiler compiles some modern C# features, + /// such as async/await calls, it can create synthetic function names that + /// do not match the function names in the original source code. Here we + /// reverse some of these transformations, so that the function and module + /// names that appears in the Sentry UI will match the function and module + /// names in the original source-code. + /// + private static void DemangleAsyncFunctionName(SentryStackFrame frame) + { + if (frame.Module == null || frame.Function != "MoveNext") + { + return; + } + + // Search for the function name in angle brackets followed by d__. + // + // Change: + // RemotePrinterService+d__24 in MoveNext at line 457:13 + // to: + // RemotePrinterService in UpdateNotification at line 457:13 + + var match = RegexAsyncFunctionName.Match(frame.Module); + if (match is {Success: true, Groups.Count: 3}) + { + frame.Module = match.Groups[1].Value; + frame.Function = match.Groups[2].Value; + } + } + + /// + /// Clean up function names for anonymous lambda calls. + /// + internal static void DemangleAnonymousFunction(SentryStackFrame frame) + { + if (frame.Function == null) + { + return; + } + + // Search for the function name in angle brackets followed by b__. + // + // Change: + // b__36 + // to: + // BeginInvokeAsynchronousActionMethod { } + + var match = RegexAnonymousFunction.Match(frame.Function); + if (match is {Success: true, Groups.Count: 2}) + { + frame.Function = match.Groups[1].Value + " { }"; + } + } + + /// + /// Remove return type from module in a Task with a Lambda with a return value. + /// This was seen in Unity, see https://github.com/getsentry/sentry-unity/issues/845 + /// + private static void DemangleLambdaReturnType(SentryStackFrame frame) + { + if (frame.Module == null) + { + return; + } + + // Change: + // System.Threading.Tasks.Task`1[[System.Int32, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]] in InnerInvoke + // or System.Collections.Generic.List`1[[UnityEngine.Events.PersistentCall, UnityEngine.CoreModule, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null]] in get_Item + // to: + // 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}) + { + frame.Module = match.Groups[1].Value; + } + } + +#if ANDROID + private static IAndroidAssemblyReader? GetAndroidAssemblyReader(SentryOptions options) + { + var apkPath = Environment.CommandLine; + try + { + if (File.Exists(apkPath)) + { + var supportedAbis = AndroidHelpers.GetSupportedAbis(); + return AndroidAssemblyReaderFactory.Open(apkPath, supportedAbis, options.DiagnosticLogger); + } + + options.LogWarning( + "Cannot create AssemblyReader: cannot read APK path from Environment.CommandLine={0}", + apkPath); + } + catch (Exception e) + { + options.LogError("Cannot create AssemblyReader: {0}", e); + } + + return null; + } +#endif + + private PEReader? TryReadAssembly(string assemblyName) + { +#if ANDROID + return _assemblyReader.Value?.TryReadAssembly(assemblyName); +#else + return File.Exists(assemblyName) ? new PEReader(File.OpenRead(assemblyName)) : null; +#endif + } + + private int? AddDebugImage(Module module) + { + var id = module.ModuleVersionId; + + if (_debugImageIndexByModule.TryGetValue(id, out var idx)) + { + return idx; + } + + var assemblyName = module.FullyQualifiedName; + using var peReader = TryReadAssembly(assemblyName); + if (peReader is null) + { + _options.LogDebug("Skipping DebugImage for module '{0}' because assembly wasn't found: '{1}'", + module.Name, assemblyName); + _debugImageIndexByModule.Add(id, DebugImageMissing); // don't try to resolve again + return null; + } + + string? codeId = null; + var headers = peReader.PEHeaders; + if (headers.PEHeader is { } peHeader) + { + codeId = $"{headers.CoffHeader.TimeDateStamp:X8}{peHeader.SizeOfImage:x}"; + } + + string? debugId = null; + string? debugFile = null; + string? debugChecksum = null; + + var debugDirs = peReader.ReadDebugDirectory(); + foreach (var entry in debugDirs) + { + if (entry.Type == DebugDirectoryEntryType.PdbChecksum) + { + var checksum = peReader.ReadPdbChecksumDebugDirectoryData(entry); + var checksumHex = checksum.Checksum.AsSpan().ToHexString(); + debugChecksum = $"{checksum.AlgorithmName}:{checksumHex}"; + } + if (!entry.IsPortableCodeView) + { + continue; + } + var codeView = peReader.ReadCodeViewDebugDirectoryData(entry); + + // Together 16B of the Guid concatenated with 4B of the TimeDateStamp field of the entry form a PDB ID that + // should be used to match the PE/COFF image with the associated PDB (instead of Guid and Age). + // Matching PDB ID is stored in the #Pdb stream of the .pdb file. + // See https://github.com/dotnet/runtime/blob/main/docs/design/specs/PE-COFF.md#codeview-debug-directory-entry-type-2 + debugId = $"{codeView.Guid}-{entry.Stamp:x}"; + debugFile = codeView.Path; + } + + // well, we are out of luck :-( + if (debugId == null) + { + _options.LogWarning("Skipping DebugImage for module '{0}' because DebugId couldn't be determined", module.Name); + _debugImageIndexByModule.Add(id, DebugImageMissing); // don't try to resolve again + return null; + } + + idx = DebugImages.Count; + DebugImages.Add(new DebugImage + { + Type = "pe_dotnet", + CodeId = codeId, + CodeFile = assemblyName, + DebugId = debugId, + DebugChecksum = debugChecksum, + DebugFile = debugFile, + ModuleVersionId = id, + }); + _debugImageIndexByModule.Add(id, idx); + + return idx; + } +} diff --git a/src/Sentry/Internal/Extensions/HashExtensions.cs b/src/Sentry/Internal/Extensions/HashExtensions.cs index e76ce83f4f..dfaa149917 100644 --- a/src/Sentry/Internal/Extensions/HashExtensions.cs +++ b/src/Sentry/Internal/Extensions/HashExtensions.cs @@ -2,23 +2,11 @@ namespace Sentry.Internal.Extensions; internal static class HashExtensions { - private static string GetHexString(this byte[] data) - { - var buffer = new StringBuilder(); - - foreach (var t in data) - { - buffer.Append(t.ToString("X2")); - } - - return buffer.ToString(); - } - public static string GetHashString(this string str) { var bytes = Encoding.UTF8.GetBytes(str); using var sha = SHA1.Create(); var hash = sha.ComputeHash(bytes); - return hash.GetHexString(); + return hash.ToHexString(); } } diff --git a/src/Sentry/Internal/Extensions/MiscExtensions.cs b/src/Sentry/Internal/Extensions/MiscExtensions.cs index 55615dd76a..97145ed0ae 100644 --- a/src/Sentry/Internal/Extensions/MiscExtensions.cs +++ b/src/Sentry/Internal/Extensions/MiscExtensions.cs @@ -9,8 +9,27 @@ internal static class MiscExtensions ? value : null; - public static string ToHexString(this long l) => - "0x" + l.ToString("x", CultureInfo.InvariantCulture); + public static string ToHexString(this long l) => "0x" + l.ToString("x", CultureInfo.InvariantCulture); + + public static string ToHexString(this byte[] bytes) => new ReadOnlySpan(bytes).ToHexString(); + + public static string ToHexString(this Span bytes) => ((ReadOnlySpan)bytes).ToHexString(); + + public static string ToHexString(this ReadOnlySpan bytes) + { +#if NET5_0_OR_GREATER + return Convert.ToHexString(bytes).ToLowerInvariant(); +#else + var buffer = new StringBuilder(bytes.Length * 2); + + foreach (var t in bytes) + { + buffer.Append(t.ToString("x2", CultureInfo.InvariantCulture)); + } + + return buffer.ToString(); +#endif + } private static readonly TimeSpan MaxTimeout = TimeSpan.FromMilliseconds(int.MaxValue); diff --git a/src/Sentry/Internal/MainSentryEventProcessor.cs b/src/Sentry/Internal/MainSentryEventProcessor.cs index 0e55b07e71..ce537f9a6f 100644 --- a/src/Sentry/Internal/MainSentryEventProcessor.cs +++ b/src/Sentry/Internal/MainSentryEventProcessor.cs @@ -85,7 +85,8 @@ public SentryEvent Process(SentryEvent @event) if (@event.Exception == null) { - var stackTrace = SentryStackTraceFactoryAccessor().Create(@event.Exception); + // if there's no exception, get the current stack trace + var stackTrace = SentryStackTraceFactoryAccessor().Create(); if (stackTrace != null) { var currentThread = Thread.CurrentThread; @@ -101,6 +102,23 @@ public SentryEvent Process(SentryEvent @event) @event.SentryThreads = @event.SentryThreads?.Any() == true ? new List(@event.SentryThreads) { thread } : new[] { thread }.AsEnumerable(); + + if (stackTrace is DebugStackTrace debugStackTrace) + { + debugStackTrace.MergeDebugImagesInto(@event); + } + } + } + + // Add all the Debug Images that were referenced from stack traces to the Event. + if (@event.SentryExceptions is { } sentryExceptions) + { + foreach (var sentryException in sentryExceptions) + { + if (sentryException.Stacktrace is DebugStackTrace debugStackTrace) + { + debugStackTrace.MergeDebugImagesInto(@event); + } } } diff --git a/src/Sentry/Internal/MonoSentryStackTraceFactory.cs b/src/Sentry/Internal/MonoSentryStackTraceFactory.cs deleted file mode 100644 index 8d7ffe5aeb..0000000000 --- a/src/Sentry/Internal/MonoSentryStackTraceFactory.cs +++ /dev/null @@ -1,74 +0,0 @@ -using Sentry.Extensibility; - -namespace Sentry.Internal; - -/// -/// Mono factory to from an . -/// -internal class MonoSentryStackTraceFactory : SentryStackTraceFactory -{ - private readonly SentryOptions _options; - - /// - /// Creates an instance of . - /// - public MonoSentryStackTraceFactory(SentryOptions options) : base(options) => _options = options; - - /// - /// Creates a from the optional . - /// - /// The exception to create the stacktrace from. - /// A Sentry stack trace. - public override SentryStackTrace? Create(Exception? exception = null) - { - if (exception == null) - { - _options.LogDebug("No Exception to collect Mono stack trace."); - - // ReSharper disable once RedundantArgumentDefaultValue - return base.Create(null); - } - - List? frames = null; - if (exception.StackTrace is { } stacktrace) - { - foreach (var line in stacktrace.Split('\n')) - { - if (StackFrameData.TryParse(line, out var frame)) - { - frames ??= new List(); - frames.Add(frame); - } - } - } - - if (frames == null) - { - _options.LogWarning("Couldn't resolve a Mono stacktrace, calling fallback"); - return base.Create(exception); - } - - return new SentryStackTrace - { - // https://develop.sentry.dev/sdk/event-payloads/stacktrace/ - Frames = frames.Select(f => new SentryStackFrame - { - Module = f.TypeFullName, - InstructionOffset = f.Offset != 0 ? f.Offset : null, - Function = f.MethodSignature, - LineNumber = GetLineNumber(f.Line), - }).Reverse().ToArray() - }; - - static int? GetLineNumber(string line) - { - // Protocol is uint. Also, Mono AOT / IL2CPP / no pdb means no line number (0) which isn't useful. - if (line is { } l && int.TryParse(l, out var parsedLine) && parsedLine >= 0) - { - return parsedLine; - } - - return null; - } - } -} diff --git a/src/Sentry/Platforms/Android/AndroidHelpers.cs b/src/Sentry/Platforms/Android/AndroidHelpers.cs new file mode 100644 index 0000000000..130525c823 --- /dev/null +++ b/src/Sentry/Platforms/Android/AndroidHelpers.cs @@ -0,0 +1,21 @@ +namespace Sentry.Android; + +internal static class AndroidHelpers +{ + public static string? GetCpuAbi() => GetSupportedAbis().FirstOrDefault(); + + public static IList GetSupportedAbis() + { + var result = AndroidBuild.SupportedAbis; + if (result != null) + { + return result; + } + +#pragma warning disable CS0618 + var abi = AndroidBuild.CpuAbi; +#pragma warning restore CS0618 + + return abi != null ? new[] {abi} : Array.Empty(); + } +} diff --git a/src/Sentry/Platforms/Android/AssemblyReader/AndroidAssemblyDirectoryReader.cs b/src/Sentry/Platforms/Android/AssemblyReader/AndroidAssemblyDirectoryReader.cs new file mode 100644 index 0000000000..d7e69e1e59 --- /dev/null +++ b/src/Sentry/Platforms/Android/AssemblyReader/AndroidAssemblyDirectoryReader.cs @@ -0,0 +1,54 @@ +using System.Reflection.PortableExecutable; +using Sentry.Extensibility; + +namespace Sentry.Android.AssemblyReader; + +// The "Old" app type - where each DLL is placed in the 'assemblies' directory as an individual file. +internal sealed class AndroidAssemblyDirectoryReader : AndroidAssemblyReader, IAndroidAssemblyReader +{ + public AndroidAssemblyDirectoryReader(ZipArchive zip, IList supportedAbis, IDiagnosticLogger? logger) + : base(zip, supportedAbis, logger) { } + + public PEReader? TryReadAssembly(string name) + { + var zipEntry = FindAssembly(name); + if (zipEntry is null) + { + Logger?.LogDebug("Couldn't find assembly {0} in the APK", name); + return null; + } + + Logger?.LogDebug("Resolved assembly {0} in the APK at {1}", name, zipEntry.FullName); + + // We need a seekable stream for the PEReader (or even to check whether the DLL is compressed), so make a copy. + var memStream = new MemoryStream((int)zipEntry.Length); + using (var zipStream = zipEntry.Open()) + { + zipStream.CopyTo(memStream); + memStream.Position = 0; + } + return CreatePEReader(name, memStream); + } + + private ZipArchiveEntry? FindAssembly(string name) + { + var zipEntry = ZipArchive.GetEntry($"assemblies/{name}"); + + if (zipEntry is null) + { + foreach (var abi in SupportedAbis) + { + if (abi.Length > 0) + { + zipEntry = ZipArchive.GetEntry($"assemblies/{abi}/{name}"); + if (zipEntry is not null) + { + break; + } + } + } + } + + return zipEntry; + } +} diff --git a/src/Sentry/Platforms/Android/AssemblyReader/AndroidAssemblyReader.cs b/src/Sentry/Platforms/Android/AssemblyReader/AndroidAssemblyReader.cs new file mode 100644 index 0000000000..ac92e42155 --- /dev/null +++ b/src/Sentry/Platforms/Android/AssemblyReader/AndroidAssemblyReader.cs @@ -0,0 +1,89 @@ +using System.Reflection.PortableExecutable; +using Sentry.Extensibility; +using K4os.Compression.LZ4; + +namespace Sentry.Android.AssemblyReader; + +internal abstract class AndroidAssemblyReader : IDisposable +{ + protected IDiagnosticLogger? Logger { get; } + protected ZipArchive ZipArchive { get; } + protected IList SupportedAbis { get; } + + protected AndroidAssemblyReader(ZipArchive zip, IList supportedAbis, IDiagnosticLogger? logger) + { + ZipArchive = zip; + Logger = logger; + SupportedAbis = supportedAbis; + } + + public void Dispose() + { + ZipArchive.Dispose(); + } + + protected PEReader CreatePEReader(string assemblyName, MemoryStream inputStream) + { + var decompressedStream = TryDecompressLZ4(assemblyName, inputStream); + + // Use the decompressed stream, or if null, i.e. it wasn't compressed, use the original. + return new PEReader(decompressedStream ?? inputStream); + } + + /// + /// The DLL may be LZ4 compressed, see https://github.com/xamarin/xamarin-android/pull/4686 + /// The format is: + /// [ 4 byte magic header ] (XALZ) + /// [ 4 byte header index ] + /// [ 4 byte uncompressed payload length ] + /// [rest: lz4 compressed payload] + /// + /// + private Stream? TryDecompressLZ4(string assemblyName, MemoryStream inputStream) + { + const uint compressedDataMagic = 0x5A4C4158; // 'XALZ', little-endian + const int payloadOffset = 12; + var reader = new BinaryReader(inputStream); + if (reader.ReadUInt32() != compressedDataMagic) + { + // Restore the input stream to the beginning if we're not decompressing. + inputStream.Position = 0; + return null; + } + reader.ReadUInt32(); // ignore descriptor index, we don't need it + var decompressedLength = reader.ReadInt32(); + Debug.Assert(inputStream.Position == payloadOffset); + var inputLength = (int)(inputStream.Length - payloadOffset); + + Logger?.LogDebug("Decompressing assembly ({0} bytes uncompressed) using LZ4", decompressedLength); + + var outputStream = new MemoryStream(decompressedLength); + + // We're writing to the underlying array manually, so we need to set the length. + outputStream.SetLength(decompressedLength); + var outputBuffer = outputStream.GetBuffer(); + + var inputBuffer = inputStream is MemorySlice slice ? slice.FullBuffer : inputStream.GetBuffer(); + var offset = inputStream is MemorySlice memorySlice ? memorySlice.Offset + payloadOffset : payloadOffset; + var decoded = LZ4Codec.Decode(inputBuffer, offset, inputLength, outputBuffer, 0, decompressedLength); + if (decoded != decompressedLength) + { + throw new Exception($"Failed to decompress LZ4 data of assembly {assemblyName} - decoded {decoded} instead of expected {decompressedLength} bytes"); + } + return outputStream; + } + + // Allows consumer to access the underlying buffer even if the MemoryStream is created as a slice over another. + // Plain MemoryStream would throw "MemoryStream's internal buffer cannot be accessed." + protected class MemorySlice : MemoryStream + { + public readonly int Offset; + public readonly byte[] FullBuffer; + + public MemorySlice(MemoryStream other, int offset, int size) : base(other.GetBuffer(), offset, size, writable: false) + { + Offset = offset; + FullBuffer = other.GetBuffer(); + } + } +} diff --git a/src/Sentry/Platforms/Android/AssemblyReader/AndroidAssemblyReaderFactory.cs b/src/Sentry/Platforms/Android/AssemblyReader/AndroidAssemblyReaderFactory.cs new file mode 100644 index 0000000000..6a54cd1f90 --- /dev/null +++ b/src/Sentry/Platforms/Android/AssemblyReader/AndroidAssemblyReaderFactory.cs @@ -0,0 +1,21 @@ +using Sentry.Extensibility; + +namespace Sentry.Android.AssemblyReader; + +internal static class AndroidAssemblyReaderFactory +{ + public static IAndroidAssemblyReader Open(string apkPath, IList supportedAbis, IDiagnosticLogger? logger) + { + logger?.LogDebug("Opening APK: {0}", apkPath); + var zipArchive = ZipFile.Open(apkPath, ZipArchiveMode.Read); + + if (zipArchive.GetEntry("assemblies/assemblies.manifest") is not null) + { + logger?.LogDebug("APK uses AssemblyStore"); + return new AndroidAssemblyStoreReader(zipArchive, supportedAbis, logger); + } + + logger?.LogDebug("APK doesn't use AssemblyStore"); + return new AndroidAssemblyDirectoryReader(zipArchive, supportedAbis, logger); + } +} diff --git a/src/Sentry/Platforms/Android/AssemblyReader/AndroidAssemblyStoreReader.cs b/src/Sentry/Platforms/Android/AssemblyReader/AndroidAssemblyStoreReader.cs new file mode 100644 index 0000000000..d25473e6b1 --- /dev/null +++ b/src/Sentry/Platforms/Android/AssemblyReader/AndroidAssemblyStoreReader.cs @@ -0,0 +1,513 @@ +using System.Reflection.PortableExecutable; +using Sentry.Extensibility; + +namespace Sentry.Android.AssemblyReader; + +// See https://devblogs.microsoft.com/dotnet/performance-improvements-in-dotnet-maui/#single-file-assembly-stores +internal sealed class AndroidAssemblyStoreReader : AndroidAssemblyReader, IAndroidAssemblyReader +{ + private readonly AssemblyStoreExplorer _explorer; + + public AndroidAssemblyStoreReader(ZipArchive zip, IList supportedAbis, IDiagnosticLogger? logger) + : base(zip, supportedAbis, logger) + { + _explorer = new(zip, supportedAbis, logger); + } + + public PEReader? TryReadAssembly(string name) + { + var assembly = TryFindAssembly(name); + if (assembly is null) + { + Logger?.LogDebug("Couldn't find assembly {0} in the APK AssemblyStore", name); + return null; + } + + Logger?.LogDebug("Resolved assembly {0} in the APK {1} AssemblyStore", name, assembly.Store.Arch); + + var stream = assembly.GetImage(); + if (stream is null) + { + Logger?.LogDebug("Couldn't access assembly {0} image stream", name); + return null; + } + + return CreatePEReader(name, stream); + } + + private AssemblyStoreAssembly? TryFindAssembly(string name) + { + if (_explorer.AssembliesByName.TryGetValue(name, out var assembly)) + { + return assembly; + } + + if (name.EndsWith(".dll", ignoreCase: true, CultureInfo.InvariantCulture) || + name.EndsWith(".exe", ignoreCase: true, CultureInfo.InvariantCulture)) + { + if (_explorer.AssembliesByName.TryGetValue(name.Substring(0, name.Length - 4), out assembly)) + { + return assembly; + } + } + + return null; + } + + // Adapted from https://github.com/xamarin/xamarin-android/blob/c92702619f5fabcff0ed88e09160baf9edd70f41/tools/assembly-store-reader/AssemblyStoreExplorer.cs + // With the original code licensed under MIT License (https://github.com/xamarin/xamarin-android/blob/2bd13c4a00ae78db34663a4b9c7a4c5bfb20c344/LICENSE). + private class AssemblyStoreExplorer + { + private AssemblyStoreReader? _indexStore; + private readonly AssemblyStoreManifestReader _manifest; + private readonly IDiagnosticLogger? _logger; + + public IDictionary AssembliesByName { get; } = + new SortedDictionary(StringComparer.OrdinalIgnoreCase); + + public IDictionary AssembliesByHash32 { get; } = + new Dictionary(); + + public IDictionary AssembliesByHash64 { get; } = + new Dictionary(); + + public List Assemblies { get; } = new(); + + public IDictionary> Stores { get; } = + new SortedDictionary>(); + + public AssemblyStoreExplorer(ZipArchive zip, IList supportedAbis, IDiagnosticLogger? logger) + { + _logger = logger; + _manifest = new AssemblyStoreManifestReader(zip.GetEntry("assemblies/assemblies.manifest")!.Open()); + + TryAddStore(zip, null); + foreach (var abi in supportedAbis) + { + if (!string.IsNullOrEmpty(abi)) + { + TryAddStore(zip, abi); + } + } + + zip.Dispose(); + ProcessStores(); + } + + private void ProcessStores() + { + if (Stores.Count == 0 || _indexStore == null) + { + return; + } + + ProcessIndex(_indexStore.GlobalIndex32, "32", (he, assembly) => + { + assembly.Hash32 = (uint)he.Hash; + assembly.RuntimeIndex = he.MappingIndex; + + if (_manifest.EntriesByHash32.TryGetValue(assembly.Hash32, out var me)) + { + assembly.Name = me.Name; + } + + if (!AssembliesByHash32.ContainsKey(assembly.Hash32)) + { + AssembliesByHash32.Add(assembly.Hash32, assembly); + } + }); + + ProcessIndex(_indexStore.GlobalIndex64, "64", (he, assembly) => + { + assembly.Hash64 = he.Hash; + if (assembly.RuntimeIndex != he.MappingIndex) + { + _logger?.LogDebug( + $"assembly with hashes 0x{assembly.Hash32} and 0x{assembly.Hash64} has a different 32-bit runtime index ({assembly.RuntimeIndex}) than the 64-bit runtime index({he.MappingIndex})"); + } + + if (_manifest.EntriesByHash64.TryGetValue(assembly.Hash64, out var me)) + { + if (string.IsNullOrEmpty(assembly.Name)) + { + _logger?.LogDebug( + $"32-bit hash 0x{assembly.Hash32:x} did not match any assembly name in the manifest"); + assembly.Name = me.Name; + if (string.IsNullOrEmpty(assembly.Name)) + { + _logger?.LogDebug( + $"64-bit hash 0x{assembly.Hash64:x} did not match any assembly name in the manifest"); + } + } + else if (!string.Equals(assembly.Name, me.Name, StringComparison.Ordinal)) + { + _logger?.LogDebug( + $"32-bit hash 0x{assembly.Hash32:x} maps to assembly name '{assembly.Name}', however 64-bit hash 0x{assembly.Hash64:x} for the same entry matches assembly name '{me.Name}'"); + } + } + + if (!AssembliesByHash64.ContainsKey(assembly.Hash64)) + { + AssembliesByHash64.Add(assembly.Hash64, assembly); + } + }); + + foreach (var kvp in Stores) + { + var list = kvp.Value; + if (list.Count < 2) + { + continue; + } + + var template = list[0]; + for (var i = 1; i < list.Count; i++) + { + var other = list[i]; + if (!template.HasIdenticalContent(other)) + { + throw new Exception( + $"Store ID {template.StoreId} for architecture {other.Arch} is not identical to other stores with the same ID"); + } + } + } + + void ProcessIndex(List index, string bitness, + Action assemblyHandler) + { + foreach (var he in index) + { + if (!Stores.TryGetValue(he.StoreId, out var storeList)) + { + _logger?.LogDebug($"store with id {he.StoreId} not part of the set"); + continue; + } + + foreach (var store in storeList) + { + if (he.LocalStoreIndex >= (uint)store.Assemblies.Count) + { + _logger?.LogDebug( + $"{bitness}-bit index entry with hash 0x{he.Hash:x} has invalid store {store.StoreId} index {he.LocalStoreIndex} (maximum allowed is {store.Assemblies.Count})"); + continue; + } + + var assembly = store.Assemblies[(int)he.LocalStoreIndex]; + assemblyHandler(he, assembly); + + if (!AssembliesByName.ContainsKey(assembly.Name)) + { + AssembliesByName.Add(assembly.Name, assembly); + } + } + } + } + } + + private void TryAddStore(ZipArchive archive, string? abi) + { + var infix = abi is null ? "" : $".{abi}"; + if (archive.GetEntry($"assemblies/assemblies{infix}.blob") is { } zipEntry) + { + var memStream = new MemoryStream((int)zipEntry.Length); + using (var zipStream = zipEntry.Open()) + { + zipStream.CopyTo(memStream); + memStream.Position = 0; + } + + AddStore(new AssemblyStoreReader(memStream, abi)); + } + } + + private void AddStore(AssemblyStoreReader reader) + { + if (reader.HasGlobalIndex) + { + _indexStore = reader; + } + + if (!Stores.TryGetValue(reader.StoreId, out var storeList)) + { + storeList = new List(); + Stores.Add(reader.StoreId, storeList); + } + + storeList.Add(reader); + + Assemblies.AddRange(reader.Assemblies); + } + } + + // Adapted from https://github.com/xamarin/xamarin-android/blob/c92702619f5fabcff0ed88e09160baf9edd70f41/tools/assembly-store-reader/AssemblyStoreManifestReader.cs + // With the original code licensed under MIT License (https://github.com/xamarin/xamarin-android/blob/2bd13c4a00ae78db34663a4b9c7a4c5bfb20c344/LICENSE). + private class AssemblyStoreManifestReader + { + public List Entries { get; } = new(); + public Dictionary EntriesByHash32 { get; } = new(); + public Dictionary EntriesByHash64 { get; } = new(); + + public AssemblyStoreManifestReader(Stream manifest) + { + using var sr = new StreamReader(manifest, Encoding.UTF8, detectEncodingFromByteOrderMarks: false); + ReadManifest(sr); + } + + private void ReadManifest(StreamReader reader) + { + // First line is ignored, it contains headers + reader.ReadLine(); + + // Each subsequent line consists of fields separated with any number of spaces (for the pleasure of a human being reading the manifest) + while (!reader.EndOfStream) + { + var fields = reader.ReadLine()?.Split(' ', StringSplitOptions.RemoveEmptyEntries); + if (fields == null) + { + continue; + } + + var entry = new AssemblyStoreManifestEntry(fields); + Entries.Add(entry); + if (entry.Hash32 != 0) + { + EntriesByHash32.Add(entry.Hash32, entry); + } + + if (entry.Hash64 != 0) + { + EntriesByHash64.Add(entry.Hash64, entry); + } + } + } + } + + // Adapted from https://github.com/xamarin/xamarin-android/blob/c92702619f5fabcff0ed88e09160baf9edd70f41/tools/assembly-store-reader/AssemblyStoreManifestEntry.cs + // With the original code licensed under MIT License (https://github.com/xamarin/xamarin-android/blob/2bd13c4a00ae78db34663a4b9c7a4c5bfb20c344/LICENSE). + private class AssemblyStoreManifestEntry + { + // Fields are: + // Hash 32 | Hash 64 | Store ID | Store idx | Name + private const int NumberOfFields = 5; + private const int Hash32FieldIndex = 0; + private const int Hash64FieldIndex = 1; + private const int StoreIdFieldIndex = 2; + private const int StoreIndexFieldIndex = 3; + private const int NameFieldIndex = 4; + + public uint Hash32 { get; } + public ulong Hash64 { get; } + public uint StoreId { get; } + public uint IndexInStore { get; } + public string Name { get; } + + public AssemblyStoreManifestEntry(string[] fields) + { + if (fields.Length != NumberOfFields) + { + throw new ArgumentOutOfRangeException(nameof(fields), "Invalid number of fields"); + } + + Hash32 = GetUInt32(fields[Hash32FieldIndex]); + Hash64 = GetUInt64(fields[Hash64FieldIndex]); + StoreId = GetUInt32(fields[StoreIdFieldIndex]); + IndexInStore = GetUInt32(fields[StoreIndexFieldIndex]); + Name = fields[NameFieldIndex].Trim(); + } + + private static uint GetUInt32(string value) + { + if (uint.TryParse(PrepHexValue(value), NumberStyles.HexNumber, null, out var hash)) + { + return hash; + } + + return 0; + } + + private static ulong GetUInt64(string value) + { + if (ulong.TryParse(PrepHexValue(value), NumberStyles.HexNumber, null, out var hash)) + { + return hash; + } + + return 0; + } + + private static string PrepHexValue(string value) + { + if (value.StartsWith("0x", StringComparison.Ordinal)) + { + return value.Substring(2); + } + + return value; + } + } + + // Adapted from https://github.com/xamarin/xamarin-android/blob/c92702619f5fabcff0ed88e09160baf9edd70f41/tools/assembly-store-reader/AssemblyStoreHashEntry.cs + // With the original code licensed under MIT License (https://github.com/xamarin/xamarin-android/blob/2bd13c4a00ae78db34663a4b9c7a4c5bfb20c344/LICENSE). + private class AssemblyStoreHashEntry + { + public bool Is32Bit { get; } + public ulong Hash { get; } + public uint MappingIndex { get; } + public uint LocalStoreIndex { get; } + public uint StoreId { get; } + + internal AssemblyStoreHashEntry(BinaryReader reader, bool is32Bit) + { + Is32Bit = is32Bit; + Hash = reader.ReadUInt64(); + MappingIndex = reader.ReadUInt32(); + LocalStoreIndex = reader.ReadUInt32(); + StoreId = reader.ReadUInt32(); + } + } + + // Adapted from https://github.com/xamarin/xamarin-android/blob/c92702619f5fabcff0ed88e09160baf9edd70f41/tools/assembly-store-reader/AssemblyStoreReader.cs + // With the original code licensed under MIT License (https://github.com/xamarin/xamarin-android/blob/2bd13c4a00ae78db34663a4b9c7a4c5bfb20c344/LICENSE). + private class AssemblyStoreReader + { + // These two constants must be identical to the native ones in src/monodroid/jni/xamarin-app.hh + private const uint ASSEMBLY_STORE_MAGIC = 0x41424158; // 'XABA', little-endian + private const uint ASSEMBLY_STORE_FORMAT_VERSION = 1; // The highest format version this reader understands + + private readonly MemoryStream _storeData; + + public uint Version { get; private set; } + public uint LocalEntryCount { get; private set; } + public uint GlobalEntryCount { get; private set; } + public uint StoreId { get; private set; } + public List Assemblies { get; } + public List GlobalIndex32 { get; } = new(); + public List GlobalIndex64 { get; } = new(); + public string Arch { get; } + + public bool HasGlobalIndex => StoreId == 0; + + public AssemblyStoreReader(MemoryStream store, string? arch = null) + { + Arch = arch ?? string.Empty; + _storeData = store; + using var reader = new BinaryReader(store, Encoding.UTF8, leaveOpen: true); + ReadHeader(reader); + + Assemblies = new List(); + ReadLocalEntries(reader, Assemblies); + if (HasGlobalIndex) + { + ReadGlobalIndex(reader, GlobalIndex32, GlobalIndex64); + } + } + + internal MemoryStream? GetAssemblyImageSlice(AssemblyStoreAssembly assembly) => + GetDataSlice(assembly.DataOffset, assembly.DataSize); + + internal MemoryStream? GetAssemblyDebugDataSlice(AssemblyStoreAssembly assembly) => + assembly.DebugDataOffset == 0 ? null : GetDataSlice(assembly.DebugDataOffset, assembly.DebugDataSize); + + internal MemoryStream? GetAssemblyConfigSlice(AssemblyStoreAssembly assembly) => + assembly.ConfigDataOffset == 0 ? null : GetDataSlice(assembly.ConfigDataOffset, assembly.ConfigDataSize); + + private MemoryStream? GetDataSlice(uint offset, uint size) => + size == 0 ? null : new MemorySlice(_storeData, (int)offset, (int)size); + + public bool HasIdenticalContent(AssemblyStoreReader other) + { + return + other.Version == Version && + other.LocalEntryCount == LocalEntryCount && + other.GlobalEntryCount == GlobalEntryCount && + other.StoreId == StoreId && + other.Assemblies.Count == Assemblies.Count && + other.GlobalIndex32.Count == GlobalIndex32.Count && + other.GlobalIndex64.Count == GlobalIndex64.Count; + } + + private void ReadHeader(BinaryReader reader) + { + if (reader.ReadUInt32() != ASSEMBLY_STORE_MAGIC) + { + throw new InvalidOperationException("Invalid header magic number"); + } + + Version = reader.ReadUInt32(); + if (Version == 0) + { + throw new InvalidOperationException("Invalid version number: 0"); + } + + if (Version > ASSEMBLY_STORE_FORMAT_VERSION) + { + throw new InvalidOperationException( + $"Store format version {Version} is higher than the one understood by this reader, {ASSEMBLY_STORE_FORMAT_VERSION}"); + } + + LocalEntryCount = reader.ReadUInt32(); + GlobalEntryCount = reader.ReadUInt32(); + StoreId = reader.ReadUInt32(); + } + + private void ReadLocalEntries(BinaryReader reader, List assemblies) + { + for (uint i = 0; i < LocalEntryCount; i++) + { + assemblies.Add(new AssemblyStoreAssembly(reader, this)); + } + } + + private void ReadGlobalIndex(BinaryReader reader, List index32, + List index64) + { + ReadIndex(true, index32); + ReadIndex(false, index64); + + void ReadIndex(bool is32Bit, List index) + { + for (uint i = 0; i < GlobalEntryCount; i++) + { + index.Add(new AssemblyStoreHashEntry(reader, is32Bit)); + } + } + } + } + + // Adapted from https://github.com/xamarin/xamarin-android/blob/c92702619f5fabcff0ed88e09160baf9edd70f41/tools/assembly-store-reader/AssemblyStoreAssembly.cs + // With the original code licensed under MIT License (https://github.com/xamarin/xamarin-android/blob/2bd13c4a00ae78db34663a4b9c7a4c5bfb20c344/LICENSE). + private class AssemblyStoreAssembly + { + public uint DataOffset { get; } + public uint DataSize { get; } + public uint DebugDataOffset { get; } + public uint DebugDataSize { get; } + public uint ConfigDataOffset { get; } + public uint ConfigDataSize { get; } + + public uint Hash32 { get; set; } + public ulong Hash64 { get; set; } + public string Name { get; set; } = string.Empty; + public uint RuntimeIndex { get; set; } + + public AssemblyStoreReader Store { get; } + + internal AssemblyStoreAssembly(BinaryReader reader, AssemblyStoreReader store) + { + Store = store; + + DataOffset = reader.ReadUInt32(); + DataSize = reader.ReadUInt32(); + DebugDataOffset = reader.ReadUInt32(); + DebugDataSize = reader.ReadUInt32(); + ConfigDataOffset = reader.ReadUInt32(); + ConfigDataSize = reader.ReadUInt32(); + } + + public MemoryStream? GetImage() => Store.GetAssemblyImageSlice(this); + + public MemoryStream? GetDebugData() => Store.GetAssemblyDebugDataSlice(this); + + public MemoryStream? GetAssemblyConfig() => Store.GetAssemblyConfigSlice(this); + } +} diff --git a/src/Sentry/Platforms/Android/AssemblyReader/IAndroidAssemblyReader.cs b/src/Sentry/Platforms/Android/AssemblyReader/IAndroidAssemblyReader.cs new file mode 100644 index 0000000000..874de9b422 --- /dev/null +++ b/src/Sentry/Platforms/Android/AssemblyReader/IAndroidAssemblyReader.cs @@ -0,0 +1,8 @@ +using System.Reflection.PortableExecutable; + +namespace Sentry.Android.AssemblyReader; + +internal interface IAndroidAssemblyReader : IDisposable +{ + PEReader? TryReadAssembly(string name); +} diff --git a/src/Sentry/Platforms/Android/Extensions/DeviceExtensions.cs b/src/Sentry/Platforms/Android/Extensions/DeviceExtensions.cs index 25bf9b9d84..b608480191 100644 --- a/src/Sentry/Platforms/Android/Extensions/DeviceExtensions.cs +++ b/src/Sentry/Platforms/Android/Extensions/DeviceExtensions.cs @@ -9,17 +9,7 @@ public static void ApplyFromAndroidRuntime(this Device device) device.Manufacturer ??= AndroidBuild.Manufacturer; device.Brand ??= AndroidBuild.Brand; device.Model ??= AndroidBuild.Model; - - if (AndroidBuild.SupportedAbis is { } abis) - { - device.Architecture ??= abis[0]; - } - else - { -#pragma warning disable CS0618 // Type or member is obsolete - device.Architecture ??= AndroidBuild.CpuAbi; -#pragma warning restore CS0618 // Type or member is obsolete - } + device.Architecture ??= AndroidHelpers.GetCpuAbi(); } public static void ApplyFromSentryAndroidSdk(this Device device, JavaSdk.Protocol.Device d) diff --git a/src/Sentry/Platforms/Android/Sentry.Android.props b/src/Sentry/Platforms/Android/Sentry.Android.props index 484d446d44..3b7d430f06 100644 --- a/src/Sentry/Platforms/Android/Sentry.Android.props +++ b/src/Sentry/Platforms/Android/Sentry.Android.props @@ -2,6 +2,7 @@ + diff --git a/src/Sentry/Properties/AssemblyInfo.cs b/src/Sentry/Properties/AssemblyInfo.cs index ea786af7e2..971cbd7de9 100644 --- a/src/Sentry/Properties/AssemblyInfo.cs +++ b/src/Sentry/Properties/AssemblyInfo.cs @@ -8,6 +8,7 @@ [assembly: InternalsVisibleTo("Sentry.NLog, PublicKey=002400000480000094000000060200000024000052534131000400000100010059964a931488bcdbd14657f1ee0df32df61b57b3d14d7290c262c2cc9ddaad6ec984044f761f778e1823049d2cb996a4f58c8ea5b46c37891414cb34b4036b1c178d7b582289d2eef3c0f1e9b692c229a306831ee3d371d9e883f0eb0f74aeac6c6ab8c85fd1ec04b267e15a31532c4b4e2191f5980459db4dce0081f1050fb8")] [assembly: InternalsVisibleTo("Sentry.NLog.Tests, PublicKey=002400000480000094000000060200000024000052534131000400000100010059964a931488bcdbd14657f1ee0df32df61b57b3d14d7290c262c2cc9ddaad6ec984044f761f778e1823049d2cb996a4f58c8ea5b46c37891414cb34b4036b1c178d7b582289d2eef3c0f1e9b692c229a306831ee3d371d9e883f0eb0f74aeac6c6ab8c85fd1ec04b267e15a31532c4b4e2191f5980459db4dce0081f1050fb8")] [assembly: InternalsVisibleTo("Sentry.Serilog.Tests, PublicKey=002400000480000094000000060200000024000052534131000400000100010059964a931488bcdbd14657f1ee0df32df61b57b3d14d7290c262c2cc9ddaad6ec984044f761f778e1823049d2cb996a4f58c8ea5b46c37891414cb34b4036b1c178d7b582289d2eef3c0f1e9b692c229a306831ee3d371d9e883f0eb0f74aeac6c6ab8c85fd1ec04b267e15a31532c4b4e2191f5980459db4dce0081f1050fb8")] +[assembly: InternalsVisibleTo("Sentry.Android.Tests, PublicKey=002400000480000094000000060200000024000052534131000400000100010059964a931488bcdbd14657f1ee0df32df61b57b3d14d7290c262c2cc9ddaad6ec984044f761f778e1823049d2cb996a4f58c8ea5b46c37891414cb34b4036b1c178d7b582289d2eef3c0f1e9b692c229a306831ee3d371d9e883f0eb0f74aeac6c6ab8c85fd1ec04b267e15a31532c4b4e2191f5980459db4dce0081f1050fb8")] [assembly: InternalsVisibleTo("Sentry.AspNet, PublicKey=002400000480000094000000060200000024000052534131000400000100010059964a931488bcdbd14657f1ee0df32df61b57b3d14d7290c262c2cc9ddaad6ec984044f761f778e1823049d2cb996a4f58c8ea5b46c37891414cb34b4036b1c178d7b582289d2eef3c0f1e9b692c229a306831ee3d371d9e883f0eb0f74aeac6c6ab8c85fd1ec04b267e15a31532c4b4e2191f5980459db4dce0081f1050fb8")] [assembly: InternalsVisibleTo("Sentry.AspNet.Tests, PublicKey=002400000480000094000000060200000024000052534131000400000100010059964a931488bcdbd14657f1ee0df32df61b57b3d14d7290c262c2cc9ddaad6ec984044f761f778e1823049d2cb996a4f58c8ea5b46c37891414cb34b4036b1c178d7b582289d2eef3c0f1e9b692c229a306831ee3d371d9e883f0eb0f74aeac6c6ab8c85fd1ec04b267e15a31532c4b4e2191f5980459db4dce0081f1050fb8")] [assembly: InternalsVisibleTo("Sentry.AspNetCore.Tests, PublicKey=002400000480000094000000060200000024000052534131000400000100010059964a931488bcdbd14657f1ee0df32df61b57b3d14d7290c262c2cc9ddaad6ec984044f761f778e1823049d2cb996a4f58c8ea5b46c37891414cb34b4036b1c178d7b582289d2eef3c0f1e9b692c229a306831ee3d371d9e883f0eb0f74aeac6c6ab8c85fd1ec04b267e15a31532c4b4e2191f5980459db4dce0081f1050fb8")] diff --git a/src/Sentry/SentryStackFrame.cs b/src/Sentry/SentryStackFrame.cs index 1c6766cd97..a04df6e451 100644 --- a/src/Sentry/SentryStackFrame.cs +++ b/src/Sentry/SentryStackFrame.cs @@ -138,6 +138,14 @@ public sealed class SentryStackFrame : IJsonSerializable /// public string? AddressMode { get; set; } + /// + /// The optional Function Id.
+ /// This is derived from the `MetadataToken`, and should be the record id + /// of a `MethodDef`.
+ /// This should be a string with a hexadecimal number that includes a 0x prefix.
+ ///
+ public string? FunctionId { get; set; } + /// public void WriteTo(Utf8JsonWriter writer, IDiagnosticLogger? logger) { @@ -162,6 +170,7 @@ public void WriteTo(Utf8JsonWriter writer, IDiagnosticLogger? logger) writer.WriteStringIfNotWhiteSpace("instruction_addr", InstructionAddress); writer.WriteNumberIfNotNull("instruction_offset", InstructionOffset); writer.WriteStringIfNotWhiteSpace("addr_mode", AddressMode); + writer.WriteStringIfNotWhiteSpace("function_id", FunctionId); writer.WriteEndObject(); } @@ -213,6 +222,7 @@ public static SentryStackFrame FromJson(JsonElement json) var instructionAddress = json.GetPropertyOrNull("instruction_addr")?.GetString(); var instructionOffset = json.GetPropertyOrNull("instruction_offset")?.GetInt64(); var addressMode = json.GetPropertyOrNull("addr_mode")?.GetString(); + var functionId = json.GetPropertyOrNull("function_id")?.GetString(); return new SentryStackFrame { @@ -235,6 +245,7 @@ public static SentryStackFrame FromJson(JsonElement json) InstructionAddress = instructionAddress, InstructionOffset = instructionOffset, AddressMode = addressMode, + FunctionId = functionId, }; } } diff --git a/src/Sentry/SentryStackTrace.cs b/src/Sentry/SentryStackTrace.cs index e4d9215ba4..0951a08e7b 100644 --- a/src/Sentry/SentryStackTrace.cs +++ b/src/Sentry/SentryStackTrace.cs @@ -11,7 +11,7 @@ namespace Sentry; /// Frames should be sorted from oldest to newest. /// /// -public sealed class SentryStackTrace : IJsonSerializable +public class SentryStackTrace : IJsonSerializable { internal IList? InternalFrames { get; private set; } diff --git a/test/AndroidTestApp/AndroidManifest.xml b/test/AndroidTestApp/AndroidManifest.xml new file mode 100644 index 0000000000..3fead98ca0 --- /dev/null +++ b/test/AndroidTestApp/AndroidManifest.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/test/AndroidTestApp/AndroidTestApp.csproj b/test/AndroidTestApp/AndroidTestApp.csproj new file mode 100644 index 0000000000..1ff3c71975 --- /dev/null +++ b/test/AndroidTestApp/AndroidTestApp.csproj @@ -0,0 +1,12 @@ + + + net7.0-android + 21 + Exe + enable + enable + com.companyname.AndroidTestApp + 1 + 1.0 + + diff --git a/test/AndroidTestApp/Directory.Build.props b/test/AndroidTestApp/Directory.Build.props new file mode 100644 index 0000000000..bbb7413d29 --- /dev/null +++ b/test/AndroidTestApp/Directory.Build.props @@ -0,0 +1,2 @@ + + diff --git a/test/AndroidTestApp/Directory.Build.targets b/test/AndroidTestApp/Directory.Build.targets new file mode 100644 index 0000000000..bbb7413d29 --- /dev/null +++ b/test/AndroidTestApp/Directory.Build.targets @@ -0,0 +1,2 @@ + + diff --git a/test/AndroidTestApp/MainActivity.cs b/test/AndroidTestApp/MainActivity.cs new file mode 100644 index 0000000000..4d26918933 --- /dev/null +++ b/test/AndroidTestApp/MainActivity.cs @@ -0,0 +1,13 @@ +namespace AndroidTestApp; + +[Activity(Label = "@string/app_name", MainLauncher = true)] +public class MainActivity : Activity +{ + protected override void OnCreate(Bundle? savedInstanceState) + { + base.OnCreate(savedInstanceState); + + // Set our view from the "main" layout resource + SetContentView(Resource.Layout.activity_main); + } +} \ No newline at end of file diff --git a/test/AndroidTestApp/Readme.md b/test/AndroidTestApp/Readme.md new file mode 100644 index 0000000000..289b49cab7 --- /dev/null +++ b/test/AndroidTestApp/Readme.md @@ -0,0 +1 @@ +This is a minimal Android app created by `dotnet new android`. It is used solely for testing by the `AndroidAssemblyReaderTests`. diff --git a/test/AndroidTestApp/Resources/layout/activity_main.xml b/test/AndroidTestApp/Resources/layout/activity_main.xml new file mode 100644 index 0000000000..f94985291b --- /dev/null +++ b/test/AndroidTestApp/Resources/layout/activity_main.xml @@ -0,0 +1,13 @@ + + + + \ No newline at end of file diff --git a/test/AndroidTestApp/Resources/mipmap-anydpi-v26/appicon.xml b/test/AndroidTestApp/Resources/mipmap-anydpi-v26/appicon.xml new file mode 100644 index 0000000000..7751f69514 --- /dev/null +++ b/test/AndroidTestApp/Resources/mipmap-anydpi-v26/appicon.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/test/AndroidTestApp/Resources/mipmap-anydpi-v26/appicon_round.xml b/test/AndroidTestApp/Resources/mipmap-anydpi-v26/appicon_round.xml new file mode 100644 index 0000000000..7751f69514 --- /dev/null +++ b/test/AndroidTestApp/Resources/mipmap-anydpi-v26/appicon_round.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/test/AndroidTestApp/Resources/mipmap-hdpi/appicon.png b/test/AndroidTestApp/Resources/mipmap-hdpi/appicon.png new file mode 100644 index 0000000000..f97217f4aa Binary files /dev/null and b/test/AndroidTestApp/Resources/mipmap-hdpi/appicon.png differ diff --git a/test/AndroidTestApp/Resources/mipmap-hdpi/appicon_background.png b/test/AndroidTestApp/Resources/mipmap-hdpi/appicon_background.png new file mode 100644 index 0000000000..513e69d841 Binary files /dev/null and b/test/AndroidTestApp/Resources/mipmap-hdpi/appicon_background.png differ diff --git a/test/AndroidTestApp/Resources/mipmap-hdpi/appicon_foreground.png b/test/AndroidTestApp/Resources/mipmap-hdpi/appicon_foreground.png new file mode 100644 index 0000000000..99d3a291bc Binary files /dev/null and b/test/AndroidTestApp/Resources/mipmap-hdpi/appicon_foreground.png differ diff --git a/test/AndroidTestApp/Resources/mipmap-mdpi/appicon.png b/test/AndroidTestApp/Resources/mipmap-mdpi/appicon.png new file mode 100644 index 0000000000..76ceb98d48 Binary files /dev/null and b/test/AndroidTestApp/Resources/mipmap-mdpi/appicon.png differ diff --git a/test/AndroidTestApp/Resources/mipmap-mdpi/appicon_background.png b/test/AndroidTestApp/Resources/mipmap-mdpi/appicon_background.png new file mode 100644 index 0000000000..9e2d1e4d8d Binary files /dev/null and b/test/AndroidTestApp/Resources/mipmap-mdpi/appicon_background.png differ diff --git a/test/AndroidTestApp/Resources/mipmap-mdpi/appicon_foreground.png b/test/AndroidTestApp/Resources/mipmap-mdpi/appicon_foreground.png new file mode 100644 index 0000000000..a28d342c1c Binary files /dev/null and b/test/AndroidTestApp/Resources/mipmap-mdpi/appicon_foreground.png differ diff --git a/test/AndroidTestApp/Resources/mipmap-xhdpi/appicon.png b/test/AndroidTestApp/Resources/mipmap-xhdpi/appicon.png new file mode 100644 index 0000000000..83f089c37b Binary files /dev/null and b/test/AndroidTestApp/Resources/mipmap-xhdpi/appicon.png differ diff --git a/test/AndroidTestApp/Resources/mipmap-xhdpi/appicon_background.png b/test/AndroidTestApp/Resources/mipmap-xhdpi/appicon_background.png new file mode 100644 index 0000000000..658be3fb6a Binary files /dev/null and b/test/AndroidTestApp/Resources/mipmap-xhdpi/appicon_background.png differ diff --git a/test/AndroidTestApp/Resources/mipmap-xhdpi/appicon_foreground.png b/test/AndroidTestApp/Resources/mipmap-xhdpi/appicon_foreground.png new file mode 100644 index 0000000000..70a542ac03 Binary files /dev/null and b/test/AndroidTestApp/Resources/mipmap-xhdpi/appicon_foreground.png differ diff --git a/test/AndroidTestApp/Resources/mipmap-xxhdpi/appicon.png b/test/AndroidTestApp/Resources/mipmap-xxhdpi/appicon.png new file mode 100644 index 0000000000..988160a190 Binary files /dev/null and b/test/AndroidTestApp/Resources/mipmap-xxhdpi/appicon.png differ diff --git a/test/AndroidTestApp/Resources/mipmap-xxhdpi/appicon_background.png b/test/AndroidTestApp/Resources/mipmap-xxhdpi/appicon_background.png new file mode 100644 index 0000000000..9171c3e404 Binary files /dev/null and b/test/AndroidTestApp/Resources/mipmap-xxhdpi/appicon_background.png differ diff --git a/test/AndroidTestApp/Resources/mipmap-xxhdpi/appicon_foreground.png b/test/AndroidTestApp/Resources/mipmap-xxhdpi/appicon_foreground.png new file mode 100644 index 0000000000..cb63bfb9f8 Binary files /dev/null and b/test/AndroidTestApp/Resources/mipmap-xxhdpi/appicon_foreground.png differ diff --git a/test/AndroidTestApp/Resources/mipmap-xxxhdpi/appicon.png b/test/AndroidTestApp/Resources/mipmap-xxxhdpi/appicon.png new file mode 100644 index 0000000000..b65da5a735 Binary files /dev/null and b/test/AndroidTestApp/Resources/mipmap-xxxhdpi/appicon.png differ diff --git a/test/AndroidTestApp/Resources/mipmap-xxxhdpi/appicon_background.png b/test/AndroidTestApp/Resources/mipmap-xxxhdpi/appicon_background.png new file mode 100644 index 0000000000..1232d8c8d2 Binary files /dev/null and b/test/AndroidTestApp/Resources/mipmap-xxxhdpi/appicon_background.png differ diff --git a/test/AndroidTestApp/Resources/mipmap-xxxhdpi/appicon_foreground.png b/test/AndroidTestApp/Resources/mipmap-xxxhdpi/appicon_foreground.png new file mode 100644 index 0000000000..9f9c9e6d93 Binary files /dev/null and b/test/AndroidTestApp/Resources/mipmap-xxxhdpi/appicon_foreground.png differ diff --git a/test/AndroidTestApp/Resources/values/ic_launcher_background.xml b/test/AndroidTestApp/Resources/values/ic_launcher_background.xml new file mode 100644 index 0000000000..6ec24e6413 --- /dev/null +++ b/test/AndroidTestApp/Resources/values/ic_launcher_background.xml @@ -0,0 +1,4 @@ + + + #2C3E50 + \ No newline at end of file diff --git a/test/AndroidTestApp/Resources/values/strings.xml b/test/AndroidTestApp/Resources/values/strings.xml new file mode 100644 index 0000000000..5daa9b1b4a --- /dev/null +++ b/test/AndroidTestApp/Resources/values/strings.xml @@ -0,0 +1,4 @@ + + AndroidTestApp + Hello, Android! + diff --git a/test/Sentry.Android.Tests/AndroidAssemblyReaderTests.cs b/test/Sentry.Android.Tests/AndroidAssemblyReaderTests.cs new file mode 100644 index 0000000000..26de3fd73f --- /dev/null +++ b/test/Sentry.Android.Tests/AndroidAssemblyReaderTests.cs @@ -0,0 +1,94 @@ +using Sentry.Android.AssemblyReader; + +namespace Sentry.Android.Tests; + +public class AndroidAssemblyReaderTests +{ + private readonly ITestOutputHelper _output; + private readonly IDiagnosticLogger _logger; + + public AndroidAssemblyReaderTests(ITestOutputHelper output) + { + _output = output; + _logger = new TestOutputDiagnosticLogger(output); + } + + private IAndroidAssemblyReader GetSut(bool isAssemblyStore, bool isCompressed) + { +#if ANDROID + // On Android, this gets the current app APK. + var apkPath = Environment.CommandLine; + var supportedAbis = Sentry.Android.AndroidHelpers.GetSupportedAbis(); +#else + var apkPath = Path.Combine( + Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!, + "..", "..", "..", "TestAPKs", + $"android-Store={isAssemblyStore}-Compressed={isCompressed}.apk"); + + var supportedAbis = new List { "x86_64" }; +#endif + _output.WriteLine($"Checking if APK exists: {apkPath}"); + File.Exists(apkPath).Should().BeTrue(); + + return AndroidAssemblyReaderFactory.Open(apkPath, supportedAbis, _logger); + } + + [SkippableTheory] + [InlineData(false)] + [InlineData(true)] + public void CreatesCorrectReader(bool isAssemblyStore) + { +#if ANDROID + Skip.If(true, "It's unknown whether the current Android app APK is an assembly store or not."); +#endif + using var sut = GetSut(isAssemblyStore, isCompressed: true); + if (isAssemblyStore) + { + Assert.IsType(sut); + } + else + { + Assert.IsType(sut); + } + } + + [SkippableTheory] + [InlineData(false)] + [InlineData(true)] + public void ReturnsNullIfAssemblyDoesntExist(bool isAssemblyStore) + { + using var sut = GetSut(isAssemblyStore, isCompressed: true); + Assert.Null(sut.TryReadAssembly("NonExistent.dll")); + } + + [SkippableTheory] + [InlineData(false, true, "Mono.Android.dll")] + [InlineData(false, false, "Mono.Android.dll")] + [InlineData(false, true, "System.Runtime.dll")] + [InlineData(false, false, "System.Runtime.dll")] + [InlineData(true, true, "Mono.Android.dll")] + [InlineData(true, false, "Mono.Android.dll")] + [InlineData(true, true, "System.Runtime.dll")] + [InlineData(true, false, "System.Runtime.dll")] + public void ReadsAssembly(bool isAssemblyStore, bool isCompressed, string assemblyName) + { +#if ANDROID + // No need to run all combinations - we only test the current APK which is (likely) compressed assembly store. + Skip.If(!isAssemblyStore); + Skip.If(!isCompressed); +#endif + using var sut = GetSut(isAssemblyStore, isCompressed); + + var peReader = sut.TryReadAssembly(assemblyName); + Assert.NotNull(peReader); + Assert.True(peReader.HasMetadata); + + var headers = peReader.PEHeaders; + Assert.True(headers.IsDll); + headers.MetadataSize.Should().BeGreaterThan(0); + Assert.NotNull(headers.PEHeader); + headers.PEHeader.SizeOfImage.Should().BeGreaterThan(0); + var debugDirs = peReader.ReadDebugDirectory(); + debugDirs.Length.Should().BeGreaterThan(0); + } +} diff --git a/test/Sentry.Android.Tests/Sentry.Android.Tests.csproj b/test/Sentry.Android.Tests/Sentry.Android.Tests.csproj new file mode 100644 index 0000000000..8217860e05 --- /dev/null +++ b/test/Sentry.Android.Tests/Sentry.Android.Tests.csproj @@ -0,0 +1,30 @@ + + + + net7.0;net6.0;net7.0-android + enable + enable + + + + + + + + + + + + + + + + + + + + + diff --git a/test/Sentry.AspNetCore.Tests/ModuleInit.cs b/test/Sentry.AspNetCore.Tests/ModuleInit.cs index cc68680029..97205a6634 100644 --- a/test/Sentry.AspNetCore.Tests/ModuleInit.cs +++ b/test/Sentry.AspNetCore.Tests/ModuleInit.cs @@ -4,9 +4,6 @@ namespace Sentry.AspNetCore.Tests; public static class ModuleInit { [ModuleInitializer] - public static void Init() - { - VerifyHttp.Enable(); - } + public static void Init() => VerifyHttp.Enable(); } #endif diff --git a/test/Sentry.DiagnosticSource.IntegrationTests/SqlListenerTests.Logging.DotNet6_0.verified.txt b/test/Sentry.DiagnosticSource.IntegrationTests/SqlListenerTests.Logging.DotNet6_0.verified.txt index 35e5c53e66..ec6cab3b35 100644 --- a/test/Sentry.DiagnosticSource.IntegrationTests/SqlListenerTests.Logging.DotNet6_0.verified.txt +++ b/test/Sentry.DiagnosticSource.IntegrationTests/SqlListenerTests.Logging.DotNet6_0.verified.txt @@ -14,6 +14,47 @@ WHERE @@ROWCOUNT = 1 AND [Id] = scope_identity(); }, Logger: Microsoft.EntityFrameworkCore.Database.Command, Platform: csharp, + DebugImages: [ + {, + "type": "pe_dotnet", + "debug_id": "________-____-____-____-____________-________", + "debug_checksum": "______:________________________________________________________________", + "debug_file": ".../Microsoft.Extensions.Logging.pdb", + "code_id": "____________", + "code_file": ".../Microsoft.Extensions.Logging.dll", + }, + {, + "type": "pe_dotnet", + "debug_id": "________-____-____-____-____________-________", + "debug_checksum": "______:________________________________________________________________", + "debug_file": ".../Microsoft.Extensions.Logging.Abstractions.pdb", + "code_id": "_____________", + "code_file": ".../Microsoft.Extensions.Logging.Abstractions.dll", + }, + {, + "type": "pe_dotnet", + "debug_id": "________-____-____-____-____________-________", + "debug_checksum": "______:________________________________________________________________", + "debug_file": ".../Microsoft.EntityFrameworkCore.pdb", + "code_id": "______________", + "code_file": ".../Microsoft.EntityFrameworkCore.dll", + }, + {, + "type": "pe_dotnet", + "debug_id": "________-____-____-____-____________-________", + "debug_checksum": "______:________________________________________________________________", + "debug_file": ".../Microsoft.EntityFrameworkCore.Relational.pdb", + "code_id": "______________", + "code_file": ".../Microsoft.EntityFrameworkCore.Relational.dll", + }, + {, + "type": "pe_dotnet", + "debug_id": "________-____-____-____-____________-________", + "debug_file": ".../System.Private.CoreLib.pdb", + "code_id": "______________", + "code_file": ".../System.Private.CoreLib.dll", + } + ], Level: error, Request: {}, Contexts: {}, diff --git a/test/Sentry.DiagnosticSource.IntegrationTests/SqlListenerTests.Logging.DotNet7_0.verified.txt b/test/Sentry.DiagnosticSource.IntegrationTests/SqlListenerTests.Logging.DotNet7_0.verified.txt index 35e5c53e66..0ce1adf3a1 100644 --- a/test/Sentry.DiagnosticSource.IntegrationTests/SqlListenerTests.Logging.DotNet7_0.verified.txt +++ b/test/Sentry.DiagnosticSource.IntegrationTests/SqlListenerTests.Logging.DotNet7_0.verified.txt @@ -14,6 +14,48 @@ WHERE @@ROWCOUNT = 1 AND [Id] = scope_identity(); }, Logger: Microsoft.EntityFrameworkCore.Database.Command, Platform: csharp, + DebugImages: [ + {, + "type": "pe_dotnet", + "debug_id": "________-____-____-____-____________-________", + "debug_checksum": "______:________________________________________________________________", + "debug_file": ".../Microsoft.Extensions.Logging.pdb", + "code_id": "____________", + "code_file": ".../Microsoft.Extensions.Logging.dll", + }, + {, + "type": "pe_dotnet", + "debug_id": "________-____-____-____-____________-________", + "debug_checksum": "______:________________________________________________________________", + "debug_file": ".../Microsoft.Extensions.Logging.Abstractions.pdb", + "code_id": "_____________", + "code_file": ".../Microsoft.Extensions.Logging.Abstractions.dll", + }, + {, + "type": "pe_dotnet", + "debug_id": "________-____-____-____-____________-________", + "debug_checksum": "______:________________________________________________________________", + "debug_file": ".../Microsoft.EntityFrameworkCore.pdb", + "code_id": "______________", + "code_file": ".../Microsoft.EntityFrameworkCore.dll", + }, + {, + "type": "pe_dotnet", + "debug_id": "________-____-____-____-____________-________", + "debug_checksum": "______:________________________________________________________________", + "debug_file": ".../Microsoft.EntityFrameworkCore.Relational.pdb", + "code_id": "______________", + "code_file": ".../Microsoft.EntityFrameworkCore.Relational.dll", + }, + {, + "type": "pe_dotnet", + "debug_id": "________-____-____-____-____________-________", + "debug_checksum": "______:________________________________________________________________", + "debug_file": ".../System.Private.CoreLib.pdb", + "code_id": "______________", + "code_file": ".../System.Private.CoreLib.dll", + } + ], Level: error, Request: {}, Contexts: {}, diff --git a/test/Sentry.Maui.Device.TestApp/Sentry.Maui.Device.TestApp.csproj b/test/Sentry.Maui.Device.TestApp/Sentry.Maui.Device.TestApp.csproj index 26d0c54e6a..621c73eebf 100644 --- a/test/Sentry.Maui.Device.TestApp/Sentry.Maui.Device.TestApp.csproj +++ b/test/Sentry.Maui.Device.TestApp/Sentry.Maui.Device.TestApp.csproj @@ -47,14 +47,14 @@ - + + diff --git a/test/Sentry.Maui.Device.TestApp/Startup.cs b/test/Sentry.Maui.Device.TestApp/Startup.cs index 5f20f7cfd3..d9ccb6f478 100644 --- a/test/Sentry.Maui.Device.TestApp/Startup.cs +++ b/test/Sentry.Maui.Device.TestApp/Startup.cs @@ -14,7 +14,11 @@ public static MauiApp CreateMauiApp() { typeof(Sentry.Tests.SentrySdkTests).Assembly, typeof(Sentry.Extensions.Logging.Tests.LogLevelExtensionsTests).Assembly, - typeof(Sentry.Maui.Tests.SentryMauiOptionsTests).Assembly + typeof(Sentry.Maui.Tests.SentryMauiOptionsTests).Assembly, + +#if ANDROID + typeof(Sentry.Android.Tests.AndroidAssemblyReaderTests).Assembly, +#endif } }) .UseHeadlessRunner(new HeadlessRunnerOptions diff --git a/test/Sentry.NLog.Tests/IntegrationTests.Simple.Core3_1.verified.txt b/test/Sentry.NLog.Tests/IntegrationTests.Simple.Core3_1.verified.txt new file mode 100644 index 0000000000..f88f63cdf7 --- /dev/null +++ b/test/Sentry.NLog.Tests/IntegrationTests.Simple.Core3_1.verified.txt @@ -0,0 +1,84 @@ +[ + { + Header: { + event_id: Guid_1, + sdk: { + name: sentry.dotnet + } + }, + Items: [ + { + Header: { + type: event + }, + Payload: { + Source: { + Exception: { + $type: Exception, + Type: Exception, + Message: Exception message + }, + Message: { + Message: message = {arg}, + Formatted: message = "arg value" + }, + Logger: Sentry.NLog.Tests.IntegrationTests, + Platform: csharp, + SentryExceptions: [ + { + Type: System.Exception, + Value: Exception message, + Stacktrace: { + Frames: [ + {, + "filename": ".../IntegrationTests.verify.cs", + "function": "Task IntegrationTests.Simple()", + "lineno": 47, + "colno": 17, + "in_app": false, + "package": "Sentry.NLog.Tests, Version=SCRUBBED, Culture=SCRUBBED, PublicKeyToken=SCRUBBED", + "instruction_addr": "____", + "addr_mode": "rel:0", + "function_id": "____", + } + ] + }, + Mechanism: {} + } + ], + DebugImages: [ + {, + "type": "pe_dotnet", + "debug_id": "________-____-____-____-____________-________", + "debug_checksum": "______:________________________________________________________________", + "debug_file": ".../Sentry.NLog.Tests.pdb", + "code_id": "_____________", + "code_file": ".../Sentry.NLog.Tests.dll", + } + ], + Level: error, + Request: {}, + Contexts: {}, + User: { + Id: myId, + Username: , + Email: , + IpAddress: , + Segment: , + Other: { + mood: joyous + } + }, + Environment: production, + Extra: { + arg: arg value + }, + Tags: { + logger: Sentry.NLog.Tests.IntegrationTests + } + } + } + } + ] + } +] \ No newline at end of file diff --git a/test/Sentry.NLog.Tests/IntegrationTests.Simple.DotNet6_0.verified.txt b/test/Sentry.NLog.Tests/IntegrationTests.Simple.DotNet6_0.verified.txt new file mode 100644 index 0000000000..f88f63cdf7 --- /dev/null +++ b/test/Sentry.NLog.Tests/IntegrationTests.Simple.DotNet6_0.verified.txt @@ -0,0 +1,84 @@ +[ + { + Header: { + event_id: Guid_1, + sdk: { + name: sentry.dotnet + } + }, + Items: [ + { + Header: { + type: event + }, + Payload: { + Source: { + Exception: { + $type: Exception, + Type: Exception, + Message: Exception message + }, + Message: { + Message: message = {arg}, + Formatted: message = "arg value" + }, + Logger: Sentry.NLog.Tests.IntegrationTests, + Platform: csharp, + SentryExceptions: [ + { + Type: System.Exception, + Value: Exception message, + Stacktrace: { + Frames: [ + {, + "filename": ".../IntegrationTests.verify.cs", + "function": "Task IntegrationTests.Simple()", + "lineno": 47, + "colno": 17, + "in_app": false, + "package": "Sentry.NLog.Tests, Version=SCRUBBED, Culture=SCRUBBED, PublicKeyToken=SCRUBBED", + "instruction_addr": "____", + "addr_mode": "rel:0", + "function_id": "____", + } + ] + }, + Mechanism: {} + } + ], + DebugImages: [ + {, + "type": "pe_dotnet", + "debug_id": "________-____-____-____-____________-________", + "debug_checksum": "______:________________________________________________________________", + "debug_file": ".../Sentry.NLog.Tests.pdb", + "code_id": "_____________", + "code_file": ".../Sentry.NLog.Tests.dll", + } + ], + Level: error, + Request: {}, + Contexts: {}, + User: { + Id: myId, + Username: , + Email: , + IpAddress: , + Segment: , + Other: { + mood: joyous + } + }, + Environment: production, + Extra: { + arg: arg value + }, + Tags: { + logger: Sentry.NLog.Tests.IntegrationTests + } + } + } + } + ] + } +] \ No newline at end of file diff --git a/test/Sentry.NLog.Tests/IntegrationTests.Simple.DotNet7_0.verified.txt b/test/Sentry.NLog.Tests/IntegrationTests.Simple.DotNet7_0.verified.txt new file mode 100644 index 0000000000..f88f63cdf7 --- /dev/null +++ b/test/Sentry.NLog.Tests/IntegrationTests.Simple.DotNet7_0.verified.txt @@ -0,0 +1,84 @@ +[ + { + Header: { + event_id: Guid_1, + sdk: { + name: sentry.dotnet + } + }, + Items: [ + { + Header: { + type: event + }, + Payload: { + Source: { + Exception: { + $type: Exception, + Type: Exception, + Message: Exception message + }, + Message: { + Message: message = {arg}, + Formatted: message = "arg value" + }, + Logger: Sentry.NLog.Tests.IntegrationTests, + Platform: csharp, + SentryExceptions: [ + { + Type: System.Exception, + Value: Exception message, + Stacktrace: { + Frames: [ + {, + "filename": ".../IntegrationTests.verify.cs", + "function": "Task IntegrationTests.Simple()", + "lineno": 47, + "colno": 17, + "in_app": false, + "package": "Sentry.NLog.Tests, Version=SCRUBBED, Culture=SCRUBBED, PublicKeyToken=SCRUBBED", + "instruction_addr": "____", + "addr_mode": "rel:0", + "function_id": "____", + } + ] + }, + Mechanism: {} + } + ], + DebugImages: [ + {, + "type": "pe_dotnet", + "debug_id": "________-____-____-____-____________-________", + "debug_checksum": "______:________________________________________________________________", + "debug_file": ".../Sentry.NLog.Tests.pdb", + "code_id": "_____________", + "code_file": ".../Sentry.NLog.Tests.dll", + } + ], + Level: error, + Request: {}, + Contexts: {}, + User: { + Id: myId, + Username: , + Email: , + IpAddress: , + Segment: , + Other: { + mood: joyous + } + }, + Environment: production, + Extra: { + arg: arg value + }, + Tags: { + logger: Sentry.NLog.Tests.IntegrationTests + } + } + } + } + ] + } +] \ No newline at end of file diff --git a/test/Sentry.NLog.Tests/IntegrationTests.Simple.verified.txt b/test/Sentry.NLog.Tests/IntegrationTests.Simple.Mono4_0.verified.txt similarity index 62% rename from test/Sentry.NLog.Tests/IntegrationTests.Simple.verified.txt rename to test/Sentry.NLog.Tests/IntegrationTests.Simple.Mono4_0.verified.txt index 1d5ecf5a7a..9f5828ca87 100644 --- a/test/Sentry.NLog.Tests/IntegrationTests.Simple.verified.txt +++ b/test/Sentry.NLog.Tests/IntegrationTests.Simple.Mono4_0.verified.txt @@ -1,4 +1,4 @@ -[ +[ { Header: { event_id: Guid_1, @@ -30,15 +30,29 @@ Value: Exception message, Stacktrace: { Frames: [ - { - Function: Task IntegrationTests.Simple(), - InApp: false + {, + "function": "Task IntegrationTests.Simple()", + "in_app": false, + "package": "Sentry.NLog.Tests, Version=SCRUBBED, Culture=SCRUBBED, PublicKeyToken=SCRUBBED", + "instruction_addr": "____", + "addr_mode": "rel:0", + "function_id": "____", } ] }, Mechanism: {} } ], + DebugImages: [ + {, + "type": "pe_dotnet", + "debug_id": "________-____-____-____-____________-________", + "debug_checksum": "______:________________________________________________________________", + "debug_file": ".../Sentry.NLog.Tests.pdb", + "code_id": "_____________", + "code_file": ".../Sentry.NLog.Tests.dll", + } + ], Level: error, Request: {}, Contexts: {}, diff --git a/test/Sentry.NLog.Tests/IntegrationTests.Simple.Net4_8.verified.txt b/test/Sentry.NLog.Tests/IntegrationTests.Simple.Net4_8.verified.txt new file mode 100644 index 0000000000..f88f63cdf7 --- /dev/null +++ b/test/Sentry.NLog.Tests/IntegrationTests.Simple.Net4_8.verified.txt @@ -0,0 +1,84 @@ +[ + { + Header: { + event_id: Guid_1, + sdk: { + name: sentry.dotnet + } + }, + Items: [ + { + Header: { + type: event + }, + Payload: { + Source: { + Exception: { + $type: Exception, + Type: Exception, + Message: Exception message + }, + Message: { + Message: message = {arg}, + Formatted: message = "arg value" + }, + Logger: Sentry.NLog.Tests.IntegrationTests, + Platform: csharp, + SentryExceptions: [ + { + Type: System.Exception, + Value: Exception message, + Stacktrace: { + Frames: [ + {, + "filename": ".../IntegrationTests.verify.cs", + "function": "Task IntegrationTests.Simple()", + "lineno": 47, + "colno": 17, + "in_app": false, + "package": "Sentry.NLog.Tests, Version=SCRUBBED, Culture=SCRUBBED, PublicKeyToken=SCRUBBED", + "instruction_addr": "____", + "addr_mode": "rel:0", + "function_id": "____", + } + ] + }, + Mechanism: {} + } + ], + DebugImages: [ + {, + "type": "pe_dotnet", + "debug_id": "________-____-____-____-____________-________", + "debug_checksum": "______:________________________________________________________________", + "debug_file": ".../Sentry.NLog.Tests.pdb", + "code_id": "_____________", + "code_file": ".../Sentry.NLog.Tests.dll", + } + ], + Level: error, + Request: {}, + Contexts: {}, + User: { + Id: myId, + Username: , + Email: , + IpAddress: , + Segment: , + Other: { + mood: joyous + } + }, + Environment: production, + Extra: { + arg: arg value + }, + Tags: { + logger: Sentry.NLog.Tests.IntegrationTests + } + } + } + } + ] + } +] \ No newline at end of file diff --git a/test/Sentry.NLog.Tests/IntegrationTests.verify.cs b/test/Sentry.NLog.Tests/IntegrationTests.verify.cs index ee16093db5..1fb57eb86f 100644 --- a/test/Sentry.NLog.Tests/IntegrationTests.verify.cs +++ b/test/Sentry.NLog.Tests/IntegrationTests.verify.cs @@ -55,6 +55,7 @@ public Task Simple() LogManager.Flush(); return Verify(transport.Envelopes) + .UniqueForRuntimeAndVersion() .IgnoreStandardSentryMembers(); } diff --git a/test/Sentry.Serilog.Tests/IntegrationTests.Simple.Core3_1.verified.txt b/test/Sentry.Serilog.Tests/IntegrationTests.Simple.Core3_1.verified.txt new file mode 100644 index 0000000000..1ed4c52299 --- /dev/null +++ b/test/Sentry.Serilog.Tests/IntegrationTests.Simple.Core3_1.verified.txt @@ -0,0 +1,224 @@ +[ + { + Header: { + event_id: Guid_1, + sdk: { + name: sentry.dotnet + } + }, + Items: [ + { + Header: { + type: event + }, + Payload: { + Source: { + Message: { + Message: Debug message stored as breadcrumb., + Formatted: [42] Debug message stored as breadcrumb. + }, + Platform: csharp, + ServerName: TheMachineName, + Level: debug, + Request: {}, + Contexts: {}, + User: { + Username: TheUserName, + IpAddress: {{auto}} + }, + Environment: production, + Extra: { + inventory: { SmallPotion = 3, BigPotion = 0, CheeseWheels = 512 }, + MyTaskId: 42 + } + } + } + } + ] + }, + { + Header: { + event_id: Guid_2, + sdk: { + name: sentry.dotnet + } + }, + Items: [ + { + Header: { + type: event + }, + Payload: { + Source: { + Message: { + Message: Message with a different MyTaskId, + Formatted: [65] Message with a different MyTaskId + }, + Platform: csharp, + ServerName: TheMachineName, + Level: debug, + Request: {}, + Contexts: {}, + User: { + Username: TheUserName, + IpAddress: {{auto}} + }, + Environment: production, + Breadcrumbs: [ + { + Message: [42] Debug message stored as breadcrumb., + Level: debug + } + ], + Extra: { + inventory: { SmallPotion = 3, BigPotion = 0, CheeseWheels = 512 }, + MyTaskId: 65 + } + } + } + } + ] + }, + { + Header: { + event_id: Guid_3, + sdk: { + name: sentry.dotnet + } + }, + Items: [ + { + Header: { + type: event + }, + Payload: { + Source: { + Message: { + Message: Some event that includes the previous breadcrumbs, + Formatted: [42] Some event that includes the previous breadcrumbs + }, + Platform: csharp, + ServerName: TheMachineName, + Level: error, + Request: {}, + Contexts: {}, + User: { + Username: TheUserName, + IpAddress: {{auto}} + }, + Environment: production, + Breadcrumbs: [ + { + Message: [42] Debug message stored as breadcrumb., + Level: debug + }, + { + Message: [65] Message with a different MyTaskId, + Level: debug + } + ], + Extra: { + inventory: { SmallPotion = 3, BigPotion = 0, CheeseWheels = 512 }, + MyTaskId: 42 + } + } + } + } + ] + }, + { + Header: { + event_id: Guid_4, + sdk: { + name: sentry.dotnet + } + }, + Items: [ + { + Header: { + type: event + }, + Payload: { + Source: { + Exception: { + $type: Exception, + Type: Exception, + Message: Exception message, + Data: { + details: Do work always throws. + } + }, + Message: { + Message: Error: with exception, + Formatted: [42] Error: with exception + }, + Platform: csharp, + ServerName: TheMachineName, + SentryExceptions: [ + { + Type: System.Exception, + Value: Exception message, + Stacktrace: { + Frames: [ + {, + "filename": ".../IntegrationTests.verify.cs", + "function": "Task IntegrationTests.Simple()", + "lineno": 46, + "colno": 17, + "in_app": false, + "package": "Sentry.Serilog.Tests, Version=SCRUBBED, Culture=SCRUBBED, PublicKeyToken=SCRUBBED", + "instruction_addr": "____", + "addr_mode": "rel:0", + "function_id": "____", + } + ] + }, + Mechanism: {}, + Data: { + details: Do work always throws. + } + } + ], + DebugImages: [ + {, + "type": "pe_dotnet", + "debug_id": "________-____-____-____-____________-________", + "debug_checksum": "______:________________________________________________________________", + "debug_file": ".../Sentry.Serilog.Tests.pdb", + "code_id": "_____________", + "code_file": ".../Sentry.Serilog.Tests.dll", + } + ], + Level: fatal, + Request: {}, + Contexts: {}, + User: { + Username: TheUserName, + IpAddress: {{auto}} + }, + Environment: production, + Breadcrumbs: [ + { + Message: [42] Debug message stored as breadcrumb., + Level: debug + }, + { + Message: [65] Message with a different MyTaskId, + Level: debug + }, + { + Message: [42] Some event that includes the previous breadcrumbs, + Level: error + } + ], + Extra: { + Exception[0][details]: Do work always throws., + inventory: { SmallPotion = 3, BigPotion = 0, CheeseWheels = 512 }, + MyTaskId: 42 + } + } + } + } + ] + } +] \ No newline at end of file diff --git a/test/Sentry.Serilog.Tests/IntegrationTests.Simple.DotNet6_0.verified.txt b/test/Sentry.Serilog.Tests/IntegrationTests.Simple.DotNet6_0.verified.txt new file mode 100644 index 0000000000..1ed4c52299 --- /dev/null +++ b/test/Sentry.Serilog.Tests/IntegrationTests.Simple.DotNet6_0.verified.txt @@ -0,0 +1,224 @@ +[ + { + Header: { + event_id: Guid_1, + sdk: { + name: sentry.dotnet + } + }, + Items: [ + { + Header: { + type: event + }, + Payload: { + Source: { + Message: { + Message: Debug message stored as breadcrumb., + Formatted: [42] Debug message stored as breadcrumb. + }, + Platform: csharp, + ServerName: TheMachineName, + Level: debug, + Request: {}, + Contexts: {}, + User: { + Username: TheUserName, + IpAddress: {{auto}} + }, + Environment: production, + Extra: { + inventory: { SmallPotion = 3, BigPotion = 0, CheeseWheels = 512 }, + MyTaskId: 42 + } + } + } + } + ] + }, + { + Header: { + event_id: Guid_2, + sdk: { + name: sentry.dotnet + } + }, + Items: [ + { + Header: { + type: event + }, + Payload: { + Source: { + Message: { + Message: Message with a different MyTaskId, + Formatted: [65] Message with a different MyTaskId + }, + Platform: csharp, + ServerName: TheMachineName, + Level: debug, + Request: {}, + Contexts: {}, + User: { + Username: TheUserName, + IpAddress: {{auto}} + }, + Environment: production, + Breadcrumbs: [ + { + Message: [42] Debug message stored as breadcrumb., + Level: debug + } + ], + Extra: { + inventory: { SmallPotion = 3, BigPotion = 0, CheeseWheels = 512 }, + MyTaskId: 65 + } + } + } + } + ] + }, + { + Header: { + event_id: Guid_3, + sdk: { + name: sentry.dotnet + } + }, + Items: [ + { + Header: { + type: event + }, + Payload: { + Source: { + Message: { + Message: Some event that includes the previous breadcrumbs, + Formatted: [42] Some event that includes the previous breadcrumbs + }, + Platform: csharp, + ServerName: TheMachineName, + Level: error, + Request: {}, + Contexts: {}, + User: { + Username: TheUserName, + IpAddress: {{auto}} + }, + Environment: production, + Breadcrumbs: [ + { + Message: [42] Debug message stored as breadcrumb., + Level: debug + }, + { + Message: [65] Message with a different MyTaskId, + Level: debug + } + ], + Extra: { + inventory: { SmallPotion = 3, BigPotion = 0, CheeseWheels = 512 }, + MyTaskId: 42 + } + } + } + } + ] + }, + { + Header: { + event_id: Guid_4, + sdk: { + name: sentry.dotnet + } + }, + Items: [ + { + Header: { + type: event + }, + Payload: { + Source: { + Exception: { + $type: Exception, + Type: Exception, + Message: Exception message, + Data: { + details: Do work always throws. + } + }, + Message: { + Message: Error: with exception, + Formatted: [42] Error: with exception + }, + Platform: csharp, + ServerName: TheMachineName, + SentryExceptions: [ + { + Type: System.Exception, + Value: Exception message, + Stacktrace: { + Frames: [ + {, + "filename": ".../IntegrationTests.verify.cs", + "function": "Task IntegrationTests.Simple()", + "lineno": 46, + "colno": 17, + "in_app": false, + "package": "Sentry.Serilog.Tests, Version=SCRUBBED, Culture=SCRUBBED, PublicKeyToken=SCRUBBED", + "instruction_addr": "____", + "addr_mode": "rel:0", + "function_id": "____", + } + ] + }, + Mechanism: {}, + Data: { + details: Do work always throws. + } + } + ], + DebugImages: [ + {, + "type": "pe_dotnet", + "debug_id": "________-____-____-____-____________-________", + "debug_checksum": "______:________________________________________________________________", + "debug_file": ".../Sentry.Serilog.Tests.pdb", + "code_id": "_____________", + "code_file": ".../Sentry.Serilog.Tests.dll", + } + ], + Level: fatal, + Request: {}, + Contexts: {}, + User: { + Username: TheUserName, + IpAddress: {{auto}} + }, + Environment: production, + Breadcrumbs: [ + { + Message: [42] Debug message stored as breadcrumb., + Level: debug + }, + { + Message: [65] Message with a different MyTaskId, + Level: debug + }, + { + Message: [42] Some event that includes the previous breadcrumbs, + Level: error + } + ], + Extra: { + Exception[0][details]: Do work always throws., + inventory: { SmallPotion = 3, BigPotion = 0, CheeseWheels = 512 }, + MyTaskId: 42 + } + } + } + } + ] + } +] \ No newline at end of file diff --git a/test/Sentry.Serilog.Tests/IntegrationTests.Simple.DotNet7_0.verified.txt b/test/Sentry.Serilog.Tests/IntegrationTests.Simple.DotNet7_0.verified.txt new file mode 100644 index 0000000000..1ed4c52299 --- /dev/null +++ b/test/Sentry.Serilog.Tests/IntegrationTests.Simple.DotNet7_0.verified.txt @@ -0,0 +1,224 @@ +[ + { + Header: { + event_id: Guid_1, + sdk: { + name: sentry.dotnet + } + }, + Items: [ + { + Header: { + type: event + }, + Payload: { + Source: { + Message: { + Message: Debug message stored as breadcrumb., + Formatted: [42] Debug message stored as breadcrumb. + }, + Platform: csharp, + ServerName: TheMachineName, + Level: debug, + Request: {}, + Contexts: {}, + User: { + Username: TheUserName, + IpAddress: {{auto}} + }, + Environment: production, + Extra: { + inventory: { SmallPotion = 3, BigPotion = 0, CheeseWheels = 512 }, + MyTaskId: 42 + } + } + } + } + ] + }, + { + Header: { + event_id: Guid_2, + sdk: { + name: sentry.dotnet + } + }, + Items: [ + { + Header: { + type: event + }, + Payload: { + Source: { + Message: { + Message: Message with a different MyTaskId, + Formatted: [65] Message with a different MyTaskId + }, + Platform: csharp, + ServerName: TheMachineName, + Level: debug, + Request: {}, + Contexts: {}, + User: { + Username: TheUserName, + IpAddress: {{auto}} + }, + Environment: production, + Breadcrumbs: [ + { + Message: [42] Debug message stored as breadcrumb., + Level: debug + } + ], + Extra: { + inventory: { SmallPotion = 3, BigPotion = 0, CheeseWheels = 512 }, + MyTaskId: 65 + } + } + } + } + ] + }, + { + Header: { + event_id: Guid_3, + sdk: { + name: sentry.dotnet + } + }, + Items: [ + { + Header: { + type: event + }, + Payload: { + Source: { + Message: { + Message: Some event that includes the previous breadcrumbs, + Formatted: [42] Some event that includes the previous breadcrumbs + }, + Platform: csharp, + ServerName: TheMachineName, + Level: error, + Request: {}, + Contexts: {}, + User: { + Username: TheUserName, + IpAddress: {{auto}} + }, + Environment: production, + Breadcrumbs: [ + { + Message: [42] Debug message stored as breadcrumb., + Level: debug + }, + { + Message: [65] Message with a different MyTaskId, + Level: debug + } + ], + Extra: { + inventory: { SmallPotion = 3, BigPotion = 0, CheeseWheels = 512 }, + MyTaskId: 42 + } + } + } + } + ] + }, + { + Header: { + event_id: Guid_4, + sdk: { + name: sentry.dotnet + } + }, + Items: [ + { + Header: { + type: event + }, + Payload: { + Source: { + Exception: { + $type: Exception, + Type: Exception, + Message: Exception message, + Data: { + details: Do work always throws. + } + }, + Message: { + Message: Error: with exception, + Formatted: [42] Error: with exception + }, + Platform: csharp, + ServerName: TheMachineName, + SentryExceptions: [ + { + Type: System.Exception, + Value: Exception message, + Stacktrace: { + Frames: [ + {, + "filename": ".../IntegrationTests.verify.cs", + "function": "Task IntegrationTests.Simple()", + "lineno": 46, + "colno": 17, + "in_app": false, + "package": "Sentry.Serilog.Tests, Version=SCRUBBED, Culture=SCRUBBED, PublicKeyToken=SCRUBBED", + "instruction_addr": "____", + "addr_mode": "rel:0", + "function_id": "____", + } + ] + }, + Mechanism: {}, + Data: { + details: Do work always throws. + } + } + ], + DebugImages: [ + {, + "type": "pe_dotnet", + "debug_id": "________-____-____-____-____________-________", + "debug_checksum": "______:________________________________________________________________", + "debug_file": ".../Sentry.Serilog.Tests.pdb", + "code_id": "_____________", + "code_file": ".../Sentry.Serilog.Tests.dll", + } + ], + Level: fatal, + Request: {}, + Contexts: {}, + User: { + Username: TheUserName, + IpAddress: {{auto}} + }, + Environment: production, + Breadcrumbs: [ + { + Message: [42] Debug message stored as breadcrumb., + Level: debug + }, + { + Message: [65] Message with a different MyTaskId, + Level: debug + }, + { + Message: [42] Some event that includes the previous breadcrumbs, + Level: error + } + ], + Extra: { + Exception[0][details]: Do work always throws., + inventory: { SmallPotion = 3, BigPotion = 0, CheeseWheels = 512 }, + MyTaskId: 42 + } + } + } + } + ] + } +] \ No newline at end of file diff --git a/test/Sentry.Serilog.Tests/IntegrationTests.Simple.verified.txt b/test/Sentry.Serilog.Tests/IntegrationTests.Simple.Mono4_0.verified.txt similarity index 85% rename from test/Sentry.Serilog.Tests/IntegrationTests.Simple.verified.txt rename to test/Sentry.Serilog.Tests/IntegrationTests.Simple.Mono4_0.verified.txt index f7c0fc4cf6..f6d68a98b1 100644 --- a/test/Sentry.Serilog.Tests/IntegrationTests.Simple.verified.txt +++ b/test/Sentry.Serilog.Tests/IntegrationTests.Simple.Mono4_0.verified.txt @@ -1,4 +1,4 @@ -[ +[ { Header: { event_id: Guid_1, @@ -160,9 +160,13 @@ Value: Exception message, Stacktrace: { Frames: [ - { - Function: Task IntegrationTests.Simple(), - InApp: true + {, + "function": "Task IntegrationTests.Simple()", + "in_app": false, + "package": "Sentry.Serilog.Tests, Version=SCRUBBED, Culture=SCRUBBED, PublicKeyToken=SCRUBBED", + "instruction_addr": "____", + "addr_mode": "rel:0", + "function_id": "____", } ] }, @@ -172,6 +176,16 @@ } } ], + DebugImages: [ + {, + "type": "pe_dotnet", + "debug_id": "________-____-____-____-____________-________", + "debug_checksum": "______:________________________________________________________________", + "debug_file": ".../Sentry.Serilog.Tests.pdb", + "code_id": "_____________", + "code_file": ".../Sentry.Serilog.Tests.dll", + } + ], Level: fatal, Request: {}, Contexts: {}, diff --git a/test/Sentry.Serilog.Tests/IntegrationTests.Simple.Net4_8.verified.txt b/test/Sentry.Serilog.Tests/IntegrationTests.Simple.Net4_8.verified.txt new file mode 100644 index 0000000000..1ed4c52299 --- /dev/null +++ b/test/Sentry.Serilog.Tests/IntegrationTests.Simple.Net4_8.verified.txt @@ -0,0 +1,224 @@ +[ + { + Header: { + event_id: Guid_1, + sdk: { + name: sentry.dotnet + } + }, + Items: [ + { + Header: { + type: event + }, + Payload: { + Source: { + Message: { + Message: Debug message stored as breadcrumb., + Formatted: [42] Debug message stored as breadcrumb. + }, + Platform: csharp, + ServerName: TheMachineName, + Level: debug, + Request: {}, + Contexts: {}, + User: { + Username: TheUserName, + IpAddress: {{auto}} + }, + Environment: production, + Extra: { + inventory: { SmallPotion = 3, BigPotion = 0, CheeseWheels = 512 }, + MyTaskId: 42 + } + } + } + } + ] + }, + { + Header: { + event_id: Guid_2, + sdk: { + name: sentry.dotnet + } + }, + Items: [ + { + Header: { + type: event + }, + Payload: { + Source: { + Message: { + Message: Message with a different MyTaskId, + Formatted: [65] Message with a different MyTaskId + }, + Platform: csharp, + ServerName: TheMachineName, + Level: debug, + Request: {}, + Contexts: {}, + User: { + Username: TheUserName, + IpAddress: {{auto}} + }, + Environment: production, + Breadcrumbs: [ + { + Message: [42] Debug message stored as breadcrumb., + Level: debug + } + ], + Extra: { + inventory: { SmallPotion = 3, BigPotion = 0, CheeseWheels = 512 }, + MyTaskId: 65 + } + } + } + } + ] + }, + { + Header: { + event_id: Guid_3, + sdk: { + name: sentry.dotnet + } + }, + Items: [ + { + Header: { + type: event + }, + Payload: { + Source: { + Message: { + Message: Some event that includes the previous breadcrumbs, + Formatted: [42] Some event that includes the previous breadcrumbs + }, + Platform: csharp, + ServerName: TheMachineName, + Level: error, + Request: {}, + Contexts: {}, + User: { + Username: TheUserName, + IpAddress: {{auto}} + }, + Environment: production, + Breadcrumbs: [ + { + Message: [42] Debug message stored as breadcrumb., + Level: debug + }, + { + Message: [65] Message with a different MyTaskId, + Level: debug + } + ], + Extra: { + inventory: { SmallPotion = 3, BigPotion = 0, CheeseWheels = 512 }, + MyTaskId: 42 + } + } + } + } + ] + }, + { + Header: { + event_id: Guid_4, + sdk: { + name: sentry.dotnet + } + }, + Items: [ + { + Header: { + type: event + }, + Payload: { + Source: { + Exception: { + $type: Exception, + Type: Exception, + Message: Exception message, + Data: { + details: Do work always throws. + } + }, + Message: { + Message: Error: with exception, + Formatted: [42] Error: with exception + }, + Platform: csharp, + ServerName: TheMachineName, + SentryExceptions: [ + { + Type: System.Exception, + Value: Exception message, + Stacktrace: { + Frames: [ + {, + "filename": ".../IntegrationTests.verify.cs", + "function": "Task IntegrationTests.Simple()", + "lineno": 46, + "colno": 17, + "in_app": false, + "package": "Sentry.Serilog.Tests, Version=SCRUBBED, Culture=SCRUBBED, PublicKeyToken=SCRUBBED", + "instruction_addr": "____", + "addr_mode": "rel:0", + "function_id": "____", + } + ] + }, + Mechanism: {}, + Data: { + details: Do work always throws. + } + } + ], + DebugImages: [ + {, + "type": "pe_dotnet", + "debug_id": "________-____-____-____-____________-________", + "debug_checksum": "______:________________________________________________________________", + "debug_file": ".../Sentry.Serilog.Tests.pdb", + "code_id": "_____________", + "code_file": ".../Sentry.Serilog.Tests.dll", + } + ], + Level: fatal, + Request: {}, + Contexts: {}, + User: { + Username: TheUserName, + IpAddress: {{auto}} + }, + Environment: production, + Breadcrumbs: [ + { + Message: [42] Debug message stored as breadcrumb., + Level: debug + }, + { + Message: [65] Message with a different MyTaskId, + Level: debug + }, + { + Message: [42] Some event that includes the previous breadcrumbs, + Level: error + } + ], + Extra: { + Exception[0][details]: Do work always throws., + inventory: { SmallPotion = 3, BigPotion = 0, CheeseWheels = 512 }, + MyTaskId: 42 + } + } + } + } + ] + } +] \ No newline at end of file diff --git a/test/Sentry.Serilog.Tests/IntegrationTests.verify.cs b/test/Sentry.Serilog.Tests/IntegrationTests.verify.cs index d86e3606a4..7f9c8a45c6 100644 --- a/test/Sentry.Serilog.Tests/IntegrationTests.verify.cs +++ b/test/Sentry.Serilog.Tests/IntegrationTests.verify.cs @@ -1,4 +1,7 @@ -[UsesVerify] +namespace Sentry.Serilog.Tests; + +[UsesVerify] +[Collection(nameof(SentrySdkCollection))] public class IntegrationTests { [Fact] @@ -52,6 +55,7 @@ public Task Simple() Log.CloseAndFlush(); return Verify(transport.Envelopes) + .UniqueForRuntimeAndVersion() .IgnoreStandardSentryMembers(); } diff --git a/test/Sentry.Testing/VerifyExtensions.cs b/test/Sentry.Testing/VerifyExtensions.cs index 2abb8fc55f..a87a313db4 100644 --- a/test/Sentry.Testing/VerifyExtensions.cs +++ b/test/Sentry.Testing/VerifyExtensions.cs @@ -1,4 +1,7 @@ #if !__MOBILE__ + +namespace Sentry.Testing; + public static class VerifyExtensions { public static SettingsTask IgnoreStandardSentryMembers(this SettingsTask settings) @@ -10,6 +13,8 @@ public static SettingsTask IgnoreStandardSentryMembers(this SettingsTask setting { _.Converters.Add(new SpansConverter()); _.Converters.Add(new ContextsConverter()); + _.Converters.Add(new DebugImageConverter()); + _.Converters.Add(new StackFrameConverter()); }) .IgnoreMembers("version", "elapsed") .IgnoreMembersWithType() @@ -40,7 +45,7 @@ public static SettingsTask IgnoreStandardSentryMembers(this SettingsTask setting .IgnoreStackTrace(); } - class SpansConverter : WriteOnlyJsonConverter> + private class SpansConverter : WriteOnlyJsonConverter> { public override void Write(VerifyJsonWriter writer, IReadOnlyCollection spans) { @@ -59,7 +64,7 @@ public override void Write(VerifyJsonWriter writer, IReadOnlyCollection sp } } - class ContextsConverter : WriteOnlyJsonConverter + private class ContextsConverter : WriteOnlyJsonConverter { public override void Write(VerifyJsonWriter writer, Contexts contexts) { @@ -79,5 +84,82 @@ public override void Write(VerifyJsonWriter writer, Contexts contexts) writer.Serialize(items); } } + + private class DebugImageConverter : WriteOnlyJsonConverter + { + public override void Write(VerifyJsonWriter writer, DebugImage obj) + { + obj.DebugId = ScrubAlphaNum(obj.DebugId); + obj.DebugChecksum = ScrubAlphaNum(obj.DebugChecksum); + obj.DebugFile = ScrubPath(obj.DebugFile); + obj.CodeFile = ScrubPath(obj.CodeFile); + obj.CodeId = ScrubAlphaNum(obj.CodeId); + writer.WriteJson(obj); + } + } + + private class StackFrameConverter : WriteOnlyJsonConverter + { + public override void Write(VerifyJsonWriter writer, SentryStackFrame obj) + { + obj.FileName = ScrubPath(obj.FileName); + obj.FunctionId = ScrubAlphaNum(obj.FunctionId); + obj.InstructionAddress = ScrubAlphaNum(obj.InstructionAddress); + obj.Package = obj.Package.Replace(new Regex("=[^,]+"), "=SCRUBBED"); + writer.WriteJson(obj); + } + } + + // Extension so we can use `nullableString?.Replace()`. + private static string Replace(this string str, Regex regex, string replacement) => regex.Replace(str, replacement); + + private static string ScrubAlphaNum(string str) => str?.Replace(new Regex("[a-zA-Z0-9]"), "_"); + + private static string ScrubPath(string str) => str?.Replace(new Regex(@"^.*[/\\]"), ".../"); + + private static void WriteJson(this VerifyJsonWriter verifyWriter, IJsonSerializable @object) + { + using var stream = new MemoryStream(); + using var jsonWriter = new Utf8JsonWriter(stream, new() { Indented = true }); + @object.WriteTo(jsonWriter, null); + jsonWriter.Flush(); + var str = Encoding.UTF8.GetString(stream.ToArray()); + + // Note: this is not perfect because we don't respect indentation. of the surrounding objects. + // Unfortunately, there doesn't seem to be the way to get current serialization depth. + // There's `Indentation` and `IndentChar` but those are only relevant in combination with the current depth + // should be available as `Top`, which is protected internal... + // verifyWriter.WriteValue(str); + + // Therefore, we have the following best-effort approach of splitting lines and writing individually + // which makes the JsonWriter add proper indentation. + var lines = str.Replace("\r", "").Split('\n'); + var depth = 0; + for (var i = 0; i < lines.Length; i++) + { + var line = lines[i].Trim(); + + // Note: we can't use WriteStartObject/EndObject because following WriteRawValueWithScrubbers() fails with: + // Argon.JsonWriterException : Token Undefined in state ObjectStart would result in an invalid JSON object. Path '[0].Items[0].Payload.Source.objs[0]'. + switch (line) + { + case "{": + case "[": + verifyWriter.WriteRawValue(line); + depth++; + break; + case "}": + case "]": + verifyWriter.WriteRawValue(line); + depth--; + break; + default: + var indent = new string(verifyWriter.IndentChar, verifyWriter.Indentation * depth); + // FYI: this always adds comma on the previous line before adding the given value. Can't help it. + verifyWriter.WriteRawValueWithScrubbers(indent + line.TrimEnd(',')); + break; + } + } + } } #endif diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.Core3_1.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.Core3_1.verified.txt index ca82948b74..765428a6d4 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.Core3_1.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.Core3_1.verified.txt @@ -81,6 +81,7 @@ namespace Sentry public DebugImage() { } public string? CodeFile { get; set; } public string? CodeId { get; set; } + public string? DebugChecksum { get; set; } public string? DebugFile { get; set; } public string? DebugId { get; set; } public string? ImageAddress { get; set; } @@ -676,6 +677,7 @@ namespace Sentry public string? FileName { get; set; } public System.Collections.Generic.IList FramesOmitted { get; } public string? Function { get; set; } + public string? FunctionId { get; set; } public long ImageAddress { get; set; } public bool? InApp { get; set; } public string? InstructionAddress { get; set; } @@ -692,7 +694,7 @@ namespace Sentry public void WriteTo(System.Text.Json.Utf8JsonWriter writer, Sentry.Extensibility.IDiagnosticLogger? logger) { } public static Sentry.SentryStackFrame FromJson(System.Text.Json.JsonElement json) { } } - public sealed class SentryStackTrace : Sentry.IJsonSerializable + public class SentryStackTrace : Sentry.IJsonSerializable { public SentryStackTrace() { } public System.Collections.Generic.IList Frames { get; set; } @@ -1202,14 +1204,10 @@ namespace Sentry.Extensibility public void Process(System.Exception? exception, Sentry.SentryEvent sentryEvent) { } protected abstract void ProcessException(TException exception, Sentry.SentryEvent sentryEvent); } - public class SentryStackTraceFactory : Sentry.Extensibility.ISentryStackTraceFactory + public sealed class SentryStackTraceFactory : Sentry.Extensibility.ISentryStackTraceFactory { public SentryStackTraceFactory(Sentry.SentryOptions options) { } - public virtual Sentry.SentryStackTrace? Create(System.Exception? exception = null) { } - protected virtual Sentry.SentryStackFrame CreateFrame(System.Diagnostics.StackFrame stackFrame, bool isCurrentStackTrace) { } - protected virtual System.Diagnostics.StackTrace CreateStackTrace(System.Exception? exception) { } - protected virtual System.Reflection.MethodBase? GetMethod(System.Diagnostics.StackFrame stackFrame) { } - protected Sentry.SentryStackFrame InternalCreateFrame(System.Diagnostics.StackFrame stackFrame, bool demangle) { } + public Sentry.SentryStackTrace? Create(System.Exception? exception = null) { } } } namespace Sentry.Http diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet4_8.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet4_8.verified.txt index 5341535b8a..45c5a92db6 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet4_8.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet4_8.verified.txt @@ -81,6 +81,7 @@ namespace Sentry public DebugImage() { } public string? CodeFile { get; set; } public string? CodeId { get; set; } + public string? DebugChecksum { get; set; } public string? DebugFile { get; set; } public string? DebugId { get; set; } public string? ImageAddress { get; set; } @@ -675,6 +676,7 @@ namespace Sentry public string? FileName { get; set; } public System.Collections.Generic.IList FramesOmitted { get; } public string? Function { get; set; } + public string? FunctionId { get; set; } public long ImageAddress { get; set; } public bool? InApp { get; set; } public string? InstructionAddress { get; set; } @@ -691,7 +693,7 @@ namespace Sentry public void WriteTo(System.Text.Json.Utf8JsonWriter writer, Sentry.Extensibility.IDiagnosticLogger? logger) { } public static Sentry.SentryStackFrame FromJson(System.Text.Json.JsonElement json) { } } - public sealed class SentryStackTrace : Sentry.IJsonSerializable + public class SentryStackTrace : Sentry.IJsonSerializable { public SentryStackTrace() { } public System.Collections.Generic.IList Frames { get; set; } @@ -1201,14 +1203,10 @@ namespace Sentry.Extensibility public void Process(System.Exception? exception, Sentry.SentryEvent sentryEvent) { } protected abstract void ProcessException(TException exception, Sentry.SentryEvent sentryEvent); } - public class SentryStackTraceFactory : Sentry.Extensibility.ISentryStackTraceFactory + public sealed class SentryStackTraceFactory : Sentry.Extensibility.ISentryStackTraceFactory { public SentryStackTraceFactory(Sentry.SentryOptions options) { } - public virtual Sentry.SentryStackTrace? Create(System.Exception? exception = null) { } - protected virtual Sentry.SentryStackFrame CreateFrame(System.Diagnostics.StackFrame stackFrame, bool isCurrentStackTrace) { } - protected virtual System.Diagnostics.StackTrace CreateStackTrace(System.Exception? exception) { } - protected virtual System.Reflection.MethodBase? GetMethod(System.Diagnostics.StackFrame stackFrame) { } - protected Sentry.SentryStackFrame InternalCreateFrame(System.Diagnostics.StackFrame stackFrame, bool demangle) { } + public Sentry.SentryStackTrace? Create(System.Exception? exception = null) { } } } namespace Sentry.Http diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet6_0.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet6_0.verified.txt index ca82948b74..765428a6d4 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet6_0.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet6_0.verified.txt @@ -81,6 +81,7 @@ namespace Sentry public DebugImage() { } public string? CodeFile { get; set; } public string? CodeId { get; set; } + public string? DebugChecksum { get; set; } public string? DebugFile { get; set; } public string? DebugId { get; set; } public string? ImageAddress { get; set; } @@ -676,6 +677,7 @@ namespace Sentry public string? FileName { get; set; } public System.Collections.Generic.IList FramesOmitted { get; } public string? Function { get; set; } + public string? FunctionId { get; set; } public long ImageAddress { get; set; } public bool? InApp { get; set; } public string? InstructionAddress { get; set; } @@ -692,7 +694,7 @@ namespace Sentry public void WriteTo(System.Text.Json.Utf8JsonWriter writer, Sentry.Extensibility.IDiagnosticLogger? logger) { } public static Sentry.SentryStackFrame FromJson(System.Text.Json.JsonElement json) { } } - public sealed class SentryStackTrace : Sentry.IJsonSerializable + public class SentryStackTrace : Sentry.IJsonSerializable { public SentryStackTrace() { } public System.Collections.Generic.IList Frames { get; set; } @@ -1202,14 +1204,10 @@ namespace Sentry.Extensibility public void Process(System.Exception? exception, Sentry.SentryEvent sentryEvent) { } protected abstract void ProcessException(TException exception, Sentry.SentryEvent sentryEvent); } - public class SentryStackTraceFactory : Sentry.Extensibility.ISentryStackTraceFactory + public sealed class SentryStackTraceFactory : Sentry.Extensibility.ISentryStackTraceFactory { public SentryStackTraceFactory(Sentry.SentryOptions options) { } - public virtual Sentry.SentryStackTrace? Create(System.Exception? exception = null) { } - protected virtual Sentry.SentryStackFrame CreateFrame(System.Diagnostics.StackFrame stackFrame, bool isCurrentStackTrace) { } - protected virtual System.Diagnostics.StackTrace CreateStackTrace(System.Exception? exception) { } - protected virtual System.Reflection.MethodBase? GetMethod(System.Diagnostics.StackFrame stackFrame) { } - protected Sentry.SentryStackFrame InternalCreateFrame(System.Diagnostics.StackFrame stackFrame, bool demangle) { } + public Sentry.SentryStackTrace? Create(System.Exception? exception = null) { } } } namespace Sentry.Http diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet7_0.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet7_0.verified.txt index ca82948b74..765428a6d4 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet7_0.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet7_0.verified.txt @@ -81,6 +81,7 @@ namespace Sentry public DebugImage() { } public string? CodeFile { get; set; } public string? CodeId { get; set; } + public string? DebugChecksum { get; set; } public string? DebugFile { get; set; } public string? DebugId { get; set; } public string? ImageAddress { get; set; } @@ -676,6 +677,7 @@ namespace Sentry public string? FileName { get; set; } public System.Collections.Generic.IList FramesOmitted { get; } public string? Function { get; set; } + public string? FunctionId { get; set; } public long ImageAddress { get; set; } public bool? InApp { get; set; } public string? InstructionAddress { get; set; } @@ -692,7 +694,7 @@ namespace Sentry public void WriteTo(System.Text.Json.Utf8JsonWriter writer, Sentry.Extensibility.IDiagnosticLogger? logger) { } public static Sentry.SentryStackFrame FromJson(System.Text.Json.JsonElement json) { } } - public sealed class SentryStackTrace : Sentry.IJsonSerializable + public class SentryStackTrace : Sentry.IJsonSerializable { public SentryStackTrace() { } public System.Collections.Generic.IList Frames { get; set; } @@ -1202,14 +1204,10 @@ namespace Sentry.Extensibility public void Process(System.Exception? exception, Sentry.SentryEvent sentryEvent) { } protected abstract void ProcessException(TException exception, Sentry.SentryEvent sentryEvent); } - public class SentryStackTraceFactory : Sentry.Extensibility.ISentryStackTraceFactory + public sealed class SentryStackTraceFactory : Sentry.Extensibility.ISentryStackTraceFactory { public SentryStackTraceFactory(Sentry.SentryOptions options) { } - public virtual Sentry.SentryStackTrace? Create(System.Exception? exception = null) { } - protected virtual Sentry.SentryStackFrame CreateFrame(System.Diagnostics.StackFrame stackFrame, bool isCurrentStackTrace) { } - protected virtual System.Diagnostics.StackTrace CreateStackTrace(System.Exception? exception) { } - protected virtual System.Reflection.MethodBase? GetMethod(System.Diagnostics.StackFrame stackFrame) { } - protected Sentry.SentryStackFrame InternalCreateFrame(System.Diagnostics.StackFrame stackFrame, bool demangle) { } + public Sentry.SentryStackTrace? Create(System.Exception? exception = null) { } } } namespace Sentry.Http diff --git a/test/Sentry.Tests/HubTests.CaptureEvent_ActiveTransaction_UnhandledExceptionTransactionEndedAsCrashed.Core3_1.verified.txt b/test/Sentry.Tests/HubTests.CaptureEvent_ActiveTransaction_UnhandledExceptionTransactionEndedAsCrashed.Core3_1.verified.txt new file mode 100644 index 0000000000..2694e50acf --- /dev/null +++ b/test/Sentry.Tests/HubTests.CaptureEvent_ActiveTransaction_UnhandledExceptionTransactionEndedAsCrashed.Core3_1.verified.txt @@ -0,0 +1,162 @@ +[ + { + Header: { + sdk: { + name: sentry.dotnet + } + }, + Items: [ + { + Header: { + type: session + }, + Payload: { + Source: { + DistinctId: Guid_1, + Release: release, + Environment: production, + IsInitial: true + } + } + } + ] + }, + { + Header: { + event_id: Guid_2, + sdk: { + name: sentry.dotnet + } + }, + Items: [ + { + Header: { + type: event + }, + Payload: { + Source: { + Platform: csharp, + SentryExceptions: [ + { + Mechanism: { + Handled: false + } + } + ], + SentryThreads: [ + { + Crashed: false, + Current: true + } + ], + DebugImages: [ + {, + "type": "pe_dotnet", + "debug_id": "________-____-____-____-____________-________", + "debug_file": "System.Private.CoreLib.pdb", + "code_id": "______________", + "code_file": ".../System.Private.CoreLib.dll", + }, + {, + "type": "pe_dotnet", + "debug_id": "________-____-____-____-____________-________", + "debug_checksum": "______:________________________________________________________________", + "debug_file": ".../Sentry.Tests.pdb", + "code_id": "_____________", + "code_file": ".../Sentry.Tests.dll", + }, + {, + "type": "pe_dotnet", + "debug_id": "________-____-____-____-____________-________", + "debug_checksum": "______:________________________________________________________________", + "debug_file": "xunit.execution.dotnet.pdb", + "code_id": "_____________", + "code_file": ".../xunit.execution.dotnet.dll", + }, + {, + "type": "pe_dotnet", + "debug_id": "________-____-____-____-____________-________", + "debug_checksum": "______:________________________________________________________________", + "debug_file": "xunit.core.pdb", + "code_id": "_____________", + "code_file": ".../xunit.core.dll", + } + ], + Level: error, + TransactionName: my transaction, + Request: {}, + Contexts: { + trace: { + Operation: + } + }, + User: {}, + Environment: production + } + } + }, + { + Header: { + type: session + }, + Payload: { + Source: { + DistinctId: Guid_1, + Release: release, + Environment: production, + ErrorCount: 1, + IsInitial: false, + SequenceNumber: 1, + EndStatus: Crashed + } + } + } + ] + }, + { + Header: { + event_id: Guid_3, + sdk: { + name: sentry.dotnet + }, + trace: { + environment: production, + public_key: d4d82fc1c2c4032a83f3a29aa3a3aff, + release: release, + sample_rate: 1, + trace_id: Guid_4, + transaction: my transaction + } + }, + Items: [ + { + Header: { + type: transaction + }, + Payload: { + Source: { + Name: my transaction, + Platform: csharp, + Operation: my operation, + Description: , + Status: Aborted, + IsSampled: true, + SampleRate: 1.0, + Request: {}, + Contexts: { + trace: { + Operation: my operation, + Description: , + Status: Aborted, + IsSampled: true + } + }, + User: {}, + Environment: production, + IsFinished: true + } + } + } + ] + } +] \ No newline at end of file diff --git a/test/Sentry.Tests/HubTests.CaptureEvent_ActiveTransaction_UnhandledExceptionTransactionEndedAsCrashed.DotNet6_0.verified.txt b/test/Sentry.Tests/HubTests.CaptureEvent_ActiveTransaction_UnhandledExceptionTransactionEndedAsCrashed.DotNet6_0.verified.txt new file mode 100644 index 0000000000..bff8475af9 --- /dev/null +++ b/test/Sentry.Tests/HubTests.CaptureEvent_ActiveTransaction_UnhandledExceptionTransactionEndedAsCrashed.DotNet6_0.verified.txt @@ -0,0 +1,162 @@ +[ + { + Header: { + sdk: { + name: sentry.dotnet + } + }, + Items: [ + { + Header: { + type: session + }, + Payload: { + Source: { + DistinctId: Guid_1, + Release: release, + Environment: production, + IsInitial: true + } + } + } + ] + }, + { + Header: { + event_id: Guid_2, + sdk: { + name: sentry.dotnet + } + }, + Items: [ + { + Header: { + type: event + }, + Payload: { + Source: { + Platform: csharp, + SentryExceptions: [ + { + Mechanism: { + Handled: false + } + } + ], + SentryThreads: [ + { + Crashed: false, + Current: true + } + ], + DebugImages: [ + {, + "type": "pe_dotnet", + "debug_id": "________-____-____-____-____________-________", + "debug_file": ".../System.Private.CoreLib.pdb", + "code_id": "______________", + "code_file": ".../System.Private.CoreLib.dll", + }, + {, + "type": "pe_dotnet", + "debug_id": "________-____-____-____-____________-________", + "debug_checksum": "______:________________________________________________________________", + "debug_file": ".../Sentry.Tests.pdb", + "code_id": "_____________", + "code_file": ".../Sentry.Tests.dll", + }, + {, + "type": "pe_dotnet", + "debug_id": "________-____-____-____-____________-________", + "debug_checksum": "______:________________________________________________________________", + "debug_file": "xunit.execution.dotnet.pdb", + "code_id": "_____________", + "code_file": ".../xunit.execution.dotnet.dll", + }, + {, + "type": "pe_dotnet", + "debug_id": "________-____-____-____-____________-________", + "debug_checksum": "______:________________________________________________________________", + "debug_file": "xunit.core.pdb", + "code_id": "_____________", + "code_file": ".../xunit.core.dll", + } + ], + Level: error, + TransactionName: my transaction, + Request: {}, + Contexts: { + trace: { + Operation: + } + }, + User: {}, + Environment: production + } + } + }, + { + Header: { + type: session + }, + Payload: { + Source: { + DistinctId: Guid_1, + Release: release, + Environment: production, + ErrorCount: 1, + IsInitial: false, + SequenceNumber: 1, + EndStatus: Crashed + } + } + } + ] + }, + { + Header: { + event_id: Guid_3, + sdk: { + name: sentry.dotnet + }, + trace: { + environment: production, + public_key: d4d82fc1c2c4032a83f3a29aa3a3aff, + release: release, + sample_rate: 1, + trace_id: Guid_4, + transaction: my transaction + } + }, + Items: [ + { + Header: { + type: transaction + }, + Payload: { + Source: { + Name: my transaction, + Platform: csharp, + Operation: my operation, + Description: , + Status: Aborted, + IsSampled: true, + SampleRate: 1.0, + Request: {}, + Contexts: { + trace: { + Operation: my operation, + Description: , + Status: Aborted, + IsSampled: true + } + }, + User: {}, + Environment: production, + IsFinished: true + } + } + } + ] + } +] \ No newline at end of file diff --git a/test/Sentry.Tests/HubTests.CaptureEvent_ActiveTransaction_UnhandledExceptionTransactionEndedAsCrashed.DotNet7_0.verified.txt b/test/Sentry.Tests/HubTests.CaptureEvent_ActiveTransaction_UnhandledExceptionTransactionEndedAsCrashed.DotNet7_0.verified.txt new file mode 100644 index 0000000000..0bdaf10893 --- /dev/null +++ b/test/Sentry.Tests/HubTests.CaptureEvent_ActiveTransaction_UnhandledExceptionTransactionEndedAsCrashed.DotNet7_0.verified.txt @@ -0,0 +1,163 @@ +[ + { + Header: { + sdk: { + name: sentry.dotnet + } + }, + Items: [ + { + Header: { + type: session + }, + Payload: { + Source: { + DistinctId: Guid_1, + Release: release, + Environment: production, + IsInitial: true + } + } + } + ] + }, + { + Header: { + event_id: Guid_2, + sdk: { + name: sentry.dotnet + } + }, + Items: [ + { + Header: { + type: event + }, + Payload: { + Source: { + Platform: csharp, + SentryExceptions: [ + { + Mechanism: { + Handled: false + } + } + ], + SentryThreads: [ + { + Crashed: false, + Current: true + } + ], + DebugImages: [ + {, + "type": "pe_dotnet", + "debug_id": "________-____-____-____-____________-________", + "debug_checksum": "______:________________________________________________________________", + "debug_file": ".../System.Private.CoreLib.pdb", + "code_id": "______________", + "code_file": ".../System.Private.CoreLib.dll", + }, + {, + "type": "pe_dotnet", + "debug_id": "________-____-____-____-____________-________", + "debug_checksum": "______:________________________________________________________________", + "debug_file": ".../Sentry.Tests.pdb", + "code_id": "_____________", + "code_file": ".../Sentry.Tests.dll", + }, + {, + "type": "pe_dotnet", + "debug_id": "________-____-____-____-____________-________", + "debug_checksum": "______:________________________________________________________________", + "debug_file": "xunit.execution.dotnet.pdb", + "code_id": "_____________", + "code_file": ".../xunit.execution.dotnet.dll", + }, + {, + "type": "pe_dotnet", + "debug_id": "________-____-____-____-____________-________", + "debug_checksum": "______:________________________________________________________________", + "debug_file": "xunit.core.pdb", + "code_id": "_____________", + "code_file": ".../xunit.core.dll", + } + ], + Level: error, + TransactionName: my transaction, + Request: {}, + Contexts: { + trace: { + Operation: + } + }, + User: {}, + Environment: production + } + } + }, + { + Header: { + type: session + }, + Payload: { + Source: { + DistinctId: Guid_1, + Release: release, + Environment: production, + ErrorCount: 1, + IsInitial: false, + SequenceNumber: 1, + EndStatus: Crashed + } + } + } + ] + }, + { + Header: { + event_id: Guid_3, + sdk: { + name: sentry.dotnet + }, + trace: { + environment: production, + public_key: d4d82fc1c2c4032a83f3a29aa3a3aff, + release: release, + sample_rate: 1, + trace_id: Guid_4, + transaction: my transaction + } + }, + Items: [ + { + Header: { + type: transaction + }, + Payload: { + Source: { + Name: my transaction, + Platform: csharp, + Operation: my operation, + Description: , + Status: Aborted, + IsSampled: true, + SampleRate: 1.0, + Request: {}, + Contexts: { + trace: { + Operation: my operation, + Description: , + Status: Aborted, + IsSampled: true + } + }, + User: {}, + Environment: production, + IsFinished: true + } + } + } + ] + } +] \ No newline at end of file diff --git a/test/Sentry.Tests/HubTests.CaptureEvent_ActiveTransaction_UnhandledExceptionTransactionEndedAsCrashed.Mono4_0.verified.txt b/test/Sentry.Tests/HubTests.CaptureEvent_ActiveTransaction_UnhandledExceptionTransactionEndedAsCrashed.Mono4_0.verified.txt new file mode 100644 index 0000000000..e35fbf62d2 --- /dev/null +++ b/test/Sentry.Tests/HubTests.CaptureEvent_ActiveTransaction_UnhandledExceptionTransactionEndedAsCrashed.Mono4_0.verified.txt @@ -0,0 +1,162 @@ +[ + { + Header: { + sdk: { + name: sentry.dotnet + } + }, + Items: [ + { + Header: { + type: session + }, + Payload: { + Source: { + DistinctId: Guid_1, + Release: release, + Environment: production, + IsInitial: true + } + } + } + ] + }, + { + Header: { + event_id: Guid_2, + sdk: { + name: sentry.dotnet + } + }, + Items: [ + { + Header: { + type: event + }, + Payload: { + Source: { + Platform: csharp, + SentryExceptions: [ + { + Mechanism: { + Handled: false + } + } + ], + SentryThreads: [ + { + Crashed: false, + Current: true + } + ], + DebugImages: [ + {, + "type": "pe_dotnet", + "debug_id": "________-____-____-____-____________-________", + "debug_file": ".../mscorlib.pdb", + "code_id": "______________", + "code_file": ".../mscorlib.dll", + }, + {, + "type": "pe_dotnet", + "debug_id": "________-____-____-____-____________-________", + "debug_checksum": "______:________________________________________________________________", + "debug_file": ".../Sentry.Tests.pdb", + "code_id": "_____________", + "code_file": ".../Sentry.Tests.dll", + }, + {, + "type": "pe_dotnet", + "debug_id": "________-____-____-____-____________-________", + "debug_checksum": "______:________________________________________________________________", + "debug_file": "xunit.execution.desktop.pdb", + "code_id": "_____________", + "code_file": ".../xunit.execution.desktop.dll", + }, + {, + "type": "pe_dotnet", + "debug_id": "________-____-____-____-____________-________", + "debug_checksum": "______:________________________________________________________________", + "debug_file": "xunit.core.pdb", + "code_id": "_____________", + "code_file": ".../xunit.core.dll", + } + ], + Level: error, + TransactionName: my transaction, + Request: {}, + Contexts: { + trace: { + Operation: + } + }, + User: {}, + Environment: production + } + } + }, + { + Header: { + type: session + }, + Payload: { + Source: { + DistinctId: Guid_1, + Release: release, + Environment: production, + ErrorCount: 1, + IsInitial: false, + SequenceNumber: 1, + EndStatus: Crashed + } + } + } + ] + }, + { + Header: { + event_id: Guid_3, + sdk: { + name: sentry.dotnet + }, + trace: { + environment: production, + public_key: d4d82fc1c2c4032a83f3a29aa3a3aff, + release: release, + sample_rate: 1, + trace_id: Guid_4, + transaction: my transaction + } + }, + Items: [ + { + Header: { + type: transaction + }, + Payload: { + Source: { + Name: my transaction, + Platform: csharp, + Operation: my operation, + Description: , + Status: Aborted, + IsSampled: true, + SampleRate: 1.0, + Request: {}, + Contexts: { + trace: { + Operation: my operation, + Description: , + Status: Aborted, + IsSampled: true + } + }, + User: {}, + Environment: production, + IsFinished: true + } + } + } + ] + } +] \ No newline at end of file diff --git a/test/Sentry.Tests/HubTests.CaptureEvent_ActiveTransaction_UnhandledExceptionTransactionEndedAsCrashed.verified.txt b/test/Sentry.Tests/HubTests.CaptureEvent_ActiveTransaction_UnhandledExceptionTransactionEndedAsCrashed.Net4_8.verified.txt similarity index 66% rename from test/Sentry.Tests/HubTests.CaptureEvent_ActiveTransaction_UnhandledExceptionTransactionEndedAsCrashed.verified.txt rename to test/Sentry.Tests/HubTests.CaptureEvent_ActiveTransaction_UnhandledExceptionTransactionEndedAsCrashed.Net4_8.verified.txt index 3d32bf47cb..ab2a8ee5de 100644 --- a/test/Sentry.Tests/HubTests.CaptureEvent_ActiveTransaction_UnhandledExceptionTransactionEndedAsCrashed.verified.txt +++ b/test/Sentry.Tests/HubTests.CaptureEvent_ActiveTransaction_UnhandledExceptionTransactionEndedAsCrashed.Net4_8.verified.txt @@ -49,6 +49,32 @@ Current: true } ], + DebugImages: [ + {, + "type": "pe_dotnet", + "debug_id": "________-____-____-____-____________-________", + "debug_checksum": "______:________________________________________________________________", + "debug_file": ".../Sentry.Tests.pdb", + "code_id": "_____________", + "code_file": ".../Sentry.Tests.dll", + }, + {, + "type": "pe_dotnet", + "debug_id": "________-____-____-____-____________-________", + "debug_checksum": "______:________________________________________________________________", + "debug_file": "xunit.execution.desktop.pdb", + "code_id": "_____________", + "code_file": ".../xunit.execution.desktop.dll", + }, + {, + "type": "pe_dotnet", + "debug_id": "________-____-____-____-____________-________", + "debug_checksum": "______:________________________________________________________________", + "debug_file": "xunit.core.pdb", + "code_id": "_____________", + "code_file": ".../xunit.core.dll", + } + ], Level: error, TransactionName: my transaction, Request: {}, diff --git a/test/Sentry.Tests/HubTests.cs b/test/Sentry.Tests/HubTests.cs index c372b7d410..4170323a55 100644 --- a/test/Sentry.Tests/HubTests.cs +++ b/test/Sentry.Tests/HubTests.cs @@ -300,7 +300,7 @@ async Task VerifyAsync(HttpRequestMessage message) var expectedContextKey = Guid.NewGuid().ToString(); var evt = new SentryEvent { - Contexts = {[expectedContextKey] = new EvilContext()}, + Contexts = { [expectedContextKey] = new EvilContext() }, Message = new() { Formatted = expectedMessage diff --git a/test/Sentry.Tests/HubTests.verify.cs b/test/Sentry.Tests/HubTests.verify.cs index 28d7868cc0..2aaa4df7c9 100644 --- a/test/Sentry.Tests/HubTests.verify.cs +++ b/test/Sentry.Tests/HubTests.verify.cs @@ -1,5 +1,3 @@ -using Sentry.Testing; - namespace Sentry.Tests; [UsesVerify] @@ -40,8 +38,16 @@ public async Task CaptureEvent_ActiveTransaction_UnhandledExceptionTransactionEn }); await Verify(worker.Envelopes) + .UniqueForRuntimeAndVersion() .IgnoreStandardSentryMembers() .IgnoreMember("Stacktrace") - .IgnoreMember(_ => _.Name); + .IgnoreMember(_ => _.Name) + .IgnoreInstance(_ => + _.DebugFile != null && ( + _.DebugFile.Contains("Xunit.SkippableFact") || + _.DebugFile.Contains("xunit.runner") || + _.DebugFile.Contains("Microsoft.TestPlatform") + ) + ); } } diff --git a/test/Sentry.Tests/Internals/DebugStackTraceTests.cs b/test/Sentry.Tests/Internals/DebugStackTraceTests.cs new file mode 100644 index 0000000000..f61bda8a36 --- /dev/null +++ b/test/Sentry.Tests/Internals/DebugStackTraceTests.cs @@ -0,0 +1,174 @@ +// ReSharper disable once CheckNamespace +// Stack trace filters out Sentry frames by namespace +namespace Other.Tests.Internals; + +public class DebugStackTraceTests +{ + private class Fixture + { + public SentryOptions SentryOptions { get; set; } = new(); + public InjectableDebugStackTrace GetSut() => new(SentryOptions); + } + + private readonly Fixture _fixture = new(); + private static readonly string ThisNamespace = typeof(SentryStackTraceFactoryTests).Namespace; + + [Fact] + public void CreateSentryStackFrame_AppNamespace_InAppFrame() + { + var frame = new StackFrame(); + var sut = _fixture.GetSut(); + + var actual = sut.CreateFrame(frame); + + Assert.True(actual.InApp); + } + + [Fact] + public void CreateSentryStackFrame_AppNamespaceExcluded_NotInAppFrame() + { + _fixture.SentryOptions.AddInAppExclude(ThisNamespace); + var sut = _fixture.GetSut(); + var frame = new StackFrame(); + + var actual = sut.CreateFrame(frame); + + Assert.False(actual.InApp); + } + + [Fact] + public void CreateSentryStackFrame_NamespaceIncludedAndExcluded_IncludesTakesPrecedence() + { + _fixture.SentryOptions.AddInAppExclude(ThisNamespace); + _fixture.SentryOptions.AddInAppInclude(ThisNamespace); + var sut = _fixture.GetSut(); + var frame = new StackFrame(); + + var actual = sut.CreateFrame(frame); + + Assert.True(actual.InApp); + } + + // https://github.com/getsentry/sentry-dotnet/issues/64 + [Fact] + public void DemangleAnonymousFunction_NullFunction_ContinuesNull() + { + var stackFrame = new SentryStackFrame + { + Function = null + }; + + DebugStackTrace.DemangleAnonymousFunction(stackFrame); + Assert.Null(stackFrame.Function); + } + + [Fact] + public void DemangleAsyncFunctionName_NullModule_ContinuesNull() + { + var stackFrame = new SentryStackFrame + { + Module = null + }; + + DebugStackTrace.DemangleAnonymousFunction(stackFrame); + Assert.Null(stackFrame.Module); + } + + [Fact] + public void MergeDebugImages_Empty() + { + var sut = _fixture.GetSut(); + var e = new SentryEvent(); + sut.MergeDebugImagesInto(e); + Assert.Null(e.DebugImages); + } + + [Fact] + public void MergeDebugImages_ThrowsOnSecondRun() + { + var sut = _fixture.GetSut(); + sut.Inject(1); + + var e = new SentryEvent(); + sut.MergeDebugImagesInto(e); + Assert.NotNull(e.DebugImages); + Assert.Throws(() => sut.MergeDebugImagesInto(e)); + } + + [Fact] + public void MergeDebugImages() + { + var sut1 = _fixture.GetSut(); + sut1.Inject(1111); + sut1.Inject(2222); + sut1.Frames.Add(new SentryStackFrame() { Function = "1", AddressMode = "rel:0" }); + sut1.Frames.Add(new SentryStackFrame() { Function = "2", AddressMode = "rel:1" }); + sut1.Frames.Add(new SentryStackFrame() { Function = "3", AddressMode = "rel:0" }); + + var e = new SentryEvent(); + sut1.MergeDebugImagesInto(e); + Assert.NotNull(e.DebugImages); + Assert.Equal(2, e.DebugImages.Count); + Assert.Equal("1111.dll", e.DebugImages[0].CodeFile); + Assert.Equal("2222.dll", e.DebugImages[1].CodeFile); + + // Stack trace isn't changed - there were no DebugImage relocations yet. + CheckStackTraceIsUnchanged(sut1); + + var sut2 = _fixture.GetSut(); + sut2.Inject(3333); + sut2.Inject(1111); + sut2.Inject(4444); + sut2.Frames.Add(new SentryStackFrame() { Function = "1", AddressMode = "rel:0" }); + sut2.Frames.Add(new SentryStackFrame() { Function = "2", AddressMode = "rel:1" }); + sut2.Frames.Add(new SentryStackFrame() { Function = "3", AddressMode = "rel:0" }); + sut2.Frames.Add(new SentryStackFrame() { Function = "4", AddressMode = "rel:2" }); + + // Only the two new debug images are added. + sut2.MergeDebugImagesInto(e); + Assert.Equal(4, e.DebugImages.Count); + Assert.Equal("1111.dll", e.DebugImages[0].CodeFile); + Assert.Equal("2222.dll", e.DebugImages[1].CodeFile); + Assert.Equal("3333.dll", e.DebugImages[2].CodeFile); + Assert.Equal("4444.dll", e.DebugImages[3].CodeFile); + + // First stack trace must remain unchanged. + CheckStackTraceIsUnchanged(sut1); + + // Stack trace is updated to reflect new positions + Assert.Equal(4, sut2.Frames.Count); + Assert.Equal("1", sut2.Frames[0].Function); + Assert.Equal("rel:2", sut2.Frames[0].AddressMode); + Assert.Equal("2", sut2.Frames[1].Function); + Assert.Equal("rel:0", sut2.Frames[1].AddressMode); + Assert.Equal("3", sut2.Frames[2].Function); + Assert.Equal("rel:2", sut2.Frames[2].AddressMode); + Assert.Equal("4", sut2.Frames[3].Function); + Assert.Equal("rel:3", sut2.Frames[3].AddressMode); + + void CheckStackTraceIsUnchanged(SentryStackTrace stackTrace) + { + Assert.Equal(3, stackTrace.Frames.Count); + Assert.Equal("1", stackTrace.Frames[0].Function); + Assert.Equal("rel:0", stackTrace.Frames[0].AddressMode); + Assert.Equal("2", stackTrace.Frames[1].Function); + Assert.Equal("rel:1", stackTrace.Frames[1].AddressMode); + Assert.Equal("3", stackTrace.Frames[2].Function); + Assert.Equal("rel:0", stackTrace.Frames[2].AddressMode); + } + } + + private class InjectableDebugStackTrace : DebugStackTrace + { + public InjectableDebugStackTrace(SentryOptions options) : base(options) { } + + public void Inject(int identifier) + { + DebugImages.Add(new DebugImage + { + CodeFile = $"{identifier}.dll", + ModuleVersionId = new Guid($"00000000-0000-0000-0000-{identifier:D12}") + }); + } + } +} diff --git a/test/Sentry.Tests/Internals/MonoSentryStackTraceFactoryTests.cs b/test/Sentry.Tests/Internals/MonoSentryStackTraceFactoryTests.cs deleted file mode 100644 index 35a0074eab..0000000000 --- a/test/Sentry.Tests/Internals/MonoSentryStackTraceFactoryTests.cs +++ /dev/null @@ -1,139 +0,0 @@ - - -// ReSharper disable once CheckNamespace -// Stack trace filters out Sentry frames by namespace -namespace Other.Tests.Internals; - -public class MonoSentryStackTraceFactoryTests -{ - private class Fixture - { - public SentryOptions SentryOptions { get; set; } = new(); - public MonoSentryStackTraceFactory GetSut() => new(SentryOptions); - } - - private readonly Fixture _fixture = new(); - - [Fact] - public void Create_UnityIl2Cpp_LeavesOutZeroedOutPath() - { - var sut = _fixture.GetSut(); - var ex = new TestStackTraceException(@" at UnityEngine.ProcessTouchPress (UnityEngine.EventSystems.PointerEventData pointerEvent, System.Boolean pressed, System.Boolean released) [0x00000] in <00000000000000000000000000000000>:0 - at UnityEngine.EventSystems.StandaloneInputModule.Process () [0x00000] in <00000000000000000000000000000000>:0 "); - - var actual = sut.Create(ex); - Assert.Equal("Process ()", actual!.Frames[0].Function); - Assert.Equal("UnityEngine.EventSystems.StandaloneInputModule", actual!.Frames[0].Module); - Assert.Equal("ProcessTouchPress (UnityEngine.EventSystems.PointerEventData pointerEvent, System.Boolean pressed, System.Boolean released)", actual!.Frames[1].Function); - Assert.Equal("UnityEngine", actual!.Frames[1].Module); - - for (var i = 0; i < 2; i++) - { - Assert.Null(actual!.Frames[i].ColumnNumber); - Assert.Null(actual!.Frames[i].LineNumber); - Assert.Null(actual!.Frames[i].InstructionOffset); - Assert.Null(actual!.Frames[i].Package); - Assert.Null(actual!.Frames[i].Platform); - Assert.Null(actual!.Frames[i].InternalVars); - Assert.Null(actual!.Frames[i].AbsolutePath); - Assert.Null(actual!.Frames[i].ContextLine); - Assert.Null(actual!.Frames[i].FileName); - Assert.Equal(0, actual!.Frames[i].ImageAddress); - } - } - - [Fact] - public void Create_NoExceptionAndAttachStackTraceOptionFalse_NullResult() - { - _fixture.SentryOptions.AttachStacktrace = false; - var sut = _fixture.GetSut(); - - Assert.Null(sut.Create()); - } - - [Fact] - public void Create_WithExceptionAndDefaultAttachStackTraceOption_HasStackTrace() - { - var sut = _fixture.GetSut(); - - Exception exception; - try - { - Throw(); - static void Throw() => throw null!; - } - catch (Exception e) { exception = e; } - - Assert.NotNull(sut.Create(exception)); - } - - [Fact] - public void Create_WithExceptionAndAttachStackTraceOptionOn_HasStackTrace() - { - _fixture.SentryOptions.AttachStacktrace = true; - var sut = _fixture.GetSut(); - - Exception exception; - try - { - Throw(); - static void Throw() => throw null!; - } - catch (Exception e) { exception = e; } - - var stackTrace = sut.Create(exception); - - Assert.Equal(new StackTrace(exception, true).FrameCount, stackTrace!.Frames.Count); - } - - [Fact] - public void CreateSentryStackFrame_AppNamespace_InAppFrame() - { - var frame = new StackFrame(); - var sut = _fixture.GetSut(); - - var actual = sut.CreateFrame(frame); - - Assert.True(actual.InApp); - } - - [Fact] - public void CreateSentryStackFrame_AppNamespaceExcluded_NotInAppFrame() - { - _fixture.SentryOptions.AddInAppExclude(GetType().Namespace!); - var sut = _fixture.GetSut(); - var frame = new StackFrame(); - - var actual = sut.CreateFrame(frame); - - Assert.False(actual.InApp); - } - - [Fact] - public void CreateSentryStackFrame_NamespaceIncludedAndExcluded_IncludesTakesPrecedence() - { - _fixture.SentryOptions.AddInAppExclude(GetType().Namespace!); - _fixture.SentryOptions.AddInAppInclude(GetType().Namespace!); - var sut = _fixture.GetSut(); - var frame = new StackFrame(); - - var actual = sut.CreateFrame(frame); - - Assert.True(actual.InApp); - } - - private class TestStackTraceException : Exception - { - public TestStackTraceException(string stacktrace) => StackTrace = stacktrace; - - public TestStackTraceException() : base() - { - } - - public TestStackTraceException(string message, Exception innerException) : base(message, innerException) - { - } - - public override string StackTrace { get; } - } -} diff --git a/test/Sentry.Tests/Internals/SentryStackTraceFactoryTests.MethodGeneric_mode=Enhanced.verified.txt b/test/Sentry.Tests/Internals/SentryStackTraceFactoryTests.MethodGeneric_mode=Enhanced.verified.txt index f684faad9f..8fb43a4f4a 100644 --- a/test/Sentry.Tests/Internals/SentryStackTraceFactoryTests.MethodGeneric_mode=Enhanced.verified.txt +++ b/test/Sentry.Tests/Internals/SentryStackTraceFactoryTests.MethodGeneric_mode=Enhanced.verified.txt @@ -1,5 +1,6 @@ -{ +{ FileName: Internals/SentryStackTraceFactoryTests.cs, Function: void SentryStackTraceFactoryTests.GenericMethodThatThrows(T value), - InApp: true + InApp: true, + AddressMode: rel:0 } \ No newline at end of file diff --git a/test/Sentry.Tests/Internals/SentryStackTraceFactoryTests.MethodGeneric_mode=Original.verified.txt b/test/Sentry.Tests/Internals/SentryStackTraceFactoryTests.MethodGeneric_mode=Original.verified.txt index da8ff44bf1..a64466b158 100644 --- a/test/Sentry.Tests/Internals/SentryStackTraceFactoryTests.MethodGeneric_mode=Original.verified.txt +++ b/test/Sentry.Tests/Internals/SentryStackTraceFactoryTests.MethodGeneric_mode=Original.verified.txt @@ -1,6 +1,7 @@ -{ +{ FileName: Internals/SentryStackTraceFactoryTests.cs, Function: GenericMethodThatThrows, Module: Other.Tests.Internals.SentryStackTraceFactoryTests, - InApp: true + InApp: true, + AddressMode: rel:0 } \ No newline at end of file diff --git a/test/Sentry.Tests/Internals/SentryStackTraceFactoryTests.cs b/test/Sentry.Tests/Internals/SentryStackTraceFactoryTests.cs index b703f66673..e4f607c8f1 100644 --- a/test/Sentry.Tests/Internals/SentryStackTraceFactoryTests.cs +++ b/test/Sentry.Tests/Internals/SentryStackTraceFactoryTests.cs @@ -8,12 +8,11 @@ public partial class SentryStackTraceFactoryTests { private class Fixture { - public SentryOptions SentryOptions { get; set; } = new(); + public SentryOptions SentryOptions { get; } = new(); public SentryStackTraceFactory GetSut() => new(SentryOptions); } private readonly Fixture _fixture = new(); - private static readonly string ThisNamespace = typeof(SentryStackTraceFactoryTests).Namespace; [Fact] public void Create_NoExceptionAndAttachStackTraceOptionFalse_NullResult() @@ -41,7 +40,7 @@ public void Create_NoExceptionAndAttachStackTraceOptionOnWithOriginalMode_Curren Assert.DoesNotContain(stackTrace.Frames, p => p.Function?.StartsWith( - nameof(SentryStackTraceFactory.CreateFrame) + '(', + nameof(DebugStackTrace.CreateFrame) + '(', StringComparison.Ordinal ) == true); } @@ -105,7 +104,7 @@ public void Create_NoExceptionAndAttachStackTraceOptionOnWithEnhancedMode_Curren Assert.DoesNotContain(stackTrace.Frames, p => p.Function?.StartsWith( - nameof(SentryStackTraceFactory.CreateFrame) + '(', + nameof(DebugStackTrace.CreateFrame) + '(', StringComparison.Ordinal ) == true); } @@ -196,67 +195,6 @@ public void Create_InlineCase_IncludesAmpersandAfterParameterType(StackTraceMode frame.Function.Should().Be(method); } - [Fact] - public void CreateSentryStackFrame_AppNamespace_InAppFrame() - { - var frame = new StackFrame(); - var sut = _fixture.GetSut(); - - var actual = sut.CreateFrame(frame); - - Assert.True(actual.InApp); - } - - [Fact] - public void CreateSentryStackFrame_AppNamespaceExcluded_NotInAppFrame() - { - _fixture.SentryOptions.AddInAppExclude(ThisNamespace); - var sut = _fixture.GetSut(); - var frame = new StackFrame(); - - var actual = sut.CreateFrame(frame); - - Assert.False(actual.InApp); - } - - [Fact] - public void CreateSentryStackFrame_NamespaceIncludedAndExcluded_IncludesTakesPrecedence() - { - _fixture.SentryOptions.AddInAppExclude(ThisNamespace); - _fixture.SentryOptions.AddInAppInclude(ThisNamespace); - var sut = _fixture.GetSut(); - var frame = new StackFrame(); - - var actual = sut.CreateFrame(frame); - - Assert.True(actual.InApp); - } - - // https://github.com/getsentry/sentry-dotnet/issues/64 - [Fact] - public void DemangleAnonymousFunction_NullFunction_ContinuesNull() - { - var stackFrame = new SentryStackFrame - { - Function = null - }; - - SentryStackTraceFactory.DemangleAnonymousFunction(stackFrame); - Assert.Null(stackFrame.Function); - } - - [Fact] - public void DemangleAsyncFunctionName_NullModule_ContinuesNull() - { - var stackFrame = new SentryStackFrame - { - Module = null - }; - - SentryStackTraceFactory.DemangleAnonymousFunction(stackFrame); - Assert.Null(stackFrame.Module); - } - // ReSharper disable UnusedParameter.Local [MethodImpl(MethodImplOptions.NoInlining)] private static (Fixture f, int b) ByRefMethodThatThrows(int value, in int valueIn, ref int valueRef, out int valueOut) => diff --git a/test/Sentry.Tests/Internals/SentryStackTraceFactoryTests.verify.cs b/test/Sentry.Tests/Internals/SentryStackTraceFactoryTests.verify.cs index 8f72024e60..e701641303 100644 --- a/test/Sentry.Tests/Internals/SentryStackTraceFactoryTests.verify.cs +++ b/test/Sentry.Tests/Internals/SentryStackTraceFactoryTests.verify.cs @@ -34,7 +34,9 @@ public Task MethodGeneric(StackTraceMode mode) x => x.Package, x => x.LineNumber, x => x.ColumnNumber, - x => x.InstructionOffset).AddScrubber(x => x.Replace(@"\", @"/")) + x => x.InstructionAddress, + x => x.FunctionId) + .AddScrubber(x => x.Replace(@"\", @"/")) .UseParameters(mode); } } diff --git a/test/Sentry.Tests/SentryOptionsTests.cs b/test/Sentry.Tests/SentryOptionsTests.cs index 52c75befdc..72f12fd7e3 100644 --- a/test/Sentry.Tests/SentryOptionsTests.cs +++ b/test/Sentry.Tests/SentryOptionsTests.cs @@ -34,24 +34,4 @@ public void AttachStackTrace_ByDefault_True() var sut = new SentryOptions(); Assert.True(sut.AttachStacktrace); } - -#if NETFRAMEWORK - [SkippableFact(typeof(IsTypeException))] - public void StackTraceFactory_RunningOnMono_HasMonoStackTraceFactory() - { - Skip.If(!RuntimeInfo.GetRuntime().IsMono()); - - var sut = new SentryOptions(); - Assert.IsType(sut.SentryStackTraceFactory); - } - - [SkippableFact(typeof(IsNotTypeException))] - public void StackTraceFactory_NotRunningOnMono_NotMonoStackTraceFactory() - { - Skip.If(RuntimeInfo.GetRuntime().IsMono()); - - var sut = new SentryOptions(); - Assert.IsNotType(sut.SentryStackTraceFactory); - } -#endif }