diff --git a/.gitignore b/.gitignore index 719a3a76..685400fd 100644 --- a/.gitignore +++ b/.gitignore @@ -67,3 +67,7 @@ sysinfo.txt # Crashlytics generated file crashlytics-build.properties + +# Platforms +iOS/ +Mac/ \ No newline at end of file diff --git a/README.md b/README.md index cf3c8fc5..af26f731 100644 --- a/README.md +++ b/README.md @@ -34,4 +34,4 @@ catch(Exception exception) ``` ## Documentation -For more information about the Unity integration, including installation, usage, and configuration options, see the [Unity Integration guide](https://docs.saucelabs.com/error-reporting/platform-integrations/unity/setup/) in the Sauce Labs documentation. \ No newline at end of file +For more information about the Unity integration, including installation, usage, and configuration options, see the [Unity Integration guide](https://docs.saucelabs.com/error-reporting/platform-integrations/unity/setup/) in the Sauce Labs documentation. diff --git a/Runtime/Model/BacktraceRawStackTraceParser.cs b/Runtime/Model/BacktraceRawStackTraceParser.cs new file mode 100644 index 00000000..1e6e1cd1 --- /dev/null +++ b/Runtime/Model/BacktraceRawStackTraceParser.cs @@ -0,0 +1,478 @@ +using System; +using System.Collections.Generic; +using UnityEngine; + +namespace Backtrace.Unity.Model +{ + /// + /// Responsible solely for parsing raw Unity / Android / native stack trace strings + /// into instances. + /// + internal class BacktraceRawStackTraceParser + { + private static readonly string[] _javaExtensions = new string[] { ".java", ".kt", "java." }; + + internal bool NativeStackTrace { get; private set; } + + internal List ConvertStackFrames(IEnumerable frames) + { + var result = new List(); + + foreach (var frame in frames) + { + if (string.IsNullOrEmpty(frame)) + { + continue; + } + + BacktraceStackFrame convertedFrame = TryParseFrameOrFallback(frame); + if (convertedFrame != null) + { + result.Add(convertedFrame); + } + } + return result; + } + + private BacktraceStackFrame TryParseFrameOrFallback(string frame) + { + try + { + string frameString = frame.Trim(); + + // validate if stack trace has exception header + int methodNameEndIndex = frameString.IndexOf(')'); + int openParentIndex = frameString.LastIndexOf('(', methodNameEndIndex); // we require a '(' that appears before this ')' + + if (methodNameEndIndex == -1 || openParentIndex == -1 || openParentIndex > methodNameEndIndex) + { + // If either index is missing, it's an invalid frame + Debug.LogWarning($"Invalid stack frame format: '{frameString}'."); + return new BacktraceStackFrame { FunctionName = frame }; + } + + return ParseStacktraceFrame(frameString, methodNameEndIndex); + } + catch (Exception e) + { + Debug.LogError($"Exception while parsing stack frame: '{frame}'. Exception: {e}"); + return null; + } + } + + private BacktraceStackFrame ParseStacktraceFrame(string frameString, int methodNameEndIndex) + { + if (frameString.StartsWith("0x", StringComparison.Ordinal)) + { + return ParseNativeFrame(frameString); + } + else if (frameString.StartsWith("#", StringComparison.Ordinal)) + { + return SetJITStackTraceInformation(frameString); + } +#if UNITY_ANDROID || UNITY_EDITOR + // verify if the stack trace is from Unity by checking if the + // by checking source code location + const char argumentStartInitialChar = '('; + var sourceCodeStartIndex = frameString.IndexOf(argumentStartInitialChar, methodNameEndIndex + 1); + if (sourceCodeStartIndex > -1) + { + return SetDefaultStackTraceInformation(frameString, methodNameEndIndex); + } + // verify if frame has parameters that contain source code information + var methodStartIndex = frameString.IndexOf(argumentStartInitialChar); + if (methodStartIndex == -1) + { + return new BacktraceStackFrame() + { + FunctionName = frameString + }; + } + + NativeStackTrace = true; + // add length of the '(' + methodStartIndex += 1; + var methodArguments = frameString.Substring(methodStartIndex, methodNameEndIndex - methodStartIndex); + if (methodArguments.IndexOf(':') != -1 || methodArguments == "Unknown Source") + { + return SetAndroidStackTraceInformation(frameString, methodStartIndex, methodNameEndIndex); + } + // check if popular extensions are available in the frame to determine if + // the frame has any reference to java. + for (int i = 0; i < _javaExtensions.Length; i++) + { + if (frameString.IndexOf(_javaExtensions[i], StringComparison.Ordinal) != -1) + { + return SetAndroidStackTraceInformation(frameString, methodStartIndex, methodNameEndIndex); + } + } +#endif + return SetDefaultStackTraceInformation(frameString, methodNameEndIndex); + } + + /// + /// Try to convert JIT stack trace. + /// + /// JIT stack frame. + /// Backtrace stack frame. + private BacktraceStackFrame SetJITStackTraceInformation(string frameString) + { + var stackFrame = new BacktraceStackFrame + { + StackFrameType = Types.BacktraceStackFrameType.Native + }; + if (!frameString.StartsWith("#", StringComparison.Ordinal)) + { + //handle situation when we detected jit stack trace + // but jit stack trace doesn't start with # + stackFrame.FunctionName = frameString; + return stackFrame; + } + + frameString = frameString.Substring(frameString.IndexOf(' ')).Trim(); + const string monoJitPrefix = "(Mono JIT Code)"; + var monoPrefixIndex = frameString.IndexOf(monoJitPrefix, StringComparison.Ordinal); + if (monoPrefixIndex != -1) + { + frameString = frameString.Substring(monoPrefixIndex + monoJitPrefix.Length).Trim(); + } + + const string managedWraperPrefix = "(wrapper managed-to-native)"; + var managedWraperIndex = frameString.IndexOf(managedWraperPrefix, StringComparison.Ordinal); + if (managedWraperIndex != -1) + { + frameString = frameString.Substring(managedWraperIndex + managedWraperPrefix.Length).Trim(); + } + + // right now we outfiltered all known prefixes + // we should have only function name with parameters + + // filter parameters, if we can't use full frameString as function name + var parametersStart = frameString.IndexOf('('); + var parametersEnd = frameString.IndexOf(')'); + if (parametersStart != -1 && parametersEnd != -1 && parametersEnd > parametersStart) + { + stackFrame.FunctionName = frameString.Substring(0, parametersStart).Trim(); + } + else + { + stackFrame.FunctionName = frameString; + } + + if (!string.IsNullOrEmpty(stackFrame.FunctionName)) + { + var libraryNameSeparator = stackFrame.FunctionName.IndexOf(':'); + if (libraryNameSeparator != -1) + { + stackFrame.Library = stackFrame.FunctionName.Substring(0, libraryNameSeparator).Trim(); + stackFrame.FunctionName = stackFrame.FunctionName.Substring(++libraryNameSeparator).Trim(); + } + else + { + stackFrame.Library = "native"; + } + } + return stackFrame; + } + + /// + /// Try to safely convert a native stack frame string into a Backtrace stack frame. + /// Handles frames with or without symbols (e.g. "0xADDR (Module)" or "0xADDR (Module) Symbol"). + /// Prevents ArgumentOutOfRangeException by validating index ranges and allowing empty symbols. + /// + /// Raw native stack frame line to parse. + /// Parsed Backtrace stack frame containing address, library, function name, and optional line number. + internal static BacktraceStackFrame ParseNativeFrame(string frameString) + { + var stackFrame = new BacktraceStackFrame + { + StackFrameType = Types.BacktraceStackFrameType.Native, + }; + + if (string.IsNullOrWhiteSpace(frameString)) + { + return stackFrame; + } + + frameString = frameString.Trim(); + + int index = 0; + + + if (!TryParseAddress(frameString, ref index, stackFrame)) + { + return Fallback(stackFrame, frameString); + } + + TryParseLibrary(frameString, ref index, stackFrame); + ParseFunction(frameString, index, stackFrame); + NormalizeWrapper(ref stackFrame.FunctionName); + ParseBracketSource(ref stackFrame); + return stackFrame; + } + + private static bool TryParseAddress(string s, ref int index, BacktraceStackFrame frame) + { + if (!s.StartsWith("0x", StringComparison.Ordinal)) + return false; + + int space = s.IndexOf(' '); + if (space <= 2) + return false; + + frame.Address = s.Substring(0, space); + index = space + 1; + return true; + } + + private static void TryParseLibrary(string s, ref int index, BacktraceStackFrame frame) + { + SkipSpaces(s, ref index); + + if (index >= s.Length || s[index] != '(') + return; + + int start = index + 1; + int end = s.IndexOf(')', start); + + if (end > start) + { + frame.Library = s.Substring(start, end - start); + index = end + 1; + } + } + + private static void ParseFunction(string s, int index, BacktraceStackFrame frame) + { + int atIndex = s.IndexOf(" (at ", index, StringComparison.Ordinal); + + if (atIndex == -1) + { + frame.FunctionName = (index < s.Length) + ? s.Substring(index).Trim() + : string.Empty; + } + else + { + frame.FunctionName = s.Substring(index, atIndex - index).Trim(); + + ParseAtSource(s, atIndex, frame); + } + } + + private static void ParseAtSource(string s, int atIndex, BacktraceStackFrame frame) + { + int pathStart = atIndex + 5; // skip " (at " + int endParen = s.LastIndexOf(')'); + + if (endParen <= pathStart) + return; + + string pathAndLine = s.Substring(pathStart, endParen - pathStart); + + int colon = pathAndLine.LastIndexOf(':'); + if (colon <= 0 || colon >= pathAndLine.Length - 1) + return; + + string path = pathAndLine.Substring(0, colon); + string lineStr = pathAndLine.Substring(colon + 1); + + if (int.TryParse(lineStr, out int line)) + { + frame.Line = line; + frame.SourceCode = path; + } + } + + private static void ParseBracketSource(ref BacktraceStackFrame frame) + { + var fn = frame.FunctionName; + if (string.IsNullOrEmpty(fn)) + return; + + int start = fn.IndexOf('['); + int end = fn.IndexOf(']'); + + if (start == -1 || end == -1 || end <= start) + return; + + string content = fn.Substring(start + 1, end - start - 1); + + int colon = content.LastIndexOf(':'); + if (colon <= 0 || colon >= content.Length - 1) + return; + + string file = content.Substring(0, colon); + string lineStr = content.Substring(colon + 1); + + if (int.TryParse(lineStr, out int line)) + { + frame.Line = line; + + if (string.IsNullOrEmpty(frame.SourceCodeFullPath)) + { + frame.SourceCodeFullPath = file; + + } + + if (string.IsNullOrEmpty(frame.SourceCode)) + { + frame.SourceCode = file; + } + + // remove [file:line] from function name + frame.FunctionName = RemoveSegment(fn, start, end + 1); + } + } + + + /// + /// Try to convert Android stack frame string to Backtrace stack frame. + /// + /// Android stack frame. + /// Index of parameters start character '('. + /// Index of parameters end character ')'. + /// Backtrace stack frame. + private BacktraceStackFrame SetAndroidStackTraceInformation(string frameString, int parameterStart, int parameterEnd) + { + var stackFrame = new BacktraceStackFrame + { + FunctionName = frameString.Substring(0, parameterStart - 1), + StackFrameType = Types.BacktraceStackFrameType.Android + }; + var possibleSourceCodeInformation = frameString.Substring(parameterStart, parameterEnd - parameterStart); + + var sourceCodeInformation = possibleSourceCodeInformation.Split(':'); + if (sourceCodeInformation.Length == 2) + { + stackFrame.Library = sourceCodeInformation[0]; + int.TryParse(sourceCodeInformation[1], out stackFrame.Line); + } + else if (frameString.StartsWith("java.lang", StringComparison.Ordinal) || possibleSourceCodeInformation == "Unknown Source") + { + stackFrame.Library = possibleSourceCodeInformation; + } + + return stackFrame; + } + + /// + /// Try to convert default Unity stack frame to Backtrace stack frame. + /// + /// Unity stack frame. + /// Index of method name end character ')'. + /// Backtrace stack frame. + private BacktraceStackFrame SetDefaultStackTraceInformation(string frameString, int methodNameEndIndex) + { + const string wrapperPrefix = "(wrapper remoting-invoke-with-check)"; + if (frameString.StartsWith(wrapperPrefix, StringComparison.Ordinal)) + { + frameString = frameString.Replace(wrapperPrefix, string.Empty); + } + // detect source code information - format : 'at (...)' + + // find source code start based on method parameter start index + int sourceInformationStartIndex = frameString.IndexOf('(', methodNameEndIndex + 1); + if (sourceInformationStartIndex == -1) + { + return new BacktraceStackFrame() + { + FunctionName = frameString, + StackFrameType = Types.BacktraceStackFrameType.Dotnet + }; + } + + // get source code information substring + int sourceStringLength = frameString.Length - sourceInformationStartIndex; + string sourceString = frameString.Trim() + .Substring(sourceInformationStartIndex, sourceStringLength); + + int lineNumberSeparator = sourceString.LastIndexOf(':') + 1; + int endLineNumberSeparator = sourceString.LastIndexOf(')') - lineNumberSeparator; + + var result = new BacktraceStackFrame() + { + FunctionName = frameString.Substring(0, methodNameEndIndex + 1).Trim(), + StackFrameType = Types.BacktraceStackFrameType.Dotnet + }; + + if (endLineNumberSeparator > 0 && lineNumberSeparator > 0) + { + string lineNumberString = sourceString.Substring(lineNumberSeparator, endLineNumberSeparator); + int.TryParse(lineNumberString, out result.Line); + } + + if (sourceString[0] == '(' && lineNumberSeparator != -1) + { + //avoid "at" or '(' + int atSeparator = sourceString.StartsWith("(at", StringComparison.Ordinal) + ? 3 + : 1; + int endLine = lineNumberSeparator == 0 + ? sourceString.LastIndexOf(')') - atSeparator + : lineNumberSeparator - 1 - atSeparator; + if (endLine < 0) + { + return result; + } + var substring = sourceString.Substring(atSeparator, endLine); + + result.Library = (substring == null ? string.Empty : substring.Trim()); + + if (!string.IsNullOrEmpty(result.Library)) + { + var testString = string.Copy(result.Library); + testString = testString.Replace("0", string.Empty); + if (testString.Length <= 2) + { + result.Library = null; + } + } + if (string.IsNullOrEmpty(result.Library)) + { + result.Library = result.FunctionName.Substring(0, result.FunctionName.LastIndexOf(".", result.FunctionName.IndexOf("(", StringComparison.Ordinal), StringComparison.Ordinal)); + } + } + return result; + } + + private static void NormalizeWrapper(ref string functionName) + { + if (string.IsNullOrEmpty(functionName)) + return; + + functionName = RemovePrefix(functionName, "(wrapper managed-to-native)"); + functionName = RemovePrefix(functionName, "(wrapper runtime-invoke)"); + } + + private static string RemovePrefix(string value, string prefix) + { + if (value.StartsWith(prefix, StringComparison.Ordinal)) + { + return value.Substring(prefix.Length).Trim(); + } + return value; + } + + private static void SkipSpaces(string s, ref int index) + { + while (index < s.Length && s[index] == ' ') + index++; + } + + private static string RemoveSegment(string s, int start, int end) + { + if (start >= end || start < 0 || end > s.Length) + return s; + + return (s.Substring(0, start) + s.Substring(end)).Trim(); + } + + private static BacktraceStackFrame Fallback(BacktraceStackFrame frame, string raw) + { + frame.FunctionName = raw; + return frame; + } + } +} + diff --git a/Runtime/Model/BacktraceRawStackTraceParser.cs.meta b/Runtime/Model/BacktraceRawStackTraceParser.cs.meta new file mode 100644 index 00000000..c335b152 --- /dev/null +++ b/Runtime/Model/BacktraceRawStackTraceParser.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: d450322870c65034cab6c1e809eea6e9 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Model/BacktraceStackFrame.cs b/Runtime/Model/BacktraceStackFrame.cs index 0a32d082..5e6ace93 100644 --- a/Runtime/Model/BacktraceStackFrame.cs +++ b/Runtime/Model/BacktraceStackFrame.cs @@ -28,9 +28,15 @@ public string FileName { get { - return string.IsNullOrEmpty(Library) - ? GetFileNameFromFunctionName() - : Library.IndexOfAny(Path.GetInvalidPathChars()) == -1 && Path.HasExtension(Path.GetFileName(Library)) + if (!string.IsNullOrEmpty(SourceCode)) { + return SourceCode; + } + + if (string.IsNullOrEmpty(Library)) { + return GetFileNameFromFunctionName(); + } + + return Library.IndexOfAny(Path.GetInvalidPathChars()) == -1 && Path.HasExtension(Path.GetFileName(Library)) ? GetFileNameFromLibraryName() : GetFileNameFromFunctionName(); } diff --git a/Runtime/Model/BacktraceUnhandledException.cs b/Runtime/Model/BacktraceUnhandledException.cs index cb34c611..907a7e1c 100644 --- a/Runtime/Model/BacktraceUnhandledException.cs +++ b/Runtime/Model/BacktraceUnhandledException.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using UnityEngine; @@ -12,8 +12,6 @@ namespace Backtrace.Unity.Model public class BacktraceUnhandledException : Exception { private bool _header = false; - - private static string[] _javaExtensions = new string[] { ".java", ".kt", "java." }; public bool Header { get @@ -108,339 +106,11 @@ private string GetStackTraceErrorMessage(string beginningOfTheFrame) } - /// - /// Convert Unity error log message to Stack trace that Backtrace uses - /// in an exception report. Method below support default Unity and Android stack trace. - /// private List ConvertStackFrames(IEnumerable frames) { - var result = new List(); - - for (int frameIndex = 0; frameIndex < frames.Count(); frameIndex++) - { - var frame = frames.ElementAt(frameIndex); - if (string.IsNullOrEmpty(frame)) - { - continue; - } - - string frameString = frame.Trim(); - - // validate if stack trace has exception header - int methodNameEndIndex = frameString.IndexOf(')'); - if (methodNameEndIndex == -1) - { - //invalid stack frame - result.Add(new BacktraceStackFrame() { FunctionName = frame }); - continue; - - } - - //methodname index should be greater than 0 AND '(' should be before ')' - if (methodNameEndIndex < 1 && frameString[methodNameEndIndex - 1] != '(') - { - result.Add(new BacktraceStackFrame() - { - FunctionName = frame - }); - } - - result.Add(ConvertFrame(frameString, methodNameEndIndex)); - } - return result; - } - - private BacktraceStackFrame ConvertFrame(string frameString, int methodNameEndIndex) - { - if (frameString.StartsWith("0x")) - { - return SetNativeStackTraceInformation(frameString); - } - else if (frameString.StartsWith("#")) - { - return SetJITStackTraceInformation(frameString); - } - // allow to execute this code in the editor - // to validate parser via unit tests -#if UNITY_ANDROID || UNITY_EDITOR - // verify if the stack trace is from Unity by checking if the - // by checking source code location - const char argumentStartInitialChar = '('; - var sourceCodeStartIndex = frameString.IndexOf(argumentStartInitialChar, methodNameEndIndex + 1); - if (sourceCodeStartIndex > -1) - { - return SetDefaultStackTraceInformation(frameString, methodNameEndIndex); - } - // verify if frame has parameters that contain source code information - var methodStartIndex = frameString.IndexOf(argumentStartInitialChar); - if (methodStartIndex == -1) - { - return new BacktraceStackFrame() - { - FunctionName = frameString - }; - } - - NativeStackTrace = true; - // add length of the '(' - methodStartIndex += 1; - var methodArguments = frameString.Substring(methodStartIndex, methodNameEndIndex - methodStartIndex); - if (methodArguments.IndexOf(':') != -1 || methodArguments == "Unknown Source") - { - return SetAndroidStackTraceInformation(frameString, methodStartIndex, methodNameEndIndex); - } - // check if popular extensions are available in the frame to determine if - // the frame has any reference to java. - for (int i = 0; i < _javaExtensions.Length; i++) - { - if (frameString.IndexOf(_javaExtensions[i]) != -1) - { - return SetAndroidStackTraceInformation(frameString, methodStartIndex, methodNameEndIndex); - } - } -#endif - return SetDefaultStackTraceInformation(frameString, methodNameEndIndex); - - - } - - /// - /// Try to convert JIT stack trace - /// - /// JIT stack frame - /// Backtrace stack frame - private BacktraceStackFrame SetJITStackTraceInformation(string frameString) - { - var stackFrame = new BacktraceStackFrame - { - StackFrameType = Types.BacktraceStackFrameType.Native - }; - if (!frameString.StartsWith("#")) - { - //handle sitaution when we detected jit stack trace - // but jit stack trace doesn't start with # - stackFrame.FunctionName = frameString; - return stackFrame; - } - - frameString = frameString.Substring(frameString.IndexOf(' ')).Trim(); - const string monoJitPrefix = "(Mono JIT Code)"; - var monoPrefixIndex = frameString.IndexOf(monoJitPrefix); - if (monoPrefixIndex != -1) - { - frameString = frameString.Substring(monoPrefixIndex + monoJitPrefix.Length).Trim(); - } - - const string managedWraperPrefix = "(wrapper managed-to-native)"; - var managedWraperIndex = frameString.IndexOf(managedWraperPrefix); - if (managedWraperIndex != -1) - { - frameString = frameString.Substring(managedWraperIndex + managedWraperPrefix.Length).Trim(); - } - - // right now we outfiltered all known prefixes - // we should have only function name with parameters - - // filter parameters, if we can't use full frameString as function name - var parametersStart = frameString.IndexOf('('); - var parametersEnd = frameString.IndexOf(')'); - if (parametersStart != -1 && parametersEnd != -1 && parametersEnd > parametersStart) - { - stackFrame.FunctionName = frameString.Substring(0, parametersStart).Trim(); - } - else - { - stackFrame.FunctionName = frameString; - } - - if (!string.IsNullOrEmpty(stackFrame.FunctionName)) - { - var libraryNameSeparator = stackFrame.FunctionName.IndexOf(':'); - if (libraryNameSeparator != -1) - { - stackFrame.Library = stackFrame.FunctionName.Substring(0, libraryNameSeparator).Trim(); - stackFrame.FunctionName = stackFrame.FunctionName.Substring(++libraryNameSeparator).Trim(); - } - else - { - stackFrame.Library = "native"; - } - } - return stackFrame; - - } - - /// - /// Try to convert native stack frame - /// - /// Native stack frame - /// Backtrace stack frame - private BacktraceStackFrame SetNativeStackTraceInformation(string frameString) - { - var stackFrame = new BacktraceStackFrame - { - StackFrameType = Types.BacktraceStackFrameType.Native - }; - // parse address - var addressSubstringIndex = frameString.IndexOf(' '); - if (addressSubstringIndex == -1) - { - stackFrame.FunctionName = frameString; - return stackFrame; - } - stackFrame.Address = frameString.Substring(0, addressSubstringIndex); - var indexPointer = addressSubstringIndex + 1; - - // parse library - if (frameString[indexPointer] == '(') - { - indexPointer = indexPointer + 1; - var libraryNameSubstringIndex = frameString.IndexOf(')', indexPointer); - stackFrame.Library = frameString.Substring(indexPointer, libraryNameSubstringIndex - indexPointer); - indexPointer = libraryNameSubstringIndex + 2; - } - - stackFrame.FunctionName = frameString.Substring(indexPointer); - //cleanup function name - if (stackFrame.FunctionName.StartsWith("(wrapper managed-to-native)")) - { - stackFrame.FunctionName = stackFrame.FunctionName.Replace("(wrapper managed-to-native)", string.Empty).Trim(); - } - - if (stackFrame.FunctionName.StartsWith("(wrapper runtime-invoke)")) - { - stackFrame.FunctionName = stackFrame.FunctionName.Replace("(wrapper runtime-invoke)", string.Empty).Trim(); - } - - // try to find source code information - int sourceCodeStartIndex = stackFrame.FunctionName.IndexOf('['); - int sourceCodeEndIndex = stackFrame.FunctionName.IndexOf(']'); - if (sourceCodeStartIndex != -1 && sourceCodeEndIndex != -1) - { - sourceCodeStartIndex = sourceCodeStartIndex + 1; - var sourceCodeInformation = stackFrame.FunctionName.Substring( - sourceCodeStartIndex, - sourceCodeEndIndex - sourceCodeStartIndex); - - var sourceCodeParts = sourceCodeInformation.Split(new char[] { ':' }, 2); - if (sourceCodeParts.Length == 2) - { - int.TryParse(sourceCodeParts[1], out stackFrame.Line); - stackFrame.Library = sourceCodeParts[0]; - stackFrame.FunctionName = stackFrame.FunctionName.Substring(sourceCodeEndIndex + 2); - } - } - - return stackFrame; - } - - /// - /// Try to convert Android stack frame string to Backtrace stack frame - /// - /// Android stack frame - /// Index of parameters start character '(' - /// Index of paramters end character ')' - /// Backtrace stack frame - private BacktraceStackFrame SetAndroidStackTraceInformation(string frameString, int parameterStart, int parameterEnd) - { - var stackFrame = new BacktraceStackFrame - { - FunctionName = frameString.Substring(0, parameterStart - 1), - StackFrameType = Types.BacktraceStackFrameType.Android - }; - var possibleSourceCodeInformation = frameString.Substring(parameterStart, parameterEnd - parameterStart); - - var sourceCodeInformation = possibleSourceCodeInformation.Split(':'); - if (sourceCodeInformation.Length == 2) - { - stackFrame.Library = sourceCodeInformation[0]; - int.TryParse(sourceCodeInformation[1], out stackFrame.Line); - } - else if (frameString.StartsWith("java.lang") || possibleSourceCodeInformation == "Unknown Source") - { - stackFrame.Library = possibleSourceCodeInformation; - } - - return stackFrame; - } - - /// - /// Try to convert defalt unity stack frame to Backtrace stack frame - /// - /// Unity stack frame - /// - /// - private BacktraceStackFrame SetDefaultStackTraceInformation(string frameString, int methodNameEndIndex) - { - const string wrapperPrefix = "(wrapper remoting-invoke-with-check)"; - if (frameString.StartsWith(wrapperPrefix)) - { - frameString = frameString.Replace(wrapperPrefix, string.Empty); - } - // detect source code information - format : 'at (...)' - - // find source code start based on method parameter start index - int sourceInformationStartIndex = frameString.IndexOf('(', methodNameEndIndex + 1); - if (sourceInformationStartIndex == -1) - { - return new BacktraceStackFrame() - { - FunctionName = frameString, - StackFrameType = Types.BacktraceStackFrameType.Dotnet - }; - } - - // get source code information substring - int sourceStringLength = frameString.Length - sourceInformationStartIndex; - string sourceString = frameString.Trim() - .Substring(sourceInformationStartIndex, sourceStringLength); - - int lineNumberSeparator = sourceString.LastIndexOf(':') + 1; - int endLineNumberSeparator = sourceString.LastIndexOf(')') - lineNumberSeparator; - - var result = new BacktraceStackFrame() - { - FunctionName = frameString.Substring(0, methodNameEndIndex + 1).Trim(), - StackFrameType = Types.BacktraceStackFrameType.Dotnet - }; - - if (endLineNumberSeparator > 0 && lineNumberSeparator > 0) - { - string lineNumberString = sourceString.Substring(lineNumberSeparator, endLineNumberSeparator); - int.TryParse(lineNumberString, out result.Line); - } - - if (sourceString[0] == '(' && lineNumberSeparator != -1) - { - //avoid "at" or '(' - int atSeparator = sourceString.StartsWith("(at") - ? 3 - : 1; - int endLine = lineNumberSeparator == 0 - ? sourceString.LastIndexOf(')') - atSeparator - : lineNumberSeparator - 1 - atSeparator; - if (endLine < 0) - { - return result; - } - var substring = sourceString.Substring(atSeparator, endLine); - - result.Library = (substring == null ? string.Empty : substring.Trim()); - - if (!string.IsNullOrEmpty(result.Library)) - { - var testString = string.Copy(result.Library); - testString = testString.Replace("0", string.Empty); - if (testString.Length <= 2) - { - result.Library = null; - } - } - if (string.IsNullOrEmpty(result.Library)) - { - result.Library = result.FunctionName.Substring(0, result.FunctionName.LastIndexOf(".", result.FunctionName.IndexOf("("))); - } - } + var parser = new BacktraceRawStackTraceParser(); + var result = parser.ConvertStackFrames(frames); + NativeStackTrace = parser.NativeStackTrace; return result; } @@ -476,6 +146,7 @@ private void TrySetClassifier() // handle Android Java exception real classifier if (guessedClassifier == androidExceptionPrefix && guessedClassifier.Length > 1 + && messageParts.Length > 1 && messageParts[1].EndsWith(exceptionPrefix)) { Classifier = messageParts[1].Trim(); diff --git a/Runtime/Types/BacktraceStackFrameType.cs b/Runtime/Types/BacktraceStackFrameType.cs index cac46476..36acff15 100644 --- a/Runtime/Types/BacktraceStackFrameType.cs +++ b/Runtime/Types/BacktraceStackFrameType.cs @@ -1,6 +1,6 @@ namespace Backtrace.Unity.Types { - enum BacktraceStackFrameType + public enum BacktraceStackFrameType { Unknown, Dotnet, diff --git a/Tests/Editor.meta b/Tests/Editor.meta new file mode 100644 index 00000000..816be08b --- /dev/null +++ b/Tests/Editor.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: a280423b29aae9842a003c2cb836de4f +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Runtime/BacktraceStackTraceTests.cs b/Tests/Runtime/BacktraceStackTraceTests.cs index 74d085fa..0af2fb57 100644 --- a/Tests/Runtime/BacktraceStackTraceTests.cs +++ b/Tests/Runtime/BacktraceStackTraceTests.cs @@ -261,8 +261,8 @@ public class BacktraceStackTraceTests Type = StackTraceType.Native, StackFrame = "0x00000266BD67295B (Mono JIT Code) [firstSceneButtons.cs:41] firstSceneButtons:Start ()", Line = 41, + Library = "Mono JIT Code", FileName = "firstSceneButtons.cs", - Library = "firstSceneButtons.cs", Address = "0x00000266BD67295B", FunctionName = "firstSceneButtons:Start ()" }, @@ -281,7 +281,7 @@ public class BacktraceStackTraceTests StackFrame = "0x00007FFFEEB8CBB0 (mono-2.0-bdwgc) [mini-runtime.c:2809] mono_jit_runtime_invoke", Line = 2809, FileName = "mini-runtime.c", - Library = "mini-runtime.c", + Library = "mono-2.0-bdwgc", Address = "0x00007FFFEEB8CBB0", FunctionName = "mono_jit_runtime_invoke" }, diff --git a/Tests/Runtime/Model.meta b/Tests/Runtime/Model.meta new file mode 100644 index 00000000..f6b4e281 --- /dev/null +++ b/Tests/Runtime/Model.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 8d214bb14609c3b47937d0fd46e8c1f7 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Runtime/Model/BacktraceRawStackTraceParserTests.cs b/Tests/Runtime/Model/BacktraceRawStackTraceParserTests.cs new file mode 100644 index 00000000..af0e2625 --- /dev/null +++ b/Tests/Runtime/Model/BacktraceRawStackTraceParserTests.cs @@ -0,0 +1,35 @@ +using Backtrace.Unity.Model; +using Backtrace.Unity.Types; +using NUnit.Framework; + +namespace Backtrace.Unity.Tests.Runtime +{ + public class BacktraceRawStackTraceParserTests + { + [TestCase("0x00007ffad7723088 (UnityPlayer)", "0x00007ffad7723088", "UnityPlayer", "", 0, null, false, BacktraceStackFrameType.Native)] + [TestCase("0x00007ffac58086f5 (GameAssembly) DebugLogHandler_Internal_Log_m20852F18A88BB18425BA07260545E3968F7EA76C (at D:/project/app/module/Library/Example/artifacts/build/il2cppOutput/cpp/UnityEngine.CoreModule.cpp:40786)", + "0x00007ffac58086f5", "GameAssembly", "DebugLogHandler_Internal_Log_m20852F18A88BB18425BA07260545E3968F7EA76C", 40786, "D:/project/app/module/Library/Example/artifacts/build/il2cppOutput/cpp/UnityEngine.CoreModule.cpp", false, BacktraceStackFrameType.Native)] + [TestCase("0x00007ffbede3e8d7 (KERNEL32) BaseThreadInitThunk", "0x00007ffbede3e8d7", "KERNEL32", "BaseThreadInitThunk", 0, null, false, BacktraceStackFrameType.Native)] + [TestCase("nonsense frame with no address", null, null, "nonsense frame with no address", 0, null, false, BacktraceStackFrameType.Native)] + public void ParseNativeFrame_WithVariousInputs_ReturnsExpectedStackFrame( + string input, + string expectedAddress, + string expectedLibrary, + string expectedFunctionName, + int expectedLine, + string expectedSourceCode, + bool expectedInvalidFrame, + Types.BacktraceStackFrameType expectedStackFrameType) + { + var backtraceStackFrame = BacktraceRawStackTraceParser.ParseNativeFrame(input); + + Assert.AreEqual(expectedAddress, backtraceStackFrame.Address, "Address mismatch"); + Assert.AreEqual(expectedLibrary, backtraceStackFrame.Library, "Library mismatch"); + Assert.AreEqual(expectedFunctionName, backtraceStackFrame.FunctionName, "Function name mismatch"); + Assert.AreEqual(expectedLine, backtraceStackFrame.Line, "Line number mismatch"); + Assert.AreEqual(expectedSourceCode, backtraceStackFrame.SourceCode, "Source code path mismatch"); + Assert.AreEqual(expectedInvalidFrame, backtraceStackFrame.InvalidFrame, "InvalidFrame flag mismatch"); + Assert.AreEqual(expectedStackFrameType, backtraceStackFrame.StackFrameType, "StackFrameType mismatch"); + } + } +} diff --git a/Tests/Runtime/Model/BacktraceRawStackTraceParserTests.cs.meta b/Tests/Runtime/Model/BacktraceRawStackTraceParserTests.cs.meta new file mode 100644 index 00000000..de174c46 --- /dev/null +++ b/Tests/Runtime/Model/BacktraceRawStackTraceParserTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: a99b0743e15f2473894961191792e63d +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Tests.meta b/Tests/Tests.meta new file mode 100644 index 00000000..6fb38a4e --- /dev/null +++ b/Tests/Tests.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 8afeef20f8c9c954e8fca5f6880a2595 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: