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: