From 81a6ab1af09b473f8d435533ce49e6c5fcc7ab11 Mon Sep 17 00:00:00 2001 From: Sean Date: Mon, 6 May 2024 08:44:56 +1200 Subject: [PATCH 01/16] Support capturing ILOffset and MetadataToken for the method This captures the required debugging information for reversing the stacktrace line numbers and file from the stack trace when a PDB is not included in the build output --- .../Builders/RaygunErrorMessageBuilder.cs | 23 +++++++++---------- .../RaygunErrorStackTraceLineMessage.cs | 6 ++++- .../Builders/RaygunErrorMessageBuilder.cs | 21 ++++++++--------- .../RaygunErrorStackTraceLineMessage.cs | 4 ++++ 4 files changed, 30 insertions(+), 24 deletions(-) diff --git a/Mindscape.Raygun4Net.Core/Builders/RaygunErrorMessageBuilder.cs b/Mindscape.Raygun4Net.Core/Builders/RaygunErrorMessageBuilder.cs index ccb8db058..38a1a9b0d 100644 --- a/Mindscape.Raygun4Net.Core/Builders/RaygunErrorMessageBuilder.cs +++ b/Mindscape.Raygun4Net.Core/Builders/RaygunErrorMessageBuilder.cs @@ -105,31 +105,30 @@ public static RaygunErrorStackTraceLineMessage[] BuildStackTrace(StackTrace stac return lines.ToArray(); } - foreach (StackFrame frame in frames) + foreach (var frame in frames) { - MethodBase method = frame.GetMethod(); + var method = frame.GetMethod(); if (method != null) { - int lineNumber = frame.GetFileLineNumber(); - - if (lineNumber == 0) - { - lineNumber = frame.GetILOffset(); - } - + var lineNumber = frame.GetFileLineNumber(); + var ilOffset = frame.GetILOffset(); + var methodToken = method.MetadataToken; + var methodName = GenerateMethodName(method); - string file = frame.GetFileName(); + var file = frame.GetFileName(); - string className = method.ReflectedType != null ? method.ReflectedType.FullName : "(unknown)"; + var className = method.ReflectedType != null ? method.ReflectedType.FullName : "(unknown)"; var line = new RaygunErrorStackTraceLineMessage { FileName = file, LineNumber = lineNumber, MethodName = methodName, - ClassName = className + ClassName = className, + ILOffset = ilOffset, + MethodToken = methodToken }; lines.Add(line); diff --git a/Mindscape.Raygun4Net.Core/Messages/RaygunErrorStackTraceLineMessage.cs b/Mindscape.Raygun4Net.Core/Messages/RaygunErrorStackTraceLineMessage.cs index 933f6a217..cb25156fe 100644 --- a/Mindscape.Raygun4Net.Core/Messages/RaygunErrorStackTraceLineMessage.cs +++ b/Mindscape.Raygun4Net.Core/Messages/RaygunErrorStackTraceLineMessage.cs @@ -12,11 +12,15 @@ public class RaygunErrorStackTraceLineMessage public string Raw { get; set; } + public int ILOffset { get; set; } + + public int MethodToken { get; set; } + public override string ToString() { // This exists because Reflection in Xamarin can't seem to obtain the Getter methods unless the getter is used somewhere in the code. // The getter of all properties is required to serialize the Raygun messages to JSON. - return string.Format("[RaygunErrorStackTraceLineMessage: LineNumber={0}, ClassName={1}, FileName={2}, MethodName={3}, Raw={4}]", LineNumber, ClassName, FileName, MethodName, Raw); + return string.Format("[RaygunErrorStackTraceLineMessage: LineNumber={0}, ClassName={1}, FileName={2}, MethodName={3}, Raw={4}, ILOffset={5}, MethodToken={6}]", LineNumber, ClassName, FileName, MethodName, Raw, ILOffset, MethodToken); } } } \ No newline at end of file diff --git a/Mindscape.Raygun4Net.NetCore.Common/Builders/RaygunErrorMessageBuilder.cs b/Mindscape.Raygun4Net.NetCore.Common/Builders/RaygunErrorMessageBuilder.cs index 3f9c07786..19a6fc63a 100644 --- a/Mindscape.Raygun4Net.NetCore.Common/Builders/RaygunErrorMessageBuilder.cs +++ b/Mindscape.Raygun4Net.NetCore.Common/Builders/RaygunErrorMessageBuilder.cs @@ -51,31 +51,30 @@ public static RaygunErrorStackTraceLineMessage[] BuildStackTrace(StackTrace stac return lines.ToArray(); } - foreach (StackFrame frame in frames) + foreach (var frame in frames) { - MethodBase method = frame.GetMethod(); + var method = frame.GetMethod(); if (method != null) { - int lineNumber = frame.GetFileLineNumber(); - - if (lineNumber == 0) - { - lineNumber = frame.GetILOffset(); - } + var lineNumber = frame.GetFileLineNumber(); + var ilOffset = frame.GetILOffset(); + var methodToken = method.MetadataToken; var methodName = GenerateMethodName(method); - string file = frame.GetFileName(); + var file = frame.GetFileName(); - string className = method.DeclaringType != null ? method.DeclaringType.FullName : "(unknown)"; + var className = method.DeclaringType != null ? method.DeclaringType.FullName : "(unknown)"; var line = new RaygunErrorStackTraceLineMessage { FileName = file, LineNumber = lineNumber, MethodName = methodName, - ClassName = className + ClassName = className, + ILOffset = ilOffset, + MethodToken = methodToken }; lines.Add(line); diff --git a/Mindscape.Raygun4Net.NetCore.Common/Messages/RaygunErrorStackTraceLineMessage.cs b/Mindscape.Raygun4Net.NetCore.Common/Messages/RaygunErrorStackTraceLineMessage.cs index 3d99d22ce..cfba00a5a 100644 --- a/Mindscape.Raygun4Net.NetCore.Common/Messages/RaygunErrorStackTraceLineMessage.cs +++ b/Mindscape.Raygun4Net.NetCore.Common/Messages/RaygunErrorStackTraceLineMessage.cs @@ -9,5 +9,9 @@ public class RaygunErrorStackTraceLineMessage public string FileName { get; set; } public string MethodName { get; set; } + + public int ILOffset { get; set; } + + public int MethodToken { get; set; } } } \ No newline at end of file From be1382065023477f3098f7652746e6631e2b3c8b Mon Sep 17 00:00:00 2001 From: Sean Date: Mon, 6 May 2024 08:56:04 +1200 Subject: [PATCH 02/16] Wrap the retrieval of reflected properties in a try catch This could be an issue in medium trust environments, so safer just to try/catch it, especially in this critical part of code --- .../Builders/RaygunErrorMessageBuilder.cs | 28 ++++++++++++----- .../Builders/RaygunErrorMessageBuilder.cs | 31 +++++++++++++------ 2 files changed, 42 insertions(+), 17 deletions(-) diff --git a/Mindscape.Raygun4Net.Core/Builders/RaygunErrorMessageBuilder.cs b/Mindscape.Raygun4Net.Core/Builders/RaygunErrorMessageBuilder.cs index 38a1a9b0d..d825763ec 100644 --- a/Mindscape.Raygun4Net.Core/Builders/RaygunErrorMessageBuilder.cs +++ b/Mindscape.Raygun4Net.Core/Builders/RaygunErrorMessageBuilder.cs @@ -111,15 +111,27 @@ public static RaygunErrorStackTraceLineMessage[] BuildStackTrace(StackTrace stac if (method != null) { - var lineNumber = frame.GetFileLineNumber(); - var ilOffset = frame.GetILOffset(); - var methodToken = method.MetadataToken; - - var methodName = GenerateMethodName(method); + string methodName = null; + string file = null; + string className = null; + var lineNumber = 0; + var ilOffset = -1; + var methodToken = -1; - var file = frame.GetFileName(); + try + { + file = frame.GetFileName(); + lineNumber = frame.GetFileLineNumber(); + methodName = GenerateMethodName(method); + className = method.ReflectedType != null ? method.ReflectedType.FullName : "(unknown)"; - var className = method.ReflectedType != null ? method.ReflectedType.FullName : "(unknown)"; + ilOffset = frame.GetILOffset(); + methodToken = method.MetadataToken; + } + catch (Exception ex) + { + Debug.WriteLine("Exception retrieving stack frame details: {0}", ex); + } var line = new RaygunErrorStackTraceLineMessage { @@ -138,4 +150,4 @@ public static RaygunErrorStackTraceLineMessage[] BuildStackTrace(StackTrace stac return lines.ToArray(); } } -} +} \ No newline at end of file diff --git a/Mindscape.Raygun4Net.NetCore.Common/Builders/RaygunErrorMessageBuilder.cs b/Mindscape.Raygun4Net.NetCore.Common/Builders/RaygunErrorMessageBuilder.cs index 19a6fc63a..675c16167 100644 --- a/Mindscape.Raygun4Net.NetCore.Common/Builders/RaygunErrorMessageBuilder.cs +++ b/Mindscape.Raygun4Net.NetCore.Common/Builders/RaygunErrorMessageBuilder.cs @@ -24,6 +24,7 @@ protected static string FormatTypeName(Type type, bool fullName) { stringBuilder.Append(FormatTypeName(t, false)).Append(","); } + stringBuilder.Remove(stringBuilder.Length - 1, 1); stringBuilder.Append(">"); @@ -57,15 +58,27 @@ public static RaygunErrorStackTraceLineMessage[] BuildStackTrace(StackTrace stac if (method != null) { - var lineNumber = frame.GetFileLineNumber(); - var ilOffset = frame.GetILOffset(); - var methodToken = method.MetadataToken; - - var methodName = GenerateMethodName(method); - - var file = frame.GetFileName(); + string methodName = null; + string file = null; + string className = null; + var lineNumber = 0; + var ilOffset = -1; + var methodToken = -1; + + try + { + file = frame.GetFileName(); + lineNumber = frame.GetFileLineNumber(); + methodName = GenerateMethodName(method); + className = method.ReflectedType != null ? method.ReflectedType.FullName : "(unknown)"; - var className = method.DeclaringType != null ? method.DeclaringType.FullName : "(unknown)"; + ilOffset = frame.GetILOffset(); + methodToken = method.MetadataToken; + } + catch (Exception ex) + { + Debug.WriteLine("Exception retrieving stack frame details: {0}", ex); + } var line = new RaygunErrorStackTraceLineMessage { @@ -193,4 +206,4 @@ public static RaygunErrorMessage Build(Exception exception) return message; } } -} +} \ No newline at end of file From f1c8e66f536f87fa691b796731f0eda58664d09d Mon Sep 17 00:00:00 2001 From: Sean Date: Mon, 6 May 2024 10:35:12 +1200 Subject: [PATCH 03/16] Make the -1 value a constant --- .../Builders/RaygunErrorMessageBuilder.cs | 4 ++-- .../Builders/RaygunErrorMessageBuilder.cs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Mindscape.Raygun4Net.Core/Builders/RaygunErrorMessageBuilder.cs b/Mindscape.Raygun4Net.Core/Builders/RaygunErrorMessageBuilder.cs index d825763ec..efbd5790e 100644 --- a/Mindscape.Raygun4Net.Core/Builders/RaygunErrorMessageBuilder.cs +++ b/Mindscape.Raygun4Net.Core/Builders/RaygunErrorMessageBuilder.cs @@ -115,8 +115,8 @@ public static RaygunErrorStackTraceLineMessage[] BuildStackTrace(StackTrace stac string file = null; string className = null; var lineNumber = 0; - var ilOffset = -1; - var methodToken = -1; + var ilOffset = StackFrame.OFFSET_UNKNOWN; + var methodToken = StackFrame.OFFSET_UNKNOWN; try { diff --git a/Mindscape.Raygun4Net.NetCore.Common/Builders/RaygunErrorMessageBuilder.cs b/Mindscape.Raygun4Net.NetCore.Common/Builders/RaygunErrorMessageBuilder.cs index 675c16167..adef940a4 100644 --- a/Mindscape.Raygun4Net.NetCore.Common/Builders/RaygunErrorMessageBuilder.cs +++ b/Mindscape.Raygun4Net.NetCore.Common/Builders/RaygunErrorMessageBuilder.cs @@ -62,8 +62,8 @@ public static RaygunErrorStackTraceLineMessage[] BuildStackTrace(StackTrace stac string file = null; string className = null; var lineNumber = 0; - var ilOffset = -1; - var methodToken = -1; + var ilOffset = StackFrame.OFFSET_UNKNOWN; + var methodToken = StackFrame.OFFSET_UNKNOWN; try { From b437013747eb36a433cbaeb0d17cb5d6b9696676 Mon Sep 17 00:00:00 2001 From: Sean Date: Mon, 6 May 2024 10:37:21 +1200 Subject: [PATCH 04/16] Add comment on why this is in a try catch --- .../Builders/RaygunErrorMessageBuilder.cs | 4 +++- .../Builders/RaygunErrorMessageBuilder.cs | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/Mindscape.Raygun4Net.Core/Builders/RaygunErrorMessageBuilder.cs b/Mindscape.Raygun4Net.Core/Builders/RaygunErrorMessageBuilder.cs index efbd5790e..13212c8c9 100644 --- a/Mindscape.Raygun4Net.Core/Builders/RaygunErrorMessageBuilder.cs +++ b/Mindscape.Raygun4Net.Core/Builders/RaygunErrorMessageBuilder.cs @@ -124,8 +124,10 @@ public static RaygunErrorStackTraceLineMessage[] BuildStackTrace(StackTrace stac lineNumber = frame.GetFileLineNumber(); methodName = GenerateMethodName(method); className = method.ReflectedType != null ? method.ReflectedType.FullName : "(unknown)"; - ilOffset = frame.GetILOffset(); + + // This might fail in medium trust environments or for array methods, + // so don't crash the entire send process - just move on with what we have methodToken = method.MetadataToken; } catch (Exception ex) diff --git a/Mindscape.Raygun4Net.NetCore.Common/Builders/RaygunErrorMessageBuilder.cs b/Mindscape.Raygun4Net.NetCore.Common/Builders/RaygunErrorMessageBuilder.cs index adef940a4..e5a7de5bb 100644 --- a/Mindscape.Raygun4Net.NetCore.Common/Builders/RaygunErrorMessageBuilder.cs +++ b/Mindscape.Raygun4Net.NetCore.Common/Builders/RaygunErrorMessageBuilder.cs @@ -71,8 +71,10 @@ public static RaygunErrorStackTraceLineMessage[] BuildStackTrace(StackTrace stac lineNumber = frame.GetFileLineNumber(); methodName = GenerateMethodName(method); className = method.ReflectedType != null ? method.ReflectedType.FullName : "(unknown)"; - ilOffset = frame.GetILOffset(); + + // This might fail in medium trust environments or for array methods, + // so don't crash the entire send process - just move on with what we have methodToken = method.MetadataToken; } catch (Exception ex) From 1f1ae7223c358fe853f5ed711ae5aac9e1dde83c Mon Sep 17 00:00:00 2001 From: Sean Date: Thu, 16 May 2024 11:31:24 +1200 Subject: [PATCH 05/16] Add a local file system PE reader to extract the PDB information out for each stack frame --- .../Builders/RaygunErrorMessageBuilder.cs | 21 ++++-- .../Diagnostics/PdbDebugInformation.cs | 27 +++++++ .../PortableExecutableReaderExtensions.cs | 70 +++++++++++++++++++ .../RaygunErrorStackTraceLineMessage.cs | 8 +++ ...Mindscape.Raygun4Net.NetCore.Common.csproj | 5 ++ 5 files changed, 127 insertions(+), 4 deletions(-) create mode 100644 Mindscape.Raygun4Net.NetCore.Common/Diagnostics/PdbDebugInformation.cs create mode 100644 Mindscape.Raygun4Net.NetCore.Common/Diagnostics/PortableExecutableReaderExtensions.cs diff --git a/Mindscape.Raygun4Net.NetCore.Common/Builders/RaygunErrorMessageBuilder.cs b/Mindscape.Raygun4Net.NetCore.Common/Builders/RaygunErrorMessageBuilder.cs index e5a7de5bb..a40c610c5 100644 --- a/Mindscape.Raygun4Net.NetCore.Common/Builders/RaygunErrorMessageBuilder.cs +++ b/Mindscape.Raygun4Net.NetCore.Common/Builders/RaygunErrorMessageBuilder.cs @@ -3,12 +3,16 @@ using System.Collections.Generic; using System.Diagnostics; using System.Reflection; +using System.Reflection.PortableExecutable; using System.Text; +using Mindscape.Raygun4Net.Diagnostics; namespace Mindscape.Raygun4Net { public class RaygunErrorMessageBuilder { + public static Func AssemblyReaderProvider { get; set; } = PortableExecutableReaderExtensions.GetFileSystemPEReader; + protected static string FormatTypeName(Type type, bool fullName) { string name = fullName ? type.FullName : type.Name; @@ -64,6 +68,7 @@ public static RaygunErrorStackTraceLineMessage[] BuildStackTrace(StackTrace stac var lineNumber = 0; var ilOffset = StackFrame.OFFSET_UNKNOWN; var methodToken = StackFrame.OFFSET_UNKNOWN; + PdbDebugInformation debugInfo = null; try { @@ -76,6 +81,10 @@ public static RaygunErrorStackTraceLineMessage[] BuildStackTrace(StackTrace stac // This might fail in medium trust environments or for array methods, // so don't crash the entire send process - just move on with what we have methodToken = method.MetadataToken; + + // Attempt to read out the Debug Info from the PE + var peReader = AssemblyReaderProvider(method.Module.FullyQualifiedName); + debugInfo = peReader.TryGetDebugInformation(); } catch (Exception ex) { @@ -89,7 +98,11 @@ public static RaygunErrorStackTraceLineMessage[] BuildStackTrace(StackTrace stac MethodName = methodName, ClassName = className, ILOffset = ilOffset, - MethodToken = methodToken + MethodToken = methodToken, + PdbChecksum = debugInfo?.Checksum, + PdbSignature = debugInfo?.Signature, + PdbFile = debugInfo?.File, + PdbTimestamp = debugInfo?.Timestamp }; lines.Add(line); @@ -187,9 +200,7 @@ public static RaygunErrorMessage Build(Exception exception) message.Data = data; } - AggregateException ae = exception as AggregateException; - - if (ae != null && ae.InnerExceptions != null) + if (exception is AggregateException ae) { message.InnerErrors = new RaygunErrorMessage[ae.InnerExceptions.Count]; int index = 0; @@ -207,5 +218,7 @@ public static RaygunErrorMessage Build(Exception exception) return message; } + + } } \ No newline at end of file diff --git a/Mindscape.Raygun4Net.NetCore.Common/Diagnostics/PdbDebugInformation.cs b/Mindscape.Raygun4Net.NetCore.Common/Diagnostics/PdbDebugInformation.cs new file mode 100644 index 000000000..65ef2cb77 --- /dev/null +++ b/Mindscape.Raygun4Net.NetCore.Common/Diagnostics/PdbDebugInformation.cs @@ -0,0 +1,27 @@ +using System; + +namespace Mindscape.Raygun4Net.Diagnostics; + +internal sealed class PdbDebugInformation +{ + /// + /// The signature of the PE and PDB linking them together - usually a GUID + /// + public string Signature { get; set; } + + /// + /// Checksum of the PE & PDB. Format: {algorithm}:{hash:X} + /// + public string Checksum { get; set; } + + /// + /// The full location of the PDB at build time + /// + public string File { get; set; } + + /// + /// The generated Timestamp of the code at build time stored as hex + /// + public string Timestamp { get; set; } + +} \ No newline at end of file diff --git a/Mindscape.Raygun4Net.NetCore.Common/Diagnostics/PortableExecutableReaderExtensions.cs b/Mindscape.Raygun4Net.NetCore.Common/Diagnostics/PortableExecutableReaderExtensions.cs new file mode 100644 index 000000000..9e48eb4c6 --- /dev/null +++ b/Mindscape.Raygun4Net.NetCore.Common/Diagnostics/PortableExecutableReaderExtensions.cs @@ -0,0 +1,70 @@ +#nullable enable + +using System; +using System.Collections.Immutable; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Reflection.PortableExecutable; + +namespace Mindscape.Raygun4Net.Diagnostics; + +internal static class PortableExecutableReaderExtensions +{ + public static PEReader? GetFileSystemPEReader(string moduleName) + { + try + { + // Read into memory to avoid any premature stream closures + var bytes = ImmutableArray.Create(File.ReadAllBytes(moduleName)); + return new PEReader(bytes); + } + catch (Exception ex) + { + Debug.WriteLine($"Could not open module [{moduleName}] from disk: {ex}"); + return null; + } + } + + public static PdbDebugInformation? TryGetDebugInformation(this PEReader peReader) + { + try + { + return GetDebugInformation(peReader); + } + catch (Exception ex) + { + Debug.WriteLine($"Error reading PE Debug Data: {ex}"); + return null; + } + } + + private static PdbDebugInformation GetDebugInformation(this PEReader peReader) + { + var debugInfo = new PdbDebugInformation + { + Timestamp = $"{peReader.PEHeaders.CoffHeader.TimeDateStamp:X8}" + }; + + foreach (var entry in peReader.ReadDebugDirectory()) + { + if (entry.Type == DebugDirectoryEntryType.CodeView) + { + // Read the CodeView data + var codeViewData = peReader.ReadCodeViewDebugDirectoryData(entry); + + debugInfo.File = codeViewData.Path; + debugInfo.Signature = codeViewData.Guid.ToString(); + } + + if (entry.Type == DebugDirectoryEntryType.PdbChecksum) + { + var checksumEntry = peReader.ReadPdbChecksumDebugDirectoryData(entry); + var checksumHex = BitConverter.ToString(checksumEntry.Checksum.ToArray()).Replace("-", "").ToUpperInvariant(); + debugInfo.Checksum = $"{checksumEntry.AlgorithmName}:{checksumHex}"; + } + } + + return debugInfo; + } +} \ No newline at end of file diff --git a/Mindscape.Raygun4Net.NetCore.Common/Messages/RaygunErrorStackTraceLineMessage.cs b/Mindscape.Raygun4Net.NetCore.Common/Messages/RaygunErrorStackTraceLineMessage.cs index cfba00a5a..6b372c553 100644 --- a/Mindscape.Raygun4Net.NetCore.Common/Messages/RaygunErrorStackTraceLineMessage.cs +++ b/Mindscape.Raygun4Net.NetCore.Common/Messages/RaygunErrorStackTraceLineMessage.cs @@ -13,5 +13,13 @@ public class RaygunErrorStackTraceLineMessage public int ILOffset { get; set; } public int MethodToken { get; set; } + + public string PdbSignature { get; set; } + + public string PdbChecksum { get; set; } + + public string PdbFile { get; set; } + + public string PdbTimestamp { get; set; } } } \ No newline at end of file diff --git a/Mindscape.Raygun4Net.NetCore.Common/Mindscape.Raygun4Net.NetCore.Common.csproj b/Mindscape.Raygun4Net.NetCore.Common/Mindscape.Raygun4Net.NetCore.Common.csproj index 6beff7baf..f43def24e 100644 --- a/Mindscape.Raygun4Net.NetCore.Common/Mindscape.Raygun4Net.NetCore.Common.csproj +++ b/Mindscape.Raygun4Net.NetCore.Common/Mindscape.Raygun4Net.NetCore.Common.csproj @@ -39,4 +39,9 @@ + + + + + \ No newline at end of file From 6782acab1713e9feab8ef28a9ce3499479207bae Mon Sep 17 00:00:00 2001 From: Sean Date: Thu, 16 May 2024 11:41:13 +1200 Subject: [PATCH 06/16] Add a concurrent dictionary to cache the lookups for modules. This saves having to go to file/disk/whatever else, when looking for the symbols. This could be expensive on each crash/frame, so we want to avoid doing too much work --- .../Builders/RaygunErrorMessageBuilder.cs | 38 +++++++++++++++---- .../PortableExecutableReaderExtensions.cs | 9 +++-- 2 files changed, 36 insertions(+), 11 deletions(-) diff --git a/Mindscape.Raygun4Net.NetCore.Common/Builders/RaygunErrorMessageBuilder.cs b/Mindscape.Raygun4Net.NetCore.Common/Builders/RaygunErrorMessageBuilder.cs index a40c610c5..990a31b04 100644 --- a/Mindscape.Raygun4Net.NetCore.Common/Builders/RaygunErrorMessageBuilder.cs +++ b/Mindscape.Raygun4Net.NetCore.Common/Builders/RaygunErrorMessageBuilder.cs @@ -1,5 +1,6 @@ using System; using System.Collections; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.Reflection; @@ -11,8 +12,9 @@ namespace Mindscape.Raygun4Net { public class RaygunErrorMessageBuilder { + private static readonly ConcurrentDictionary DebugInformationCache = new(); public static Func AssemblyReaderProvider { get; set; } = PortableExecutableReaderExtensions.GetFileSystemPEReader; - + protected static string FormatTypeName(Type type, bool fullName) { string name = fullName ? type.FullName : type.Name; @@ -77,14 +79,10 @@ public static RaygunErrorStackTraceLineMessage[] BuildStackTrace(StackTrace stac methodName = GenerateMethodName(method); className = method.ReflectedType != null ? method.ReflectedType.FullName : "(unknown)"; ilOffset = frame.GetILOffset(); - + // This might fail in medium trust environments or for array methods, // so don't crash the entire send process - just move on with what we have methodToken = method.MetadataToken; - - // Attempt to read out the Debug Info from the PE - var peReader = AssemblyReaderProvider(method.Module.FullyQualifiedName); - debugInfo = peReader.TryGetDebugInformation(); } catch (Exception ex) { @@ -218,7 +216,31 @@ public static RaygunErrorMessage Build(Exception exception) return message; } - - + + private static PdbDebugInformation TryGetDebugInformation(string moduleName) + { + if (DebugInformationCache.TryGetValue(moduleName, out var cachedInfo)) + { + return cachedInfo; + } + + try + { + // Attempt to read out the Debug Info from the PE + var peReader = AssemblyReaderProvider(moduleName); + + if (peReader.TryGetDebugInformation(out var debugInfo)) + { + DebugInformationCache.TryAdd(moduleName, debugInfo); + return debugInfo; + } + } + catch (Exception ex) + { + Debug.WriteLine($"Could not load debug information: {ex}"); + } + + return null; + } } } \ No newline at end of file diff --git a/Mindscape.Raygun4Net.NetCore.Common/Diagnostics/PortableExecutableReaderExtensions.cs b/Mindscape.Raygun4Net.NetCore.Common/Diagnostics/PortableExecutableReaderExtensions.cs index 9e48eb4c6..0f0119c15 100644 --- a/Mindscape.Raygun4Net.NetCore.Common/Diagnostics/PortableExecutableReaderExtensions.cs +++ b/Mindscape.Raygun4Net.NetCore.Common/Diagnostics/PortableExecutableReaderExtensions.cs @@ -26,17 +26,20 @@ internal static class PortableExecutableReaderExtensions } } - public static PdbDebugInformation? TryGetDebugInformation(this PEReader peReader) + public static bool TryGetDebugInformation(this PEReader peReader, out PdbDebugInformation? debugInformation) { try { - return GetDebugInformation(peReader); + debugInformation = GetDebugInformation(peReader); + return true; } catch (Exception ex) { Debug.WriteLine($"Error reading PE Debug Data: {ex}"); - return null; } + + debugInformation = null; + return false; } private static PdbDebugInformation GetDebugInformation(this PEReader peReader) From 3c3ea4337760d0293fe1466f569d68ac76145e85 Mon Sep 17 00:00:00 2001 From: Sean Date: Thu, 16 May 2024 11:43:58 +1200 Subject: [PATCH 07/16] Whoops, actually call the thing. --- .../Builders/RaygunErrorMessageBuilder.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Mindscape.Raygun4Net.NetCore.Common/Builders/RaygunErrorMessageBuilder.cs b/Mindscape.Raygun4Net.NetCore.Common/Builders/RaygunErrorMessageBuilder.cs index 990a31b04..d48771373 100644 --- a/Mindscape.Raygun4Net.NetCore.Common/Builders/RaygunErrorMessageBuilder.cs +++ b/Mindscape.Raygun4Net.NetCore.Common/Builders/RaygunErrorMessageBuilder.cs @@ -83,6 +83,8 @@ public static RaygunErrorStackTraceLineMessage[] BuildStackTrace(StackTrace stac // This might fail in medium trust environments or for array methods, // so don't crash the entire send process - just move on with what we have methodToken = method.MetadataToken; + + debugInfo = TryGetDebugInformation(method.Module.Name); } catch (Exception ex) { From 16a37ae402ac5b07f1de6deed3ce3f6a9f6e3b05 Mon Sep 17 00:00:00 2001 From: Sean Date: Thu, 16 May 2024 11:45:02 +1200 Subject: [PATCH 08/16] Pull debug info forward because it catches its own exceptions --- .../Builders/RaygunErrorMessageBuilder.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Mindscape.Raygun4Net.NetCore.Common/Builders/RaygunErrorMessageBuilder.cs b/Mindscape.Raygun4Net.NetCore.Common/Builders/RaygunErrorMessageBuilder.cs index d48771373..f77e5457d 100644 --- a/Mindscape.Raygun4Net.NetCore.Common/Builders/RaygunErrorMessageBuilder.cs +++ b/Mindscape.Raygun4Net.NetCore.Common/Builders/RaygunErrorMessageBuilder.cs @@ -79,12 +79,11 @@ public static RaygunErrorStackTraceLineMessage[] BuildStackTrace(StackTrace stac methodName = GenerateMethodName(method); className = method.ReflectedType != null ? method.ReflectedType.FullName : "(unknown)"; ilOffset = frame.GetILOffset(); + debugInfo = TryGetDebugInformation(method.Module.Name); // This might fail in medium trust environments or for array methods, // so don't crash the entire send process - just move on with what we have methodToken = method.MetadataToken; - - debugInfo = TryGetDebugInformation(method.Module.Name); } catch (Exception ex) { From 2098a7cee224d5eb4d31fdf15f84af21947ad60a Mon Sep 17 00:00:00 2001 From: Sean Date: Thu, 16 May 2024 13:59:50 +1200 Subject: [PATCH 09/16] Port the code across to framework --- .../Builders/RaygunErrorMessageBuilder.cs | 40 ++++++++++- .../Diagnostics/PdbDebugInformation.cs | 27 +++++++ .../PortableExecutableReaderExtensions.cs | 71 +++++++++++++++++++ .../RaygunErrorStackTraceLineMessage.cs | 16 ++++- .../Mindscape.Raygun4Net.Core.csproj | 6 +- 5 files changed, 157 insertions(+), 3 deletions(-) create mode 100644 Mindscape.Raygun4Net.Core/Diagnostics/PdbDebugInformation.cs create mode 100644 Mindscape.Raygun4Net.Core/Diagnostics/PortableExecutableReaderExtensions.cs diff --git a/Mindscape.Raygun4Net.Core/Builders/RaygunErrorMessageBuilder.cs b/Mindscape.Raygun4Net.Core/Builders/RaygunErrorMessageBuilder.cs index 13212c8c9..bf1f38d6c 100644 --- a/Mindscape.Raygun4Net.Core/Builders/RaygunErrorMessageBuilder.cs +++ b/Mindscape.Raygun4Net.Core/Builders/RaygunErrorMessageBuilder.cs @@ -1,15 +1,21 @@ using System; using System.Collections; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Reflection; +using System.Reflection.PortableExecutable; +using Mindscape.Raygun4Net.Diagnostics; using Mindscape.Raygun4Net.Messages; namespace Mindscape.Raygun4Net.Builders { public class RaygunErrorMessageBuilder : RaygunErrorMessageBuilderBase { + private static readonly ConcurrentDictionary DebugInformationCache = new(); + public static Func AssemblyReaderProvider { get; set; } = PortableExecutableReaderExtensions.GetFileSystemPEReader; + public static RaygunErrorMessage Build(Exception exception) { RaygunErrorMessage message = new RaygunErrorMessage(); @@ -117,6 +123,7 @@ public static RaygunErrorStackTraceLineMessage[] BuildStackTrace(StackTrace stac var lineNumber = 0; var ilOffset = StackFrame.OFFSET_UNKNOWN; var methodToken = StackFrame.OFFSET_UNKNOWN; + PdbDebugInformation debugInfo = null; try { @@ -125,6 +132,7 @@ public static RaygunErrorStackTraceLineMessage[] BuildStackTrace(StackTrace stac methodName = GenerateMethodName(method); className = method.ReflectedType != null ? method.ReflectedType.FullName : "(unknown)"; ilOffset = frame.GetILOffset(); + debugInfo = TryGetDebugInformation(method.Module.Name); // This might fail in medium trust environments or for array methods, // so don't crash the entire send process - just move on with what we have @@ -142,7 +150,11 @@ public static RaygunErrorStackTraceLineMessage[] BuildStackTrace(StackTrace stac MethodName = methodName, ClassName = className, ILOffset = ilOffset, - MethodToken = methodToken + MethodToken = methodToken, + PdbChecksum = debugInfo?.Checksum, + PdbSignature = debugInfo?.Signature, + PdbFile = debugInfo?.File, + PdbTimestamp = debugInfo?.Timestamp }; lines.Add(line); @@ -151,5 +163,31 @@ public static RaygunErrorStackTraceLineMessage[] BuildStackTrace(StackTrace stac return lines.ToArray(); } + + private static PdbDebugInformation TryGetDebugInformation(string moduleName) + { + if (DebugInformationCache.TryGetValue(moduleName, out var cachedInfo)) + { + return cachedInfo; + } + + try + { + // Attempt to read out the Debug Info from the PE + var peReader = AssemblyReaderProvider(moduleName); + + if (peReader.TryGetDebugInformation(out var debugInfo)) + { + DebugInformationCache.TryAdd(moduleName, debugInfo); + return debugInfo; + } + } + catch (Exception ex) + { + Debug.WriteLine($"Could not load debug information: {ex}"); + } + + return null; + } } } \ No newline at end of file diff --git a/Mindscape.Raygun4Net.Core/Diagnostics/PdbDebugInformation.cs b/Mindscape.Raygun4Net.Core/Diagnostics/PdbDebugInformation.cs new file mode 100644 index 000000000..65ef2cb77 --- /dev/null +++ b/Mindscape.Raygun4Net.Core/Diagnostics/PdbDebugInformation.cs @@ -0,0 +1,27 @@ +using System; + +namespace Mindscape.Raygun4Net.Diagnostics; + +internal sealed class PdbDebugInformation +{ + /// + /// The signature of the PE and PDB linking them together - usually a GUID + /// + public string Signature { get; set; } + + /// + /// Checksum of the PE & PDB. Format: {algorithm}:{hash:X} + /// + public string Checksum { get; set; } + + /// + /// The full location of the PDB at build time + /// + public string File { get; set; } + + /// + /// The generated Timestamp of the code at build time stored as hex + /// + public string Timestamp { get; set; } + +} \ No newline at end of file diff --git a/Mindscape.Raygun4Net.Core/Diagnostics/PortableExecutableReaderExtensions.cs b/Mindscape.Raygun4Net.Core/Diagnostics/PortableExecutableReaderExtensions.cs new file mode 100644 index 000000000..3439d4932 --- /dev/null +++ b/Mindscape.Raygun4Net.Core/Diagnostics/PortableExecutableReaderExtensions.cs @@ -0,0 +1,71 @@ +using System; +using System.Collections.Immutable; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Reflection.PortableExecutable; + +namespace Mindscape.Raygun4Net.Diagnostics; + +internal static class PortableExecutableReaderExtensions +{ + public static PEReader GetFileSystemPEReader(string moduleName) + { + try + { + // Read into memory to avoid any premature stream closures + var bytes = ImmutableArray.Create(File.ReadAllBytes(moduleName)); + return new PEReader(bytes); + } + catch (Exception ex) + { + Debug.WriteLine($"Could not open module [{moduleName}] from disk: {ex}"); + return null; + } + } + + public static bool TryGetDebugInformation(this PEReader peReader, out PdbDebugInformation debugInformation) + { + try + { + debugInformation = GetDebugInformation(peReader); + return true; + } + catch (Exception ex) + { + Debug.WriteLine($"Error reading PE Debug Data: {ex}"); + } + + debugInformation = null; + return false; + } + + private static PdbDebugInformation GetDebugInformation(this PEReader peReader) + { + var debugInfo = new PdbDebugInformation + { + Timestamp = $"{peReader.PEHeaders.CoffHeader.TimeDateStamp:X8}" + }; + + foreach (var entry in peReader.ReadDebugDirectory()) + { + if (entry.Type == DebugDirectoryEntryType.CodeView) + { + // Read the CodeView data + var codeViewData = peReader.ReadCodeViewDebugDirectoryData(entry); + + debugInfo.File = codeViewData.Path; + debugInfo.Signature = codeViewData.Guid.ToString(); + } + + if (entry.Type == DebugDirectoryEntryType.PdbChecksum) + { + var checksumEntry = peReader.ReadPdbChecksumDebugDirectoryData(entry); + var checksumHex = BitConverter.ToString(checksumEntry.Checksum.ToArray()).Replace("-", "").ToUpperInvariant(); + debugInfo.Checksum = $"{checksumEntry.AlgorithmName}:{checksumHex}"; + } + } + + return debugInfo; + } +} \ No newline at end of file diff --git a/Mindscape.Raygun4Net.Core/Messages/RaygunErrorStackTraceLineMessage.cs b/Mindscape.Raygun4Net.Core/Messages/RaygunErrorStackTraceLineMessage.cs index cb25156fe..c2d5bf175 100644 --- a/Mindscape.Raygun4Net.Core/Messages/RaygunErrorStackTraceLineMessage.cs +++ b/Mindscape.Raygun4Net.Core/Messages/RaygunErrorStackTraceLineMessage.cs @@ -16,11 +16,25 @@ public class RaygunErrorStackTraceLineMessage public int MethodToken { get; set; } + + public string PdbSignature { get; set; } + + + public string PdbChecksum { get; set; } + + + public string PdbFile { get; set; } + + + public string PdbTimestamp { get; set; } + + public override string ToString() { // This exists because Reflection in Xamarin can't seem to obtain the Getter methods unless the getter is used somewhere in the code. // The getter of all properties is required to serialize the Raygun messages to JSON. - return string.Format("[RaygunErrorStackTraceLineMessage: LineNumber={0}, ClassName={1}, FileName={2}, MethodName={3}, Raw={4}, ILOffset={5}, MethodToken={6}]", LineNumber, ClassName, FileName, MethodName, Raw, ILOffset, MethodToken); + return string.Format("[RaygunErrorStackTraceLineMessage: LineNumber={0}, ClassName={1}, FileName={2}, MethodName={3}, Raw={4}, ILOffset={5}, MethodToken={6}, PdbSignature={7}, PdbChecksum={8}, PdbFile={9}, PdbTimestamp={10}]", + LineNumber, ClassName, FileName, MethodName, Raw, ILOffset, MethodToken, PdbSignature, PdbChecksum, PdbFile, PdbTimestamp); } } } \ No newline at end of file diff --git a/Mindscape.Raygun4Net.Core/Mindscape.Raygun4Net.Core.csproj b/Mindscape.Raygun4Net.Core/Mindscape.Raygun4Net.Core.csproj index dfe7a029d..b93395e0f 100644 --- a/Mindscape.Raygun4Net.Core/Mindscape.Raygun4Net.Core.csproj +++ b/Mindscape.Raygun4Net.Core/Mindscape.Raygun4Net.Core.csproj @@ -1,10 +1,10 @@  - net462 Library Mindscape.Raygun4Net Mindscape.Raygun4Net Raygun4Net.Core + net462 @@ -46,4 +46,8 @@ + + + + \ No newline at end of file From 589ac25fe5f856b7803d8eb81cc0f5a219c0883c Mon Sep 17 00:00:00 2001 From: Sean Date: Mon, 20 May 2024 12:07:19 +1200 Subject: [PATCH 10/16] Conditionally include the package for net standard --- .../Mindscape.Raygun4Net.NetCore.Common.csproj | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/Mindscape.Raygun4Net.NetCore.Common/Mindscape.Raygun4Net.NetCore.Common.csproj b/Mindscape.Raygun4Net.NetCore.Common/Mindscape.Raygun4Net.NetCore.Common.csproj index f43def24e..c6d23238d 100644 --- a/Mindscape.Raygun4Net.NetCore.Common/Mindscape.Raygun4Net.NetCore.Common.csproj +++ b/Mindscape.Raygun4Net.NetCore.Common/Mindscape.Raygun4Net.NetCore.Common.csproj @@ -40,8 +40,10 @@ - - - + + + + + \ No newline at end of file From c6af291b4e4dd9e4f8c1760a35cb9d861d9a3ede Mon Sep 17 00:00:00 2001 From: Sean Date: Tue, 21 May 2024 09:11:29 +1200 Subject: [PATCH 11/16] Rename a few of the properties and add an images section for the image specific data this should reduce the overall size of the payload --- .../Builders/RaygunErrorMessageBuilder.cs | 34 ++++++++++++--- ...ugInformation.cs => PEDebugInformation.cs} | 10 ++--- .../PortableExecutableReaderExtensions.cs | 6 +-- .../Messages/RaygunErrorMessage.cs | 5 ++- .../RaygunErrorStackTraceLineMessage.cs | 15 ++----- .../Builders/RaygunErrorMessageBuilder.cs | 43 ++++++++++++++----- ...ugInformation.cs => PEDebugInformation.cs} | 10 ++--- .../PortableExecutableReaderExtensions.cs | 6 +-- .../Messages/RaygunErrorMessage.cs | 5 ++- .../RaygunErrorStackTraceLineMessage.cs | 12 ++---- 10 files changed, 90 insertions(+), 56 deletions(-) rename Mindscape.Raygun4Net.Core/Diagnostics/{PdbDebugInformation.cs => PEDebugInformation.cs} (66%) rename Mindscape.Raygun4Net.NetCore.Common/Diagnostics/{PdbDebugInformation.cs => PEDebugInformation.cs} (66%) diff --git a/Mindscape.Raygun4Net.Core/Builders/RaygunErrorMessageBuilder.cs b/Mindscape.Raygun4Net.Core/Builders/RaygunErrorMessageBuilder.cs index bf1f38d6c..d547d47c6 100644 --- a/Mindscape.Raygun4Net.Core/Builders/RaygunErrorMessageBuilder.cs +++ b/Mindscape.Raygun4Net.Core/Builders/RaygunErrorMessageBuilder.cs @@ -13,7 +13,7 @@ namespace Mindscape.Raygun4Net.Builders { public class RaygunErrorMessageBuilder : RaygunErrorMessageBuilderBase { - private static readonly ConcurrentDictionary DebugInformationCache = new(); + private static readonly ConcurrentDictionary DebugInformationCache = new(); public static Func AssemblyReaderProvider { get; set; } = PortableExecutableReaderExtensions.GetFileSystemPEReader; public static RaygunErrorMessage Build(Exception exception) @@ -26,6 +26,13 @@ public static RaygunErrorMessage Build(Exception exception) message.ClassName = FormatTypeName(exceptionType, true); message.StackTrace = BuildStackTrace(exception); + + if (message.StackTrace != null) + { + // If we have a stack trace then grab the images out, and put de-dupe them into an array + // for the outgoing payload + message.Images = GetDebugInfoForStackFrames(message.StackTrace).ToArray(); + } if (exception.Data != null) { @@ -123,7 +130,7 @@ public static RaygunErrorStackTraceLineMessage[] BuildStackTrace(StackTrace stac var lineNumber = 0; var ilOffset = StackFrame.OFFSET_UNKNOWN; var methodToken = StackFrame.OFFSET_UNKNOWN; - PdbDebugInformation debugInfo = null; + PEDebugInformation debugInfo = null; try { @@ -151,10 +158,7 @@ public static RaygunErrorStackTraceLineMessage[] BuildStackTrace(StackTrace stac ClassName = className, ILOffset = ilOffset, MethodToken = methodToken, - PdbChecksum = debugInfo?.Checksum, - PdbSignature = debugInfo?.Signature, - PdbFile = debugInfo?.File, - PdbTimestamp = debugInfo?.Timestamp + ImageSignature = debugInfo?.Signature }; lines.Add(line); @@ -163,8 +167,24 @@ public static RaygunErrorStackTraceLineMessage[] BuildStackTrace(StackTrace stac return lines.ToArray(); } + + private static IEnumerable GetDebugInfoForStackFrames(IEnumerable frames) + { + var imageMap = DebugInformationCache.Values.ToDictionary(k => k.Signature); + var imageSet = new HashSet(); + + foreach (var stackFrame in frames) + { + if (imageMap.TryGetValue(stackFrame.ImageSignature, out var image)) + { + imageSet.Add(image); + } + } + + return imageSet; + } - private static PdbDebugInformation TryGetDebugInformation(string moduleName) + private static PEDebugInformation TryGetDebugInformation(string moduleName) { if (DebugInformationCache.TryGetValue(moduleName, out var cachedInfo)) { diff --git a/Mindscape.Raygun4Net.Core/Diagnostics/PdbDebugInformation.cs b/Mindscape.Raygun4Net.Core/Diagnostics/PEDebugInformation.cs similarity index 66% rename from Mindscape.Raygun4Net.Core/Diagnostics/PdbDebugInformation.cs rename to Mindscape.Raygun4Net.Core/Diagnostics/PEDebugInformation.cs index 65ef2cb77..933f843f9 100644 --- a/Mindscape.Raygun4Net.Core/Diagnostics/PdbDebugInformation.cs +++ b/Mindscape.Raygun4Net.Core/Diagnostics/PEDebugInformation.cs @@ -2,26 +2,26 @@ namespace Mindscape.Raygun4Net.Diagnostics; -internal sealed class PdbDebugInformation +public sealed class PEDebugInformation { /// /// The signature of the PE and PDB linking them together - usually a GUID /// - public string Signature { get; set; } + public string Signature { get; internal set; } /// /// Checksum of the PE & PDB. Format: {algorithm}:{hash:X} /// - public string Checksum { get; set; } + public string Checksum { get; internal set; } /// /// The full location of the PDB at build time /// - public string File { get; set; } + public string File { get; internal set; } /// /// The generated Timestamp of the code at build time stored as hex /// - public string Timestamp { get; set; } + public string Timestamp { get; internal set; } } \ No newline at end of file diff --git a/Mindscape.Raygun4Net.Core/Diagnostics/PortableExecutableReaderExtensions.cs b/Mindscape.Raygun4Net.Core/Diagnostics/PortableExecutableReaderExtensions.cs index 3439d4932..71f7c0dad 100644 --- a/Mindscape.Raygun4Net.Core/Diagnostics/PortableExecutableReaderExtensions.cs +++ b/Mindscape.Raygun4Net.Core/Diagnostics/PortableExecutableReaderExtensions.cs @@ -24,7 +24,7 @@ public static PEReader GetFileSystemPEReader(string moduleName) } } - public static bool TryGetDebugInformation(this PEReader peReader, out PdbDebugInformation debugInformation) + public static bool TryGetDebugInformation(this PEReader peReader, out PEDebugInformation debugInformation) { try { @@ -40,9 +40,9 @@ public static bool TryGetDebugInformation(this PEReader peReader, out PdbDebugIn return false; } - private static PdbDebugInformation GetDebugInformation(this PEReader peReader) + private static PEDebugInformation GetDebugInformation(this PEReader peReader) { - var debugInfo = new PdbDebugInformation + var debugInfo = new PEDebugInformation { Timestamp = $"{peReader.PEHeaders.CoffHeader.TimeDateStamp:X8}" }; diff --git a/Mindscape.Raygun4Net.Core/Messages/RaygunErrorMessage.cs b/Mindscape.Raygun4Net.Core/Messages/RaygunErrorMessage.cs index 380dd36ec..c50b48451 100644 --- a/Mindscape.Raygun4Net.Core/Messages/RaygunErrorMessage.cs +++ b/Mindscape.Raygun4Net.Core/Messages/RaygunErrorMessage.cs @@ -1,4 +1,5 @@ using System.Collections; +using Mindscape.Raygun4Net.Diagnostics; namespace Mindscape.Raygun4Net.Messages { @@ -15,12 +16,14 @@ public class RaygunErrorMessage public string Message { get; set; } public RaygunErrorStackTraceLineMessage[] StackTrace { get; set; } + + public PEDebugInformation[] Images { get; set; } public override string ToString() { // This exists because Reflection in Xamarin can't seem to obtain the Getter methods unless the getter is used somewhere in the code. // The getter of all properties is required to serialize the Raygun messages to JSON. - return string.Format("[RaygunErrorMessage: InnerError={0}, InnerErrors={1}, Data={2}, ClassName={3}, Message={4}, StackTrace={5}]", InnerError, InnerErrors, Data, ClassName, Message, StackTrace); + return string.Format("[RaygunErrorMessage: InnerError={0}, InnerErrors={1}, Data={2}, ClassName={3}, Message={4}, StackTrace={5}, Images={6}]", InnerError, InnerErrors, Data, ClassName, Message, StackTrace, Images); } } } diff --git a/Mindscape.Raygun4Net.Core/Messages/RaygunErrorStackTraceLineMessage.cs b/Mindscape.Raygun4Net.Core/Messages/RaygunErrorStackTraceLineMessage.cs index c2d5bf175..c42b18ba2 100644 --- a/Mindscape.Raygun4Net.Core/Messages/RaygunErrorStackTraceLineMessage.cs +++ b/Mindscape.Raygun4Net.Core/Messages/RaygunErrorStackTraceLineMessage.cs @@ -17,24 +17,15 @@ public class RaygunErrorStackTraceLineMessage public int MethodToken { get; set; } - public string PdbSignature { get; set; } - - - public string PdbChecksum { get; set; } - - - public string PdbFile { get; set; } - - - public string PdbTimestamp { get; set; } + public string ImageSignature { get; set; } public override string ToString() { // This exists because Reflection in Xamarin can't seem to obtain the Getter methods unless the getter is used somewhere in the code. // The getter of all properties is required to serialize the Raygun messages to JSON. - return string.Format("[RaygunErrorStackTraceLineMessage: LineNumber={0}, ClassName={1}, FileName={2}, MethodName={3}, Raw={4}, ILOffset={5}, MethodToken={6}, PdbSignature={7}, PdbChecksum={8}, PdbFile={9}, PdbTimestamp={10}]", - LineNumber, ClassName, FileName, MethodName, Raw, ILOffset, MethodToken, PdbSignature, PdbChecksum, PdbFile, PdbTimestamp); + return string.Format("[RaygunErrorStackTraceLineMessage: LineNumber={0}, ClassName={1}, FileName={2}, MethodName={3}, Raw={4}, ILOffset={5}, MethodToken={6}, PdbSignature={7}]", + LineNumber, ClassName, FileName, MethodName, Raw, ILOffset, MethodToken, ImageSignature); } } } \ No newline at end of file diff --git a/Mindscape.Raygun4Net.NetCore.Common/Builders/RaygunErrorMessageBuilder.cs b/Mindscape.Raygun4Net.NetCore.Common/Builders/RaygunErrorMessageBuilder.cs index f77e5457d..726b269b6 100644 --- a/Mindscape.Raygun4Net.NetCore.Common/Builders/RaygunErrorMessageBuilder.cs +++ b/Mindscape.Raygun4Net.NetCore.Common/Builders/RaygunErrorMessageBuilder.cs @@ -3,6 +3,7 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; +using System.Linq; using System.Reflection; using System.Reflection.PortableExecutable; using System.Text; @@ -12,7 +13,7 @@ namespace Mindscape.Raygun4Net { public class RaygunErrorMessageBuilder { - private static readonly ConcurrentDictionary DebugInformationCache = new(); + private static readonly ConcurrentDictionary DebugInformationCache = new(); public static Func AssemblyReaderProvider { get; set; } = PortableExecutableReaderExtensions.GetFileSystemPEReader; protected static string FormatTypeName(Type type, bool fullName) @@ -70,7 +71,7 @@ public static RaygunErrorStackTraceLineMessage[] BuildStackTrace(StackTrace stac var lineNumber = 0; var ilOffset = StackFrame.OFFSET_UNKNOWN; var methodToken = StackFrame.OFFSET_UNKNOWN; - PdbDebugInformation debugInfo = null; + PEDebugInformation debugInfo = null; try { @@ -98,10 +99,7 @@ public static RaygunErrorStackTraceLineMessage[] BuildStackTrace(StackTrace stac ClassName = className, ILOffset = ilOffset, MethodToken = methodToken, - PdbChecksum = debugInfo?.Checksum, - PdbSignature = debugInfo?.Signature, - PdbFile = debugInfo?.File, - PdbTimestamp = debugInfo?.Timestamp + ImageSignature = debugInfo?.Signature }; lines.Add(line); @@ -175,7 +173,7 @@ protected static string GenerateMethodName(MethodBase method) public static RaygunErrorMessage Build(Exception exception) { - RaygunErrorMessage message = new RaygunErrorMessage(); + var message = new RaygunErrorMessage(); var exceptionType = exception.GetType(); @@ -188,7 +186,7 @@ public static RaygunErrorMessage Build(Exception exception) { IDictionary data = new Dictionary(); - foreach (object key in exception.Data.Keys) + foreach (var key in exception.Data.Keys) { if (!RaygunClientBase.SentKey.Equals(key)) { @@ -199,12 +197,19 @@ public static RaygunErrorMessage Build(Exception exception) message.Data = data; } + if (message.StackTrace != null) + { + // If we have a stack trace then grab the images out, and put de-dupe them into an array + // for the outgoing payload + message.Images = GetDebugInfoForStackFrames(message.StackTrace).ToArray(); + } + if (exception is AggregateException ae) { message.InnerErrors = new RaygunErrorMessage[ae.InnerExceptions.Count]; - int index = 0; + var index = 0; - foreach (Exception e in ae.InnerExceptions) + foreach (var e in ae.InnerExceptions) { message.InnerErrors[index] = Build(e); index++; @@ -218,7 +223,23 @@ public static RaygunErrorMessage Build(Exception exception) return message; } - private static PdbDebugInformation TryGetDebugInformation(string moduleName) + private static IEnumerable GetDebugInfoForStackFrames(IEnumerable frames) + { + var imageMap = DebugInformationCache.Values.ToDictionary(k => k.Signature); + var imageSet = new HashSet(); + + foreach (var stackFrame in frames) + { + if (imageMap.TryGetValue(stackFrame.ImageSignature, out var image)) + { + imageSet.Add(image); + } + } + + return imageSet; + } + + private static PEDebugInformation TryGetDebugInformation(string moduleName) { if (DebugInformationCache.TryGetValue(moduleName, out var cachedInfo)) { diff --git a/Mindscape.Raygun4Net.NetCore.Common/Diagnostics/PdbDebugInformation.cs b/Mindscape.Raygun4Net.NetCore.Common/Diagnostics/PEDebugInformation.cs similarity index 66% rename from Mindscape.Raygun4Net.NetCore.Common/Diagnostics/PdbDebugInformation.cs rename to Mindscape.Raygun4Net.NetCore.Common/Diagnostics/PEDebugInformation.cs index 65ef2cb77..933f843f9 100644 --- a/Mindscape.Raygun4Net.NetCore.Common/Diagnostics/PdbDebugInformation.cs +++ b/Mindscape.Raygun4Net.NetCore.Common/Diagnostics/PEDebugInformation.cs @@ -2,26 +2,26 @@ namespace Mindscape.Raygun4Net.Diagnostics; -internal sealed class PdbDebugInformation +public sealed class PEDebugInformation { /// /// The signature of the PE and PDB linking them together - usually a GUID /// - public string Signature { get; set; } + public string Signature { get; internal set; } /// /// Checksum of the PE & PDB. Format: {algorithm}:{hash:X} /// - public string Checksum { get; set; } + public string Checksum { get; internal set; } /// /// The full location of the PDB at build time /// - public string File { get; set; } + public string File { get; internal set; } /// /// The generated Timestamp of the code at build time stored as hex /// - public string Timestamp { get; set; } + public string Timestamp { get; internal set; } } \ No newline at end of file diff --git a/Mindscape.Raygun4Net.NetCore.Common/Diagnostics/PortableExecutableReaderExtensions.cs b/Mindscape.Raygun4Net.NetCore.Common/Diagnostics/PortableExecutableReaderExtensions.cs index 0f0119c15..b1d56302f 100644 --- a/Mindscape.Raygun4Net.NetCore.Common/Diagnostics/PortableExecutableReaderExtensions.cs +++ b/Mindscape.Raygun4Net.NetCore.Common/Diagnostics/PortableExecutableReaderExtensions.cs @@ -26,7 +26,7 @@ internal static class PortableExecutableReaderExtensions } } - public static bool TryGetDebugInformation(this PEReader peReader, out PdbDebugInformation? debugInformation) + public static bool TryGetDebugInformation(this PEReader peReader, out PEDebugInformation? debugInformation) { try { @@ -42,9 +42,9 @@ public static bool TryGetDebugInformation(this PEReader peReader, out PdbDebugIn return false; } - private static PdbDebugInformation GetDebugInformation(this PEReader peReader) + private static PEDebugInformation GetDebugInformation(this PEReader peReader) { - var debugInfo = new PdbDebugInformation + var debugInfo = new PEDebugInformation { Timestamp = $"{peReader.PEHeaders.CoffHeader.TimeDateStamp:X8}" }; diff --git a/Mindscape.Raygun4Net.NetCore.Common/Messages/RaygunErrorMessage.cs b/Mindscape.Raygun4Net.NetCore.Common/Messages/RaygunErrorMessage.cs index 45b3b1f8a..d6ac7d3e5 100644 --- a/Mindscape.Raygun4Net.NetCore.Common/Messages/RaygunErrorMessage.cs +++ b/Mindscape.Raygun4Net.NetCore.Common/Messages/RaygunErrorMessage.cs @@ -1,4 +1,5 @@ using System.Collections; +using Mindscape.Raygun4Net.Diagnostics; namespace Mindscape.Raygun4Net { @@ -15,12 +16,14 @@ public class RaygunErrorMessage public string Message { get; set; } public RaygunErrorStackTraceLineMessage[] StackTrace { get; set; } + + public PEDebugInformation[] Images { get; set; } public override string ToString() { // This exists because Reflection in Xamarin can't seem to obtain the Getter methods unless the getter is used somewhere in the code. // The getter of all properties is required to serialize the Raygun messages to JSON. - return $"[RaygunErrorMessage: InnerError={InnerError}, InnerErrors={InnerErrors}, Data={Data}, ClassName={ClassName}, Message={Message}, StackTrace={StackTrace}]"; + return $"[RaygunErrorMessage: InnerError={InnerError}, InnerErrors={InnerErrors}, Data={Data}, ClassName={ClassName}, Message={Message}, StackTrace={StackTrace}, Images={Images}]"; } } } diff --git a/Mindscape.Raygun4Net.NetCore.Common/Messages/RaygunErrorStackTraceLineMessage.cs b/Mindscape.Raygun4Net.NetCore.Common/Messages/RaygunErrorStackTraceLineMessage.cs index 6b372c553..c966f8b14 100644 --- a/Mindscape.Raygun4Net.NetCore.Common/Messages/RaygunErrorStackTraceLineMessage.cs +++ b/Mindscape.Raygun4Net.NetCore.Common/Messages/RaygunErrorStackTraceLineMessage.cs @@ -1,4 +1,6 @@ -namespace Mindscape.Raygun4Net +using Mindscape.Raygun4Net.Diagnostics; + +namespace Mindscape.Raygun4Net { public class RaygunErrorStackTraceLineMessage { @@ -14,12 +16,6 @@ public class RaygunErrorStackTraceLineMessage public int MethodToken { get; set; } - public string PdbSignature { get; set; } - - public string PdbChecksum { get; set; } - - public string PdbFile { get; set; } - - public string PdbTimestamp { get; set; } + public string ImageSignature { get; set; } } } \ No newline at end of file From 6827ed595ffc0fe1ab127d6c764836e0388eedf8 Mon Sep 17 00:00:00 2001 From: Sean Date: Tue, 21 May 2024 09:24:20 +1200 Subject: [PATCH 12/16] Empty check the cache for an early return and null check signature --- .../Builders/RaygunErrorMessageBuilder.cs | 15 ++++++++++----- .../Builders/RaygunErrorMessageBuilder.cs | 7 ++++++- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/Mindscape.Raygun4Net.Core/Builders/RaygunErrorMessageBuilder.cs b/Mindscape.Raygun4Net.Core/Builders/RaygunErrorMessageBuilder.cs index d547d47c6..47c6b930e 100644 --- a/Mindscape.Raygun4Net.Core/Builders/RaygunErrorMessageBuilder.cs +++ b/Mindscape.Raygun4Net.Core/Builders/RaygunErrorMessageBuilder.cs @@ -15,7 +15,7 @@ public class RaygunErrorMessageBuilder : RaygunErrorMessageBuilderBase { private static readonly ConcurrentDictionary DebugInformationCache = new(); public static Func AssemblyReaderProvider { get; set; } = PortableExecutableReaderExtensions.GetFileSystemPEReader; - + public static RaygunErrorMessage Build(Exception exception) { RaygunErrorMessage message = new RaygunErrorMessage(); @@ -26,7 +26,7 @@ public static RaygunErrorMessage Build(Exception exception) message.ClassName = FormatTypeName(exceptionType, true); message.StackTrace = BuildStackTrace(exception); - + if (message.StackTrace != null) { // If we have a stack trace then grab the images out, and put de-dupe them into an array @@ -170,12 +170,17 @@ public static RaygunErrorStackTraceLineMessage[] BuildStackTrace(StackTrace stac private static IEnumerable GetDebugInfoForStackFrames(IEnumerable frames) { + if (DebugInformationCache.IsEmpty) + { + return Enumerable.Empty(); + } + var imageMap = DebugInformationCache.Values.ToDictionary(k => k.Signature); var imageSet = new HashSet(); - + foreach (var stackFrame in frames) { - if (imageMap.TryGetValue(stackFrame.ImageSignature, out var image)) + if (stackFrame.ImageSignature != null && imageMap.TryGetValue(stackFrame.ImageSignature, out var image)) { imageSet.Add(image); } @@ -183,7 +188,7 @@ private static IEnumerable GetDebugInfoForStackFrames(IEnume return imageSet; } - + private static PEDebugInformation TryGetDebugInformation(string moduleName) { if (DebugInformationCache.TryGetValue(moduleName, out var cachedInfo)) diff --git a/Mindscape.Raygun4Net.NetCore.Common/Builders/RaygunErrorMessageBuilder.cs b/Mindscape.Raygun4Net.NetCore.Common/Builders/RaygunErrorMessageBuilder.cs index 726b269b6..0d256cc07 100644 --- a/Mindscape.Raygun4Net.NetCore.Common/Builders/RaygunErrorMessageBuilder.cs +++ b/Mindscape.Raygun4Net.NetCore.Common/Builders/RaygunErrorMessageBuilder.cs @@ -225,12 +225,17 @@ public static RaygunErrorMessage Build(Exception exception) private static IEnumerable GetDebugInfoForStackFrames(IEnumerable frames) { + if (DebugInformationCache.IsEmpty) + { + return Enumerable.Empty(); + } + var imageMap = DebugInformationCache.Values.ToDictionary(k => k.Signature); var imageSet = new HashSet(); foreach (var stackFrame in frames) { - if (imageMap.TryGetValue(stackFrame.ImageSignature, out var image)) + if (stackFrame.ImageSignature != null && imageMap.TryGetValue(stackFrame.ImageSignature, out var image)) { imageSet.Add(image); } From 876544f0fadf96c4a287335ef11b99cba1fcad79 Mon Sep 17 00:00:00 2001 From: Sean Date: Tue, 21 May 2024 10:27:32 +1200 Subject: [PATCH 13/16] Fix typo --- Mindscape.Raygun4Net.Core/Builders/RaygunErrorMessageBuilder.cs | 2 +- .../Builders/RaygunErrorMessageBuilder.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Mindscape.Raygun4Net.Core/Builders/RaygunErrorMessageBuilder.cs b/Mindscape.Raygun4Net.Core/Builders/RaygunErrorMessageBuilder.cs index 47c6b930e..646cb6d96 100644 --- a/Mindscape.Raygun4Net.Core/Builders/RaygunErrorMessageBuilder.cs +++ b/Mindscape.Raygun4Net.Core/Builders/RaygunErrorMessageBuilder.cs @@ -29,7 +29,7 @@ public static RaygunErrorMessage Build(Exception exception) if (message.StackTrace != null) { - // If we have a stack trace then grab the images out, and put de-dupe them into an array + // If we have a stack trace then grab the debug info images, and put them into an array // for the outgoing payload message.Images = GetDebugInfoForStackFrames(message.StackTrace).ToArray(); } diff --git a/Mindscape.Raygun4Net.NetCore.Common/Builders/RaygunErrorMessageBuilder.cs b/Mindscape.Raygun4Net.NetCore.Common/Builders/RaygunErrorMessageBuilder.cs index 0d256cc07..f43d5e204 100644 --- a/Mindscape.Raygun4Net.NetCore.Common/Builders/RaygunErrorMessageBuilder.cs +++ b/Mindscape.Raygun4Net.NetCore.Common/Builders/RaygunErrorMessageBuilder.cs @@ -199,7 +199,7 @@ public static RaygunErrorMessage Build(Exception exception) if (message.StackTrace != null) { - // If we have a stack trace then grab the images out, and put de-dupe them into an array + // If we have a stack trace then grab the debug info images, and put them into an array // for the outgoing payload message.Images = GetDebugInfoForStackFrames(message.StackTrace).ToArray(); } From fe2a4ff3c7a1cf4a08c20b2a2050fb6ea758a43c Mon Sep 17 00:00:00 2001 From: Sean Date: Tue, 21 May 2024 10:29:21 +1200 Subject: [PATCH 14/16] Fix empty lines --- .../Messages/RaygunErrorStackTraceLineMessage.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/Mindscape.Raygun4Net.Core/Messages/RaygunErrorStackTraceLineMessage.cs b/Mindscape.Raygun4Net.Core/Messages/RaygunErrorStackTraceLineMessage.cs index c42b18ba2..26ebc7e90 100644 --- a/Mindscape.Raygun4Net.Core/Messages/RaygunErrorStackTraceLineMessage.cs +++ b/Mindscape.Raygun4Net.Core/Messages/RaygunErrorStackTraceLineMessage.cs @@ -16,10 +16,8 @@ public class RaygunErrorStackTraceLineMessage public int MethodToken { get; set; } - public string ImageSignature { get; set; } - public override string ToString() { // This exists because Reflection in Xamarin can't seem to obtain the Getter methods unless the getter is used somewhere in the code. From e471e0d6b89b91a39bc65282583f2e9a97b30ebd Mon Sep 17 00:00:00 2001 From: Sean Date: Tue, 21 May 2024 13:41:54 +1200 Subject: [PATCH 15/16] Update the caching based off Phill's feedback --- .../Builders/RaygunErrorMessageBuilder.cs | 10 +++++----- .../Builders/RaygunErrorMessageBuilder.cs | 10 +++++----- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/Mindscape.Raygun4Net.Core/Builders/RaygunErrorMessageBuilder.cs b/Mindscape.Raygun4Net.Core/Builders/RaygunErrorMessageBuilder.cs index 646cb6d96..5d5ff2823 100644 --- a/Mindscape.Raygun4Net.Core/Builders/RaygunErrorMessageBuilder.cs +++ b/Mindscape.Raygun4Net.Core/Builders/RaygunErrorMessageBuilder.cs @@ -201,11 +201,11 @@ private static PEDebugInformation TryGetDebugInformation(string moduleName) // Attempt to read out the Debug Info from the PE var peReader = AssemblyReaderProvider(moduleName); - if (peReader.TryGetDebugInformation(out var debugInfo)) - { - DebugInformationCache.TryAdd(moduleName, debugInfo); - return debugInfo; - } + // If we got this far, the assembly/module exists, so whatever the result + // put it in the cache to prevent reading the disk over and over + peReader.TryGetDebugInformation(out var debugInfo); + DebugInformationCache.TryAdd(moduleName, debugInfo); + return debugInfo; } catch (Exception ex) { diff --git a/Mindscape.Raygun4Net.NetCore.Common/Builders/RaygunErrorMessageBuilder.cs b/Mindscape.Raygun4Net.NetCore.Common/Builders/RaygunErrorMessageBuilder.cs index f43d5e204..dfdefa364 100644 --- a/Mindscape.Raygun4Net.NetCore.Common/Builders/RaygunErrorMessageBuilder.cs +++ b/Mindscape.Raygun4Net.NetCore.Common/Builders/RaygunErrorMessageBuilder.cs @@ -256,11 +256,11 @@ private static PEDebugInformation TryGetDebugInformation(string moduleName) // Attempt to read out the Debug Info from the PE var peReader = AssemblyReaderProvider(moduleName); - if (peReader.TryGetDebugInformation(out var debugInfo)) - { - DebugInformationCache.TryAdd(moduleName, debugInfo); - return debugInfo; - } + // If we got this far, the assembly/module exists, so whatever the result + // put it in the cache to prevent reading the disk over and over + peReader.TryGetDebugInformation(out var debugInfo); + DebugInformationCache.TryAdd(moduleName, debugInfo); + return debugInfo; } catch (Exception ex) { From 159a5eee9938636336611ed86dbc01e9ddb83109 Mon Sep 17 00:00:00 2001 From: Sean Date: Tue, 21 May 2024 15:23:41 +1200 Subject: [PATCH 16/16] Add a tests and filter out nulls --- .../Builders/RaygunErrorMessageBuilder.cs | 2 +- .../Builders/RaygunErrorMessageBuilder.cs | 2 +- .../RaygunErrorMessageExceptionTests.cs | 10 ++++++++++ .../RaygunErrorMessageExceptionTests.cs | 10 ++++++++++ 4 files changed, 22 insertions(+), 2 deletions(-) diff --git a/Mindscape.Raygun4Net.Core/Builders/RaygunErrorMessageBuilder.cs b/Mindscape.Raygun4Net.Core/Builders/RaygunErrorMessageBuilder.cs index 5d5ff2823..1d6bbabe5 100644 --- a/Mindscape.Raygun4Net.Core/Builders/RaygunErrorMessageBuilder.cs +++ b/Mindscape.Raygun4Net.Core/Builders/RaygunErrorMessageBuilder.cs @@ -175,7 +175,7 @@ private static IEnumerable GetDebugInfoForStackFrames(IEnume return Enumerable.Empty(); } - var imageMap = DebugInformationCache.Values.ToDictionary(k => k.Signature); + var imageMap = DebugInformationCache.Values.Where(x => x != null).ToDictionary(k => k.Signature); var imageSet = new HashSet(); foreach (var stackFrame in frames) diff --git a/Mindscape.Raygun4Net.NetCore.Common/Builders/RaygunErrorMessageBuilder.cs b/Mindscape.Raygun4Net.NetCore.Common/Builders/RaygunErrorMessageBuilder.cs index dfdefa364..b54cbac98 100644 --- a/Mindscape.Raygun4Net.NetCore.Common/Builders/RaygunErrorMessageBuilder.cs +++ b/Mindscape.Raygun4Net.NetCore.Common/Builders/RaygunErrorMessageBuilder.cs @@ -230,7 +230,7 @@ private static IEnumerable GetDebugInfoForStackFrames(IEnume return Enumerable.Empty(); } - var imageMap = DebugInformationCache.Values.ToDictionary(k => k.Signature); + var imageMap = DebugInformationCache.Values.Where(x => x != null).ToDictionary(k => k.Signature); var imageSet = new HashSet(); foreach (var stackFrame in frames) diff --git a/Mindscape.Raygun4Net.NetCore.Tests/RaygunErrorMessageExceptionTests.cs b/Mindscape.Raygun4Net.NetCore.Tests/RaygunErrorMessageExceptionTests.cs index b95543b40..af6c8f03f 100644 --- a/Mindscape.Raygun4Net.NetCore.Tests/RaygunErrorMessageExceptionTests.cs +++ b/Mindscape.Raygun4Net.NetCore.Tests/RaygunErrorMessageExceptionTests.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; namespace Mindscape.Raygun4Net.NetCore.Tests { @@ -45,5 +46,14 @@ public void IncludeNamespaceInExceptionClassName() var message = RaygunErrorMessageBuilder.Build(_exception); Assert.That("System.InvalidOperationException", Is.EqualTo(message.ClassName)); } + + [Test] + public void ErrorMessageBuilder_WhenAssembliesAreAvailable_HasImageInfo() + { + var errorMessage = RaygunErrorMessageBuilder.Build(_exception); + Assert.That(errorMessage.Images, Is.Not.Null); + Assert.That(errorMessage.Images, Is.Not.Empty); + Assert.That(errorMessage.StackTrace.All(x => x.ImageSignature != null), Is.True); + } } } diff --git a/Mindscape.Raygun4Net.Tests/RaygunErrorMessageExceptionTests.cs b/Mindscape.Raygun4Net.Tests/RaygunErrorMessageExceptionTests.cs index f69a4c176..aa928ba83 100644 --- a/Mindscape.Raygun4Net.Tests/RaygunErrorMessageExceptionTests.cs +++ b/Mindscape.Raygun4Net.Tests/RaygunErrorMessageExceptionTests.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using Mindscape.Raygun4Net.Builders; using Mindscape.Raygun4Net.Tests.Model; using NUnit.Framework; @@ -48,5 +49,14 @@ public void IncludeNamespaceInExceptionClassName() var message = RaygunErrorMessageBuilder.Build(_exception); Assert.That("System.InvalidOperationException", Is.EqualTo(message.ClassName)); } + + [Test] + public void ErrorMessageBuilder_WhenAssembliesAreAvailable_HasImageInfo() + { + var errorMessage = RaygunErrorMessageBuilder.Build(_exception); + Assert.That(errorMessage.Images, Is.Not.Null); + Assert.That(errorMessage.Images, Is.Not.Empty); + Assert.That(errorMessage.StackTrace.All(x => x.ImageSignature != null), Is.True); + } } }