diff --git a/Android/lib/arm64-v8a/libbacktrace-native.so b/Android/lib/arm64-v8a/libbacktrace-native.so
index d3feac38..5f4b1db7 100644
Binary files a/Android/lib/arm64-v8a/libbacktrace-native.so and b/Android/lib/arm64-v8a/libbacktrace-native.so differ
diff --git a/Android/lib/armeabi-v7a/libbacktrace-native.so b/Android/lib/armeabi-v7a/libbacktrace-native.so
index 089c9263..fbb64323 100644
Binary files a/Android/lib/armeabi-v7a/libbacktrace-native.so and b/Android/lib/armeabi-v7a/libbacktrace-native.so differ
diff --git a/Android/lib/x86/libbacktrace-native.so b/Android/lib/x86/libbacktrace-native.so
index beffaba6..4b01a7ba 100644
Binary files a/Android/lib/x86/libbacktrace-native.so and b/Android/lib/x86/libbacktrace-native.so differ
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 44d6c8c0..c766b0ed 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,20 +1,30 @@
# Backtrace Unity Release Notes
+## Version 3.3.0
+- `BacktraceReport` stack trace now includes the file name of the stack frame.
+- Performance improvements:
+ - JSON algorithm performance improvements - avoid analyzing data types.
+ - improved library attributes management.
+ - improved Unity logs management.
+- Support for Low Memory error reports on Anrdoid and iOS (these are sometimes referred to as OOM or Out Of Memory errors). If a low memory situation is detected, backtrace-unity will attempt to generate and submit a native error report to the Backtrace instance. The report will have the `error.type` value of `Low Memory`.
+- New support for hang detection on Android and iOS. If a game experiences non responsiviness after 5 seconds, backtrace-unity will generate an error report to the Backtrace instance. The report will have the `error.type` value of `Hang`.
+
+
## Version 3.2.6
- `BacktraceClient` will apply sampling only to errors lacking exception information.
- Fixed annotations nullable value.
-- Renamed `BacktraceUnhandledException` classifier to `error`.
+- Renamed `BacktraceUnhandledException` classifier, which was generated from a Debug.LogError call, to `error`.
- Fixed nullable environment annotation value.
## Version 3.2.5
- Added `BacktraceClient` Initialization method that allows developer to intialize Backtrace integration without adding game object to game scene.
- Fixed invalid `meta` file for iOS integration for Unity 2019.2.13f1.
- HTTP communication messages improvements - right now Backtrace-Unity plugin will print only one error message when network failure happen. Backtrace-Unity will stop printing failures until next successfull report upload.
-- Sampling skip fraction - Enables a new random sampling mechanism for unhandled exceptions - by default sampling is equal to 0.01 - which means only 1% of randomply sampling reports will be send to Backtrace. If you would like to send all unhandled exceptions to Backtrace - please replace 0.01 value with 1.
+- Sampling skip fraction - Enables a new random sampling mechanism for BacktraceUnhandledExceptions (errors from Debug.LogError), by setting default sampling equal to 0.01 - which means only 1% of randomly sampled Debug.LogError reports will be send to Backtrace. If you would like to send all Debug.LogError to Backtrace - please replace 0.01 value with 1.
**Be aware**
-By default Backtrace library will send only 1% of your reports - please change this value if you would like to send more unhandled exceptions to server.
+By default Backtrace library will send only 1% of your Debug.LogError reports - please change this value if you would like to send more Debug.LogErrors to server.
diff --git a/Editor/BacktraceClientConfigurationEditor.cs b/Editor/BacktraceClientConfigurationEditor.cs
index ff0c4ff4..ec1803aa 100644
--- a/Editor/BacktraceClientConfigurationEditor.cs
+++ b/Editor/BacktraceClientConfigurationEditor.cs
@@ -26,7 +26,7 @@ public override void OnInspectorGUI()
#else
settings.IgnoreSslValidation = false;
#endif
-#if UNITY_ANDROID
+#if UNITY_ANDROID || UNITY_IOS
settings.HandleANR = EditorGUILayout.Toggle(BacktraceConfigurationLabels.LABEL_HANDLE_ANR, settings.HandleANR);
#endif
settings.GameObjectDepth = EditorGUILayout.IntField(BacktraceConfigurationLabels.LABEL_GAME_OBJECT_DEPTH, settings.GameObjectDepth);
diff --git a/Editor/BacktraceConfigurationEditor.cs b/Editor/BacktraceConfigurationEditor.cs
index f3921a02..fb81e73a 100644
--- a/Editor/BacktraceConfigurationEditor.cs
+++ b/Editor/BacktraceConfigurationEditor.cs
@@ -41,14 +41,16 @@ public override void OnInspectorGUI()
serializedObject.FindProperty("IgnoreSslValidation"),
new GUIContent(BacktraceConfigurationLabels.LABEL_IGNORE_SSL_VALIDATION));
#endif
-#if UNITY_ANDROID
+#if UNITY_ANDROID || UNITY_IOS
EditorGUILayout.PropertyField(
serializedObject.FindProperty("HandleANR"),
new GUIContent(BacktraceConfigurationLabels.LABEL_HANDLE_ANR));
- EditorGUILayout.PropertyField(
- serializedObject.FindProperty("SymbolsUploadToken"),
- new GUIContent(BacktraceConfigurationLabels.LABEL_SYMBOLS_UPLOAD_TOKEN));
+#if UNITY_2019_2_OR_NEWER && UNITY_ANDROID
+ EditorGUILayout.PropertyField(
+ serializedObject.FindProperty("SymbolsUploadToken"),
+ new GUIContent(BacktraceConfigurationLabels.LABEL_SYMBOLS_UPLOAD_TOKEN));
+#endif
#endif
EditorGUILayout.PropertyField(
serializedObject.FindProperty("UseNormalizedExceptionMessage"),
diff --git a/Editor/Native/Android/SymbolsUpload.cs b/Editor/Native/Android/SymbolsUpload.cs
index a5b322f6..4160c71d 100644
--- a/Editor/Native/Android/SymbolsUpload.cs
+++ b/Editor/Native/Android/SymbolsUpload.cs
@@ -134,7 +134,7 @@ private string ConvertSymbols(string symbolsArchive)
private string GetPathToSymbolsArchive(BuildReport report)
{
- var archiveName = string.Format("{0}-{1}-v{2}.symbols.zip", Path.GetFileNameWithoutExtension(report.summary.outputPath), PlayerSettings.bundleVersion, PlayerSettings.Android.bundleVersionCode);
+ var archiveName = string.Format("{0}-{1}-v{2}.symbols.zip", Path.GetFileNameWithoutExtension(report.summary.outputPath), PlayerSettings.bundleVersion, PlayerSettings.Android.bundleVersionCode.ToString());
return Path.Combine(Directory.GetParent(report.summary.outputPath).FullName, archiveName);
}
diff --git a/README.md b/README.md
index 1a587d3e..c5893b79 100644
--- a/README.md
+++ b/README.md
@@ -1,17 +1,17 @@
# Backtrace Unity support
-[Backtrace](http://backtrace.io/)'s integration with Unity allows developers to capture and report handled and unhandled Unity exceptions and crashes to their Backtrace instance, instantly offering the ability to prioritize and debug software errors.
+[Backtrace](http://backtrace.io/)'s integration with Unity allows developers to capture and report log errors, handled and unhandled Unity exceptions, and native crashes to their Backtrace instance, instantly offering the ability to prioritize and debug software errors.
[](https://openupm.com/packages/io.backtrace.unity/)
[github release]: (https://github.com/backtrace-labs/backtrace-labs/)
-- [Features Summary ](#features-summary--a-name--features-summary----a-)
+- [Features Summary](#features-summary)
- [Prerequisites](#prerequisites)
- [Platforms Supported](#platforms-supported)
-- [Setup ](#setup--a-name--installation----a-)
+- [Setup](#installation)
- [Android Specific information](#android-specific-information)
- [iOS Specific information](#ios-specific-information)
- [Data Privacy](#data-privacy)
@@ -37,8 +37,8 @@ catch(Exception exception){
# Feature Summary
-- Lightweight library that quickly submits handled and unhandled exceptions and crashes to Backtrace
- - Supports wide range of Unity versions (2017.4+) and deployments (iOS, Android, Windows, Mac, WebGL, PS4, Xbox, Switch, Stadia)
+- Lightweight library that quickly submits log errors, handled and unhandled exceptions, and native crashes to Backtrace
+ - Supports wide range of Unity versions (2017.4+) and deployments (iOS, Android, Windows, Mac, WebGL, PS4/5 Xbox One/S/X, Nintendo Switch, Stadia)
- Install via Universal Package Manager
- Collect detailed context
- Callstacks, including function names and line numbers where possible
@@ -48,8 +48,9 @@ catch(Exception exception){
- Android NDK Crashes; iOS Native Crashes, Windows Native Crashes
- Client-side features
- Deduplication options and custom client side fingerprinting
+ - Client side filters and sampling controls
- Offline crash capture/storage for future collection
- - Customizable event handlers and base classes, Client side filters
+ - Customizable event handlers and base classes
- Performance statistics collection option for timing observability
- Unity IDE integration to configure Backtrace behaviors in your game.
@@ -66,9 +67,9 @@ PC - Windows, Mac
Web - WebGL
Game Consoles - PlayStation4, Xbox One, Nintendo Switch
There are some differences in capabilities that backtrace-unity provides based on the platform. Major capabilities are summarized as follows:
-* All Platforms - Unhandled Exceptions, Handled Exceptions, Custom Indexable Metadata, File Attachments*, Last N Log Lines, Automatic attachment of Screenshots, Client Side Deduplication Rules*, Client Side Submission Filtering, Client Side Submission Limits, Performance Diagnostics, Offline Database*(Except Nintendo Switch)
+* All Platforms - Errors, Unhandled Exceptions, Handled Exceptions, Custom Indexable Metadata, File Attachments*, Last N Log Lines, Automatic attachment of Screenshots, Client Side Deduplication Rules*, Client Side Submission Filtering, Client Side Submission Limits, Performance Diagnostics, Offline Database*(Except Nintendo Switch)
* Android -Identified by attribute uname.sysname = Android; ANRs (Hangs), Native Process and Memory Information, Java Exception Handler (Plugins, Exported Game in Android Studio), NDK crashes.
-* iOS - Identified by attribute uname.sysname = IOS; Native Engine and Plugin Crashes.
+* iOS - Identified by attribute uname.sysname = IOS; ANRs (Hangs), Native Engine and Plugin Crashes.
* WebGL - Identified by attribute uname.sysname = WebGL. The attribute device.model is currently used to share the browser information. Note that stacktraces for WebGL errors are only available if you choose to enable them in the Publishing Settings / Enable Exceptions drop down. More details in https://docs.unity3d.com/Manual/webgl-building.html
* Switch - Identified by attribute uname.sysname = Switch. Note that the attribute GUID is regenerated with each Switch restart (It is not an accurate count of number of Users or Devices. It is a count of Switch Sessions). Note that the current release does no support Offline Database or related features.
* PlayStation4 - Identified by attribute uname.sysname = PS4
@@ -125,7 +126,16 @@ If you need to use more advanced configuration, `Initialize` method accepts a `B
## Plugin best practices
-Plugin allows you to define maximum depth of game objects. By default its disabled (Game object depth is equal to -1). If you will use 0 as maximum depth of game object we will use default game object limit - 16. If you would like to specify game object depth size to n, please insert n in Backtrace configuration text box. If you require game obejct depth to be above 30, please contact support.
+The plugin will report on 5 'classes' or errors:
+1) Log Errors - Programmers use Debug.LogError(https://docs.unity3d.com/ScriptReference/Debug.LogError.html), a variant of Debug.Log, to log error messages to the console.
+2) Unhandled Exceptions - Unhandled Exceptions are exceptions in a game that occur outside of an explicit try / catch statement.
+3) Handled Exceptions - Exceptions that are explicitly caught and handled.
+4) Crashes - An end to the game play experience. The game crashes or restarts.
+5) Hangs - A game is non responsive. Some platforms will tell the user “This app has stopped responding
+
+The plugin provide 2 controls for manaing what the client will report. [SkipReports](#filtering-a-report) allows you to tell the client to only report on specific classes of these errors, and [Log Error Sampling](#sampling-log-errors) will allow you to tell the client to sample the Debug Log errors so programmers don't 'shoot themselves in the foot' by releasing the plugin to a many users and report on hundreds of low priority and recoverable errors that they may not be intending to capture.
+
+The plugin allows you to collect game objects if you like by specifying a depth of hierarchy to inspect to for game objects. By default its disabled (Game object depth is equal to -1). If you will use 0 as maximum depth of game object we will use default game object limit - 16. If you would like to specify game object depth size to n, please insert n in Backtrace configuration text box. If you require game obejct depth to be above 30, please contact support.
## Backtrace Client and Offline Database Settings
@@ -139,12 +149,12 @@ The following is a reference guide to the Backtrace Client fields:
- Send unhandled native game crashes on startup: Try to find game native crashes and send them on Game startup.
- Handle unhandled exceptions: Toggle this on or off to set the library to handle unhandled exceptions that are not captured by try-catch blocks.
- Symbols upload token - If you want to upload Unity debug symbols for Android NDK Native Crash debugging, enter your Backtrace Symbol upload token here. This option is available only in Android build.
-- Log random sampling rate - Enables a new random sampling mechanism for error message - **by default** sampling is equal to **0.01** - which means only **1%** of randomply sampling **reports will be send** to Backtrace. If you would like to send all error messages to Backtrace - please replace 0.01 value with 1.
+- Log random sampling rate - Enables a random sampling mechanism for DebugLog.error messages - **by default** sampling is equal to **0.01** - which means only **1%** of randomply sampling **reports will be send** to Backtrace. If you would like to send all DebugLog.error messages to Backtrace - please replace 0.01 value with 1.
- Game Object Depth Limit: Allows developer to filter number of game object childrens in Backtrace report.
- Collect last n game logs: Collect last n number of logs generated by game.
- Enabled performance statistics: Allows `BacktraceClient` to measure execution time and include performance information as report attributes.
- Ignore SSL validation: Unity by default will validate ssl certificates. By using this option you can avoid ssl certificates validation. However, if you don't need to ignore ssl validation, please set this option to false.
-- Handle ANR (Application not responding) - this options is available only in Android build. It allows to catch ANR (application not responding) events happened to your game in Android devices. In this release, ANR is set to detect after 5 seconds. This will be configurable in a future release.
+- Handle ANR (Application not responding) - this options is available only in Android and iOS build. It allows to catch ANR (application not responding) events happened to your game in Android/iOS devices. In this release, ANR is set to detect after 5 seconds. This will be configurable in a future release.
- Enable Database: When this setting is toggled, the backtrace-unity plugin will configure an offline database that will store reports if they can't be submitted do to being offline or not finding a network. When toggled on, there are a number of Database settings to configure.
- Backtrace Database path: This is the path to directory where the Backtrace database will store reports on your game. You can use interpolated strings SUCH AS
`${Application.persistentDataPath}/backtrace/database` to dynamically look up a known directory structure to use. NOTE: Backtrace database will remove all existing files in the database directory upion first initialization.
@@ -168,9 +178,12 @@ The backtrace-unity library includes support for capturing Android NDK crashes a
system.memory usage related information including memfree, swapfree, and vmalloc.used is now available. Additional VM details and voluntary / nonvountary ctxt switches are included.
-## ANR
+## ANRs and Hangs
+
+When configuring the backtrace-unity client for an Android deployment, programmers will have a toggle available in backtrace-unity GUI in the Unity Editor to enable or disable ANR or Hang reports. This will use the default of 5 seconds. The `error.type` for these reports will be `Hang`.
-When configuring the backtrace-unity client for an Android deployment, programmers will have a toggle available in backtrace-unity GUI in the Unity Editor to enable or disable ANR reports. This will use the default of 5 seconds.
+## Low Memory Reports
+Backtrace can detect low memory situations for a game running in Unity on Android devices, and attempt to generate an error report with an associated dump object for further investigation. The `error.type` for these reports wiill be `Low Memory`.
## Symbols upload
@@ -181,10 +194,12 @@ To generate `symbols.zip` archive make sure:
* you checked `Create symbols.zip` in the Build settings window

-To upload symbols to Backtrace, you need to rename symbols generated by Unity end simply with a `.so` extension. By default, symbol files within the .zip will end with extension `.sym.so`. or `.dbg.so` Backtrace will only match symbols to files based on the ending with `.so` extension. Please ensure all files have a single `.so` extention before uploading the zip. To upload symbols please go to your project settings, to the `Upload an archive` tab under `Symbols` section.
-
Backtrace offers to upload symbols automatically from Unity Editor to your Backtrace instance. Backtrace symbols upload pipeline will be triggered after successfull build of il2cpp Android game and when Symbols upload token is available in Backtrace Client options. After successfull build, upload pipeline will confirm symbols upload.
+If you build outside the Unity Editor and need to manually upload symbols to Backtrace, you must rename symbols generated by Unity end simply with a `.so` extension. By default, symbol files within the .zip will end with extension `.sym.so`. or `.dbg.so` Backtrace will only match symbols to files based on the ending with `.so` extension. Please ensure all files have a single `.so` extention before uploading the zip.
+
+To learn more about how to submit those symbol files to Backtrace, please see the Project Settings / Symbols. You can manage submission tokens, upload via the UI, or configure external Symbol Servers to connect and discover required symbols. Please review additional Symbol documentaion at https://support.backtrace.io/hc/en-us/articles/360040517071-Symbolication-Overview
+
# iOS Specific information
The backtrace-unity library includes support for capturing native iOS crashes as well as iOS native memory and process information from underlying iOS layer.
@@ -192,6 +207,12 @@ The backtrace-unity library includes support for capturing native iOS crashes as
system and vm usage related information including system.memory.free, system.memory.used, system.memory.total, system.memory.active, system.memory.inactive, system.memory.wired are avaialble.
+## Hangs
+When configuring the backtrace-unity client for an iOS deployment, programmers will have a toggle available in backtrace-unity GUI in the Unity Editor to enable or disable ANR or Hang reports. This will use the default of 5 seconds. The `error.type` for these reports will be `Hang`.
+
+## Low Memory Reports
+Backtrace can detect low memory situations for a game running in Unity on iOS devices, and attempt to generate an error report with an associated dump object for further investigation. The `error.type` for these reports wiill be `Low Memory`.
+
## Native Crashes
When configuring the backtrace-unity client for an iOS deployment in the Unity Editor, programmers will have a toggle to enable or disable `Capture native crashes`. If this is enabled, the backtrace-unity client will ensure the crash report is generated, stored locally, and uploaded upon next game start. Unity crash reporter might prevent Backtrace Crash reporte from sending crashes to Backtrace. To be sure Backtrace is able to collect and send data please set "Enable CrashReport API" to false.

@@ -390,6 +411,10 @@ Sample code:
For example, to only get error reporting for hangs or crashes then only return false for Hang or UnhandledException or set the corresponding options in the user interface as shown below.

+
+## Sampling Log Errors
+`BacktraceClient` allows a configuration setting for log error sampling rate, which enables a random sampling mechanism for the errors captured via the DebugLog.error call. By default this sampling is equal to 0.01 - which means 1 of randomly sampled DebugLog Error Reports will be sent to Backtrace. This is a measure to prevent users from inadvertantly collecting hundreds of thousands of error messages from released games that they may not intend to. If you would like to send all DebugLog.error messages to Backtrace - please replace 0.01 value with 1.
+
## Flush database
When your application starts, database can send stored offline reports. If you want to do make it manually you can use `Flush` method that allows you to send report to server and then remove it from hard drive. If `Send` method fails, database will no longer store data.
diff --git a/Runtime/BacktraceClient.cs b/Runtime/BacktraceClient.cs
index 65d90086..4500aa97 100644
--- a/Runtime/BacktraceClient.cs
+++ b/Runtime/BacktraceClient.cs
@@ -20,7 +20,7 @@ public class BacktraceClient : MonoBehaviour, IBacktraceClient
{
public BacktraceConfiguration Configuration;
- public const string VERSION = "3.2.6";
+ public const string VERSION = "3.3.0";
public bool Enabled { get; private set; }
///
@@ -288,10 +288,12 @@ public static BacktraceClient Initialize(BacktraceConfiguration configuration, D
}
var backtrackGameObject = new GameObject(gameObjectName, typeof(BacktraceClient), typeof(BacktraceDatabase));
BacktraceClient backtraceClient = backtrackGameObject.GetComponent();
- BacktraceDatabase backtraceDatabase = backtrackGameObject.GetComponent();
-
- backtraceDatabase.Configuration = configuration;
backtraceClient.Configuration = configuration;
+ if (configuration.Enabled)
+ {
+ BacktraceDatabase backtraceDatabase = backtrackGameObject.GetComponent();
+ backtraceDatabase.Configuration = configuration;
+ }
backtrackGameObject.SetActive(true);
backtraceClient.Refresh();
backtraceClient.SetAttributes(attributes);
@@ -371,12 +373,15 @@ public void Refresh()
DontDestroyOnLoad(gameObject);
_instance = this;
}
- Database = GetComponent();
- if (Database != null)
+ if (Configuration.Enabled)
{
- Database.Reload();
- Database.SetApi(BacktraceApi);
- Database.SetReportWatcher(_reportLimitWatcher);
+ Database = GetComponent();
+ if (Database != null)
+ {
+ Database.Reload();
+ Database.SetApi(BacktraceApi);
+ Database.SetReportWatcher(_reportLimitWatcher);
+ }
}
_nativeClient = NativeClientFactory.GetNativeClient(Configuration, name);
@@ -400,10 +405,23 @@ private void Awake()
Refresh();
}
+ ///
+ /// Update native client internal ANR timer.
+ ///
+ private void Update()
+ {
+ _nativeClient?.UpdateClientTime(Time.time);
+ }
+
private void OnDestroy()
{
Enabled = false;
Application.logMessageReceived -= HandleUnityMessage;
+#if UNITY_ANDROID || UNITY_IOS
+ Application.lowMemory -= HandleLowMemory;
+ _nativeClient?.Disable();
+#endif
+
}
///
@@ -495,7 +513,7 @@ private void SendReport(BacktraceReport report, Action sendCall
/// Backtrace Report
/// Coroutine callback
/// IEnumerator
- private IEnumerator CollectDataAndSend(BacktraceReport report, Action sendCallback = null)
+ private IEnumerator CollectDataAndSend(BacktraceReport report, Action sendCallback)
{
var queryAttributes = new Dictionary();
var stopWatch = EnablePerformanceStatistics
@@ -520,7 +538,7 @@ private IEnumerator CollectDataAndSend(BacktraceReport report, Action
- {
- result.InnerExceptionResult = innerResult;
- });
+ HandleInnerException(report);
if (sendCallback != null)
{
@@ -606,6 +625,7 @@ record = Database.Add(data);
}));
}
+
///
/// Collect additional report information from client and convert report to backtrace data
///
@@ -629,12 +649,22 @@ private BacktraceData SetupBacktraceData(BacktraceReport report)
report.AssignSourceCodeToReport(sourceCode);
- var reportAttributes = _nativeClient == null
- ? _clientAttributes
- : _nativeClient.GetAttributes().Merge(_clientAttributes);
-
// pass copy of dictionary to prevent overriding client attributes
- return report.ToBacktraceData(new Dictionary(reportAttributes), GameObjectDepth);
+ var result = report.ToBacktraceData(null, GameObjectDepth);
+
+ // add native attributes to client report
+ if (_nativeClient != null)
+ {
+ _nativeClient.GetAttributes(result.Attributes.Attributes);
+ }
+
+ // apply client attributes
+ foreach (var attribute in _clientAttributes)
+ {
+ result.Attributes.Attributes[attribute.Key] = attribute.Value;
+ }
+
+ return result;
}
#if UNITY_ANDROID
@@ -665,8 +695,32 @@ private void CaptureUnityMessages()
if (Configuration.HandleUnhandledExceptions || Configuration.NumberOfLogs != 0)
{
Application.logMessageReceived += HandleUnityMessage;
+#if UNITY_ANDROID || UNITY_IOS
+ Application.lowMemory += HandleLowMemory;
+#endif
+ }
+ }
+
+#if UNITY_ANDROID || UNITY_IOS
+ internal void HandleLowMemory()
+ {
+ if (!Enabled)
+ {
+ Debug.LogWarning("Please enable BacktraceClient first.");
+ return;
+ }
+ const string lowMemoryMessage = "OOMException: Out of memory detected.";
+ _backtraceLogManager.Enqueue(new BacktraceUnityMessage(lowMemoryMessage, string.Empty, LogType.Error));
+
+ // try to send report about OOM from managed layer if native layer is disabled.
+ bool nativeSendResult = _nativeClient != null ? _nativeClient.OnOOM() : false;
+ if (!nativeSendResult)
+ {
+ var oom = new BacktraceUnhandledException(lowMemoryMessage, string.Empty);
+ SendUnhandledException(oom);
}
}
+#endif
///
/// Catch Unity logger data and create Backtrace reports for log type that represents exception or error
@@ -838,12 +892,12 @@ private bool ShouldSendReport(BacktraceReport report)
/// if inner exception exists, client should send report twice - one with current exception, one with inner exception
///
/// current report
- private void HandleInnerException(BacktraceReport report, Action callback)
+ private void HandleInnerException(BacktraceReport report)
{
var innerExceptionReport = report.CreateInnerReport();
if (innerExceptionReport != null && ShouldSendReport(innerExceptionReport))
{
- SendReport(innerExceptionReport, callback);
+ SendReport(innerExceptionReport);
}
}
diff --git a/Runtime/BacktraceDatabase.cs b/Runtime/BacktraceDatabase.cs
index f3fd24aa..4b6f46ec 100644
--- a/Runtime/BacktraceDatabase.cs
+++ b/Runtime/BacktraceDatabase.cs
@@ -220,6 +220,15 @@ public void SetApi(IBacktraceApi backtraceApi)
BacktraceApi = backtraceApi;
}
+ ///
+ /// Validate if BacktraceDatabase is enabled
+ ///
+ /// true if BacktraceDatabase is enabled. Otherwise false.
+ public bool Enabled()
+ {
+ return Enable;
+ }
+
///
/// Get settings
///
@@ -265,7 +274,7 @@ public BacktraceDatabaseRecord Add(BacktraceData data, bool @lock = true)
var record = BacktraceDatabaseContext.Add(data);
if (!@lock)
{
- record.Dispose();
+ record.Unlock();
}
return record;
}
@@ -399,7 +408,7 @@ private void SendData(BacktraceDatabaseRecord record)
StartCoroutine(
BacktraceApi.Send(backtraceData, record.Attachments, queryAttributes, (BacktraceResult sendResult) =>
{
- record.Dispose();
+ record.Unlock();
if (sendResult.Status != BacktraceResultStatus.ServerError && sendResult.Status != BacktraceResultStatus.NetworkError)
{
Delete(record);
@@ -509,7 +518,7 @@ protected virtual void LoadReports()
{
try
{
- Debug.Log("Removing record from Backtrace Database path");
+ Debug.Log("Removing record from Backtrace Database path - invalid record.");
record.Delete();
}
catch (Exception)
@@ -520,7 +529,7 @@ protected virtual void LoadReports()
}
BacktraceDatabaseContext.Add(record);
ValidateDatabaseSize();
- record.Dispose();
+ record.Unlock();
}
}
///
@@ -536,7 +545,7 @@ private bool ValidateDatabaseSize()
//If record count == 0 then we ignore this condition
var noMoreSpaceForReport = BacktraceDatabaseContext.Count() + 1 > DatabaseSettings.MaxRecordCount && DatabaseSettings.MaxRecordCount != 0;
if (noMoreSpaceForReport)
- {
+ {
return false;
}
diff --git a/Runtime/Common/MetricsHelper.cs b/Runtime/Common/MetricsHelper.cs
index fe4f2ff9..854c6ac2 100644
--- a/Runtime/Common/MetricsHelper.cs
+++ b/Runtime/Common/MetricsHelper.cs
@@ -1,5 +1,6 @@
using System;
using System.Diagnostics;
+using System.Globalization;
namespace Backtrace.Unity.Common
{
@@ -12,8 +13,7 @@ internal static class MetricsHelper
/// Elapsed time in μs
public static string GetMicroseconds(this Stopwatch stopwatch)
{
- var elapsedTime = ((stopwatch.ElapsedTicks * 1000000) / Stopwatch.Frequency);
- return Math.Max(1, elapsedTime).ToString();
+ return Math.Max(1, ((stopwatch.ElapsedTicks * 1000000) / Stopwatch.Frequency)).ToString(CultureInfo.InvariantCulture);
}
}
}
diff --git a/Runtime/Common/StackFrameHelper.cs b/Runtime/Common/StackFrameHelper.cs
deleted file mode 100644
index ebe8ef7d..00000000
--- a/Runtime/Common/StackFrameHelper.cs
+++ /dev/null
@@ -1,54 +0,0 @@
-using System.Reflection;
-using System.Text;
-
-namespace Backtrace.Unity.Common
-{
- ///
- /// Stack frame helper methods
- ///
- public static class StackFrameHelper
- {
- public static StringBuilder AddSyncMethodName(this StringBuilder builder, string methodName)
- {
- builder.Append(".");
- builder.Append(methodName);
- return builder;
- }
-
- public static StringBuilder AddFrameParameters(this StringBuilder builder, ParameterInfo[] parameters)
- {
- builder.Append("(");
- var firstParam = true;
- foreach (var param in parameters)
- {
- if (!firstParam)
- {
- builder.Append(", ");
- }
- else
- {
- firstParam = false;
- }
- // ReSharper disable once ConstantConditionalAccessQualifier
- // ReSharper disable once ConstantNullCoalescingCondition
- var typeName = param.ParameterType != null ? param.ParameterType.Name : "";
- builder.Append(typeName);
- builder.Append(" ");
- builder.Append(param.Name);
- }
- builder.Append(")");
- return builder;
- }
-
- internal static string GetAsyncFrameFullName(string frameFullName)
- {
- var start = frameFullName.LastIndexOf('<');
- var end = frameFullName.LastIndexOf('>');
- if (start >= 0 && end >= 0)
- {
- return frameFullName.Remove(start, 1).Substring(0, end - 1);
- }
- return frameFullName;
- }
- }
-}
diff --git a/Runtime/Common/StackFrameHelper.cs.meta b/Runtime/Common/StackFrameHelper.cs.meta
deleted file mode 100644
index e30ec9c7..00000000
--- a/Runtime/Common/StackFrameHelper.cs.meta
+++ /dev/null
@@ -1,11 +0,0 @@
-fileFormatVersion: 2
-guid: e9a61fe3149dcbd44aad3f6a0765745a
-MonoImporter:
- externalObjects: {}
- serializedVersion: 2
- defaultReferences: []
- executionOrder: 0
- icon: {instanceID: 0}
- userData:
- assetBundleName:
- assetBundleVariant:
diff --git a/Runtime/Extensions/DictionaryExtensions.cs b/Runtime/Extensions/DictionaryExtensions.cs
deleted file mode 100644
index 60eabec2..00000000
--- a/Runtime/Extensions/DictionaryExtensions.cs
+++ /dev/null
@@ -1,45 +0,0 @@
-using System.Collections.Generic;
-
-namespace Backtrace.Unity.Common
-{
- ///
- /// Extensions method to dictionary data structure
- ///
- internal static class DictionaryExtensions
- {
- ///
- /// Merge two dictionaries
- /// If there is any key conflict value from source dictionary is taken
- ///
- /// Source dictionary (dictionary from report)
- /// merged dictionary (
- /// Merged dictionary
- internal static Dictionary Merge(
- this Dictionary source, Dictionary toMerge)
- {
- if(source == null && toMerge == null)
- {
- return new Dictionary();
- }
-
- if (source == null)
- {
- source = new Dictionary();
- }
- if (toMerge == null)
- {
- toMerge = new Dictionary();
- }
- var result = new Dictionary(source);
- foreach (var record in toMerge)
- {
- if (!result.ContainsKey(record.Key))
- {
- result.Add(record.Key, record.Value);
- }
- }
-
- return result;
- }
- }
-}
diff --git a/Runtime/Extensions/DictionaryExtensions.cs.meta b/Runtime/Extensions/DictionaryExtensions.cs.meta
deleted file mode 100644
index 1e884697..00000000
--- a/Runtime/Extensions/DictionaryExtensions.cs.meta
+++ /dev/null
@@ -1,11 +0,0 @@
-fileFormatVersion: 2
-guid: bbf925979e4a2e240ad18b11d9a35c92
-MonoImporter:
- externalObjects: {}
- serializedVersion: 2
- defaultReferences: []
- executionOrder: 0
- icon: {instanceID: 0}
- userData:
- assetBundleName:
- assetBundleVariant:
diff --git a/Runtime/Interfaces/Database.meta b/Runtime/Interfaces/Database.meta
deleted file mode 100644
index 74d3da0b..00000000
--- a/Runtime/Interfaces/Database.meta
+++ /dev/null
@@ -1,8 +0,0 @@
-fileFormatVersion: 2
-guid: b7c79bfbd87d7c14893704876c35e3e8
-folderAsset: yes
-DefaultImporter:
- externalObjects: {}
- userData:
- assetBundleName:
- assetBundleVariant:
diff --git a/Runtime/Interfaces/Database/IBacktraceDatabaseRecordWriter.cs b/Runtime/Interfaces/Database/IBacktraceDatabaseRecordWriter.cs
deleted file mode 100644
index eee7775e..00000000
--- a/Runtime/Interfaces/Database/IBacktraceDatabaseRecordWriter.cs
+++ /dev/null
@@ -1,10 +0,0 @@
-namespace Backtrace.Unity.Interfaces.Database
-{
- internal interface IBacktraceDatabaseRecordWriter
- {
- string Write(string data, string prefix);
- string Write(byte[] data, string prefix);
- void SaveTemporaryFile(string path, byte[] file);
- void SaveValidRecord(string sourcePath, string destinationPath);
- }
-}
diff --git a/Runtime/Interfaces/Database/IBacktraceDatabaseRecordWriter.cs.meta b/Runtime/Interfaces/Database/IBacktraceDatabaseRecordWriter.cs.meta
deleted file mode 100644
index b6fd122a..00000000
--- a/Runtime/Interfaces/Database/IBacktraceDatabaseRecordWriter.cs.meta
+++ /dev/null
@@ -1,11 +0,0 @@
-fileFormatVersion: 2
-guid: b4091ec257bee1c42bbe5f185a41306f
-MonoImporter:
- externalObjects: {}
- serializedVersion: 2
- defaultReferences: []
- executionOrder: 0
- icon: {instanceID: 0}
- userData:
- assetBundleName:
- assetBundleVariant:
diff --git a/Runtime/Interfaces/IBacktraceDatabase.cs b/Runtime/Interfaces/IBacktraceDatabase.cs
index 69f98f8f..905fe48b 100644
--- a/Runtime/Interfaces/IBacktraceDatabase.cs
+++ b/Runtime/Interfaces/IBacktraceDatabase.cs
@@ -83,5 +83,11 @@ public interface IBacktraceDatabase
/// Lock report - default true
/// Backtrace record
BacktraceDatabaseRecord Add(BacktraceData data, bool @lock = true);
+
+ ///
+ /// Validate if BacktraceDatabase is enabled
+ ///
+ /// true if BacktraceDatabase is enabled. Otherwise false.
+ bool Enabled();
}
}
diff --git a/Runtime/Json/BacktraceJObject.cs b/Runtime/Json/BacktraceJObject.cs
index 712d3161..b07ed8d7 100644
--- a/Runtime/Json/BacktraceJObject.cs
+++ b/Runtime/Json/BacktraceJObject.cs
@@ -2,7 +2,6 @@
using System.Collections;
using System.Collections.Generic;
using System.Globalization;
-using System.Linq;
using System.Text;
namespace Backtrace.Unity.Json
@@ -13,32 +12,123 @@ namespace Backtrace.Unity.Json
public class BacktraceJObject
{
///
- /// JSON object source
+ /// JSON object source - primitive values
///
- public readonly Dictionary Source = new Dictionary();
+ internal readonly Dictionary PrimitiveValues = new Dictionary();
+
+ ///
+ /// JSON object source - primitive values
+ ///
+ internal readonly Dictionary UserPrimitives;
+
+ ///
+ /// Inner objects
+ ///
+ internal readonly Dictionary InnerObjects = new Dictionary();
+
+ ///
+ /// Complex objects - array of JObjects/strings
+ ///
+ internal readonly Dictionary ComplexObjects = new Dictionary();
+
public BacktraceJObject() : this(null) { }
public BacktraceJObject(Dictionary source)
{
- if (source == null)
+ UserPrimitives = source == null ? new Dictionary() : source;
+ }
+
+
+ ///
+ /// Add boolean key-value pair to JSON object
+ ///
+ /// JSON key
+ /// value
+ public void Add(string key, bool value)
+ {
+ PrimitiveValues.Add(key, value.ToString().ToLower());
+ }
+
+
+
+ ///
+ /// Add key-value pair to JSON object
+ ///
+ /// JSON key
+ /// value
+ public void Add(string key, float value)
+ {
+ PrimitiveValues.Add(key, value.ToString("G", CultureInfo.InvariantCulture));
+ }
+
+ ///
+ /// Add key-value pair to JSON object
+ ///
+ /// JSON key
+ /// value
+ public void Add(string key, double value)
+ {
+ PrimitiveValues.Add(key, value.ToString("G", CultureInfo.InvariantCulture));
+ }
+
+ ///
+ /// Add string key-value pair to JSON object
+ ///
+ /// JSON key
+ /// value
+ public void Add(string key, string value)
+ {
+ if (string.IsNullOrEmpty(value))
{
+ ComplexObjects.Add(key, null);
return;
}
- Source = source.ToDictionary(n => n.Key, m => m.Value as object);
+
+ StringBuilder builder = new StringBuilder();
+ builder.Append("\"");
+ EscapeString(value, builder);
+ builder.Append("\"");
+
+ PrimitiveValues.Add(key, builder.ToString());
+ }
+
+ ///
+ /// Add long key-value pair to JSON object
+ ///
+ /// JSON key
+ /// value
+ public void Add(string key, long value)
+ {
+ PrimitiveValues.Add(key, value.ToString());
}
- public object this[string key]
+ ///
+ /// Add BacktraceJObject key-value pair to JSON object
+ ///
+ /// JSON key
+ /// value
+ public void Add(string key, BacktraceJObject value)
{
- get
+ if (value != null)
{
- return Source[key];
+ InnerObjects.Add(key, value);
}
- set
+ else
{
- Source[key] = value;
+ ComplexObjects.Add(key, null);
+
}
}
+ ///
+ /// Add ienumerable object key-value pair to JSON object
+ ///
+ /// JSON key
+ /// value
+ public void Add(string key, IEnumerable value)
+ {
+ ComplexObjects.Add(key, value);
+ }
///
/// Convert BacktraceJObject to JSON
@@ -46,35 +136,201 @@ public object this[string key]
/// BacktraceJObject JSON representation
public string ToJson()
{
- var stringBuilder = new StringBuilder("{");
-
- var lines = Source.Select(entry => string.Format("\"{0}\": {1}", EscapeString(entry.Key), ConvertValue(entry.Value)));
- var content = string.Join(",", lines);
+ var stringBuilder = new StringBuilder();
+ ToJson(stringBuilder);
+ return stringBuilder.ToString();
+ }
- stringBuilder.Append(content);
+ internal void ToJson(StringBuilder stringBuilder)
+ {
+ stringBuilder.Append("{");
+ AppendPrimitives(stringBuilder);
+ AddUserPrimitives(stringBuilder);
+ AppendJObjects(stringBuilder);
+ AppendComplexValues(stringBuilder);
stringBuilder.Append("}");
+ }
- return stringBuilder.ToString();
+ private void AddUserPrimitives(StringBuilder stringBuilder)
+ {
+ if (UserPrimitives.Count == 0)
+ {
+ return;
+ }
+ int propertyIndex = 0;
+ if (ShouldContinueAddingJSONProperties(stringBuilder))
+ {
+ stringBuilder.Append(',');
+ }
+ using (var enumerator = UserPrimitives.GetEnumerator())
+ {
+ while (enumerator.MoveNext())
+ {
+ propertyIndex++;
+ var entry = enumerator.Current;
+ AppendKey(entry.Key, stringBuilder);
+ if (string.IsNullOrEmpty(entry.Value))
+ {
+ stringBuilder.Append("null");
+ }
+ else
+ {
+ stringBuilder.Append("\"");
+ EscapeString(entry.Value, stringBuilder);
+ stringBuilder.Append("\"");
+ }
+ if (propertyIndex != UserPrimitives.Count)
+ {
+ stringBuilder.Append(",");
+ }
+ }
+ }
+ }
+
+ private void AppendPrimitives(StringBuilder stringBuilder)
+ {
+ int propertyIndex = 0;
+ using (var enumerator = PrimitiveValues.GetEnumerator())
+ {
+ while (enumerator.MoveNext())
+ {
+
+ propertyIndex++;
+ var entry = enumerator.Current;
+ AppendKey(entry.Key, stringBuilder);
+ stringBuilder.Append(string.IsNullOrEmpty(entry.Value) ? "null" : entry.Value);
+
+ if (propertyIndex != PrimitiveValues.Count)
+ {
+ stringBuilder.Append(",");
+ }
+ }
+ }
}
+ private void AppendJObjects(StringBuilder stringBuilder)
+ {
+ if (InnerObjects.Count == 0)
+ {
+ return;
+ }
+ int propertyIndex = 0;
+ using (var enumerator = InnerObjects.GetEnumerator())
+ {
+ if (ShouldContinueAddingJSONProperties(stringBuilder))
+ {
+ stringBuilder.Append(',');
+ }
+ while (enumerator.MoveNext())
+ {
+ propertyIndex++;
+ var entry = enumerator.Current;
+ AppendKey(entry.Key, stringBuilder);
+ entry.Value.ToJson(stringBuilder);
+ if (propertyIndex != InnerObjects.Count)
+ {
+ stringBuilder.Append(",");
+ }
+ }
+ }
+ }
+
+ private void AppendComplexValues(StringBuilder stringBuilder)
+ {
+ if (ComplexObjects.Count == 0)
+ {
+ return;
+ }
+ int propertyIndex = 0;
+ using (var enumerator = ComplexObjects.GetEnumerator())
+ {
+ if (ShouldContinueAddingJSONProperties(stringBuilder))
+ {
+ stringBuilder.Append(',');
+ }
+ while (enumerator.MoveNext())
+ {
+ propertyIndex++;
+ var entry = enumerator.Current;
+ AppendKey(entry.Key, stringBuilder);
+ if (entry.Value == null)
+ {
+ stringBuilder.Append("null");
+ }
+ else if (entry.Value is IEnumerable && !(entry.Value is IDictionary))
+ {
+ stringBuilder.Append('[');
+ int index = 0;
+ foreach (var item in (entry.Value as IEnumerable))
+ {
+ if (index != 0)
+ {
+ stringBuilder.Append(',');
+ }
+ if (item == null)
+ {
+ stringBuilder.Append("null");
+ }
+ else if (item is BacktraceJObject)
+ {
+ (item as BacktraceJObject).ToJson(stringBuilder);
+ }
+ else
+ {
+ stringBuilder.Append("\"");
+ EscapeString(item.ToString(), stringBuilder);
+ stringBuilder.Append("\"");
+ }
+ index++;
+ }
+ stringBuilder.Append(']');
+ }
+
+ if (propertyIndex != ComplexObjects.Count)
+ {
+ stringBuilder.Append(",");
+ }
+ }
+ }
+ }
+
+ private bool ShouldContinueAddingJSONProperties(StringBuilder stringBuilder)
+ {
+ return stringBuilder[stringBuilder.Length - 1] != ',' && stringBuilder[stringBuilder.Length - 1] != '{';
+ }
+
+
+ private void AppendKey(string value, StringBuilder builder)
+ {
+ builder.Append("\"");
+ if (string.IsNullOrEmpty(value))
+ {
+ builder.Append("null");
+ }
+ else
+ {
+ EscapeString(value, builder);
+ }
+ builder.Append("\":");
+ }
+
+
///
/// Escape special characters in string
///
/// string to escape
/// escaped string
- private string EscapeString(string value)
+ private void EscapeString(string value, StringBuilder output)
{
- var output = new StringBuilder(value.Length);
foreach (char c in value)
{
switch (c)
{
case '\\':
- output.AppendFormat("{0}{0}", '\\');
+ output.AppendFormat("\\\\");
break;
-
case '"':
- output.AppendFormat("{0}{1}", '\\', '"');
+ output.AppendFormat("\\\"");
break;
case '\b':
output.Append("\\b");
@@ -96,82 +352,6 @@ private string EscapeString(string value)
break;
}
}
-
- return output.ToString();
- }
-
-
- ///
- /// Convert object to json value
- ///
- /// object value
- /// json value
- private string ConvertValue(object value)
- {
- if (value == null)
- {
- return "null";
- }
-
- var analysedType = value.GetType();
- if (analysedType == typeof(string))
- {
- return string.Format("\"{0}\"", EscapeString(value as string));
- }
- else if (analysedType == typeof(double))
- {
- return Convert.ToDouble(value, CultureInfo.CurrentCulture).ToString(CultureInfo.InvariantCulture);
- }
- else if (analysedType == typeof(float))
- {
- return Convert.ToDouble(value, CultureInfo.CurrentCulture).ToString(CultureInfo.InvariantCulture);
- }
- else if (analysedType == typeof(int))
- {
- return Convert.ToInt32(value, CultureInfo.CurrentCulture).ToString();
- }
- else if (analysedType == typeof(long))
- {
- return Convert.ToInt64(value, CultureInfo.CurrentCulture).ToString();
- }
- else if (analysedType == typeof(bool))
- {
- return ((bool)value).ToString().ToLower();
- }
- else if (value is IEnumerable && !(value is IDictionary))
- {
- var collection = (value as IEnumerable);
- var builder = new StringBuilder();
- builder.Append('[');
- int index = 0;
- foreach (var item in collection)
- {
- if (index != 0)
- {
- builder.Append(',');
- }
- builder.Append(ConvertValue(item));
- index++;
- }
- builder.Append(']');
- return builder.ToString();
- }
- else if (Guid.TryParse(value.ToString(), out Guid guidResult))
- {
- return string.Format("\"{0}\"", guidResult.ToString());
- }
- else
- {
- //check if this is json inner object
- var backtraceJObjectValue = value as BacktraceJObject;
- if (backtraceJObjectValue != null)
- {
- return backtraceJObjectValue.ToJson();
- }
-
- return "null";
- }
-
}
}
}
diff --git a/Runtime/Model/BacktraceConfiguration.cs b/Runtime/Model/BacktraceConfiguration.cs
index b63f3299..1e076c4f 100644
--- a/Runtime/Model/BacktraceConfiguration.cs
+++ b/Runtime/Model/BacktraceConfiguration.cs
@@ -97,7 +97,7 @@ public class BacktraceConfiguration : ScriptableObject
///
[Tooltip("Capture native NDK Crashes (ANDROID API 21+)")]
#elif UNITY_IOS
- ///
+ ///
/// Capture native iOS Crashes.
///
[Tooltip("Capture native Crashes")]
@@ -106,7 +106,7 @@ public class BacktraceConfiguration : ScriptableObject
public bool CaptureNativeCrashes = true;
#endif
-#if UNITY_ANDROID
+#if UNITY_ANDROID || UNITY_IOS
///
/// Handle ANR events - Application not responding
///
diff --git a/Runtime/Model/BacktraceData.cs b/Runtime/Model/BacktraceData.cs
index 3a1f65c2..a003aa1e 100644
--- a/Runtime/Model/BacktraceData.cs
+++ b/Runtime/Model/BacktraceData.cs
@@ -17,6 +17,26 @@ public class BacktraceData
///
public Guid Uuid { get; private set; }
+ ///
+ /// String representation of Uuid Guid - for optimization purposes.
+ ///
+ private string _uuidString;
+
+ ///
+ /// internal Uuid in string format
+ ///
+ internal string UuidString
+ {
+ get
+ {
+ if (string.IsNullOrEmpty(_uuidString))
+ {
+ _uuidString = Uuid.ToString();
+ }
+ return _uuidString;
+ }
+ }
+
///
/// UTC timestamp in seconds
///
@@ -117,21 +137,25 @@ public BacktraceData(BacktraceReport report, Dictionary clientAt
/// Backtrace Data JSON string
public string ToJson()
{
- var jObject = new BacktraceJObject()
+ var jObject = new BacktraceJObject(new Dictionary()
{
- ["uuid"] = Uuid,
- ["timestamp"] = Timestamp,
+ ["uuid"] = UuidString,
["lang"] = Lang,
["langVersion"] = LangVersion,
["agent"] = Agent,
["agentVersion"] = AgentVersion,
["mainThread"] = MainThread,
- ["classifiers"] = Classifier,
- ["attributes"] = Attributes.ToJson(),
- ["annotations"] = Annotation.ToJson(),
- ["threads"] = ThreadData == null ? null : ThreadData.ToJson(),
- ["sourceCode"] = SourceCode == null ? null : SourceCode.ToJson()
- };
+ });
+ jObject.Add("timestamp", Timestamp);
+ jObject.Add("classifiers", Classifier);
+
+ jObject.Add("attributes", Attributes.ToJson());
+ jObject.Add("annotations", Annotation.ToJson());
+ jObject.Add("threads", ThreadData.ToJson());
+ if (SourceCode != null)
+ {
+ jObject.Add("sourceCode", SourceCode.ToJson());
+ }
return jObject.ToJson();
}
diff --git a/Runtime/Model/BacktraceLogManager.cs b/Runtime/Model/BacktraceLogManager.cs
index 14c025cf..922dce91 100644
--- a/Runtime/Model/BacktraceLogManager.cs
+++ b/Runtime/Model/BacktraceLogManager.cs
@@ -12,7 +12,7 @@ internal class BacktraceLogManager
///
/// Unity message queue
///
- internal readonly ConcurrentQueue LogQueue;
+ internal readonly ConcurrentQueue LogQueue;
///
/// Lock object
@@ -27,7 +27,7 @@ internal class BacktraceLogManager
public BacktraceLogManager(uint numberOfLogs)
{
_limit = numberOfLogs;
- LogQueue = new ConcurrentQueue();
+ LogQueue = new ConcurrentQueue();
}
///
@@ -85,10 +85,10 @@ public bool Enqueue(BacktraceUnityMessage unityMessage)
return false;
}
- LogQueue.Enqueue(unityMessage);
+ LogQueue.Enqueue(unityMessage.ToString());
lock (lockObject)
{
- while (LogQueue.Count > _limit && LogQueue.TryDequeue(out BacktraceUnityMessage _));
+ while (LogQueue.Count > _limit && LogQueue.TryDequeue(out string _)) ;
}
return true;
}
@@ -102,7 +102,7 @@ public string ToSourceCode()
var stringBuilder = new StringBuilder();
foreach (var item in LogQueue)
{
- stringBuilder.AppendLine(item.ToString());
+ stringBuilder.AppendLine(item);
}
return stringBuilder.ToString();
}
diff --git a/Runtime/Model/BacktraceReport.cs b/Runtime/Model/BacktraceReport.cs
index 6e9aff58..0b286bb6 100644
--- a/Runtime/Model/BacktraceReport.cs
+++ b/Runtime/Model/BacktraceReport.cs
@@ -119,7 +119,7 @@ public BacktraceReport(
///
internal void AssignSourceCodeToReport(string text)
{
- if (DiagnosticStack == null || !DiagnosticStack.Any())
+ if (DiagnosticStack == null || DiagnosticStack.Count == 0)
{
return;
}
@@ -129,7 +129,7 @@ internal void AssignSourceCodeToReport(string text)
Text = text
};
// assign log information to first stack frame
- DiagnosticStack.First().SourceCode = SourceCode.Id.ToString();
+ DiagnosticStack[0].SourceCode = BacktraceSourceCode.SOURCE_CODE_PROPERTY;
}
///
@@ -153,7 +153,7 @@ private void SetClassifier()
///
internal void SetReportFingerPrintForEmptyStackTrace()
{
- if ((Exception != null && string.IsNullOrEmpty(Exception.StackTrace)) || DiagnosticStack == null || !DiagnosticStack.Any() )
+ if ((Exception != null && string.IsNullOrEmpty(Exception.StackTrace)) || DiagnosticStack == null || DiagnosticStack.Count == 0)
{
// set attributes instead of fingerprint to still allow our user to define customer
// fingerprints for reports without stack trace and apply deduplication rules in report flow.
@@ -166,18 +166,6 @@ internal BacktraceData ToBacktraceData(Dictionary clientAttribut
return new BacktraceData(this, clientAttributes, gameObjectDepth);
}
- ///
- /// Concat two attributes dictionary
- ///
- /// Current report
- /// Attributes to concatenate
- ///
- internal static Dictionary ConcatAttributes(
- BacktraceReport report, Dictionary attributes)
- {
- var reportAttributes = report.Attributes;
- return reportAttributes.Merge(attributes);
- }
internal void SetStacktraceInformation()
{
diff --git a/Runtime/Model/BacktraceSourceCode.cs b/Runtime/Model/BacktraceSourceCode.cs
index 81d91571..e9faa4be 100644
--- a/Runtime/Model/BacktraceSourceCode.cs
+++ b/Runtime/Model/BacktraceSourceCode.cs
@@ -9,10 +9,7 @@ namespace Backtrace.Unity.Model
///
public class BacktraceSourceCode
{
- ///
- /// Source code id - integration uses id to assign source code to first stack frame
- ///
- public readonly string Id = Guid.NewGuid().ToString();
+ internal static string SOURCE_CODE_PROPERTY = "main";
///
/// Default source code type
///
@@ -22,10 +19,6 @@ public class BacktraceSourceCode
///
public readonly string Title = "Log File";
- ///
- /// Required source code option - we don't want to hightlight any line
- ///
- public readonly bool HighlightLine = false;
///
/// Unity engine text
@@ -39,14 +32,15 @@ public class BacktraceSourceCode
internal BacktraceJObject ToJson()
{
var json = new BacktraceJObject();
- var sourceCode = new BacktraceJObject();
- sourceCode["id"] = Id;
- sourceCode["type"] = Type;
- sourceCode["title"] = Title;
- sourceCode["highlightLine"] = HighlightLine;
- sourceCode["text"] = Text;
-
- json[Id.ToString()] = sourceCode;
+ var sourceCode = new BacktraceJObject(new System.Collections.Generic.Dictionary()
+ {
+ { "id",SOURCE_CODE_PROPERTY },
+ { "type", Type },
+ { "title", Title },
+ { "text", Text }
+ });
+ sourceCode.Add("highlightLine", false);
+ json.Add(SOURCE_CODE_PROPERTY, sourceCode);
return json;
}
}
diff --git a/Runtime/Model/BacktraceStackFrame.cs b/Runtime/Model/BacktraceStackFrame.cs
index ebb2bf3e..1d6a2afd 100644
--- a/Runtime/Model/BacktraceStackFrame.cs
+++ b/Runtime/Model/BacktraceStackFrame.cs
@@ -1,31 +1,50 @@
using Backtrace.Unity.Json;
+using Backtrace.Unity.Types;
using System;
+using System.Collections.Generic;
using System.Diagnostics;
+using System.IO;
using System.Reflection;
namespace Backtrace.Unity.Model
{
public class BacktraceStackFrame
{
+
+ private static string[] _frameSeparators = new string[] { "::", ":", "." };
+
///
/// Function where exception occurs
///
public string FunctionName;
+ internal BacktraceStackFrameType StackFrameType = BacktraceStackFrameType.Unknown;
+
///
- /// Line number in source code where exception occurs
+ /// Function file name
///
- public int Line;
+ public string FileName
+ {
+ get
+ {
+ return string.IsNullOrEmpty(Library)
+ ? GetFileNameFromFunctionName()
+ : Library.IndexOfAny(Path.GetInvalidPathChars()) == -1 && Path.HasExtension(Path.GetFileName(Library))
+ ? GetFileNameFromLibraryName()
+ : GetFileNameFromFunctionName();
+ }
+ }
+
///
- /// IL Offset
+ /// Line number in source code where exception occurs
///
- public int? Il;
+ public int Line;
///
/// PBD Unique identifier
///
- public int? MemberInfo;
+ public string MemberInfo;
///
@@ -41,7 +60,7 @@ public class BacktraceStackFrame
///
/// Address of the stack frame
///
- public int? ILOffset;
+ public int ILOffset;
///
/// Source code file name where exception occurs
@@ -64,38 +83,33 @@ public class BacktraceStackFrame
public bool InvalidFrame { get; set; }
public BacktraceJObject ToJson()
{
- var stackFrame = new BacktraceJObject
+ var stackFrame = new BacktraceJObject(new Dictionary()
{
["funcName"] = FunctionName,
- ["il"] = Il,
+ ["path"] = FileName,
["metadata_token"] = MemberInfo,
- ["address"] = ILOffset,
["assembly"] = Assembly
- };
-
+ });
+
+ stackFrame.Add("address", ILOffset);
if (!string.IsNullOrEmpty(Library) && !(Library.StartsWith("<") && Library.EndsWith(">")))
{
- stackFrame["library"] = Library;
+ stackFrame.Add("library", Library);
}
if (Line != 0)
{
- stackFrame["line"] = Line;
+ stackFrame.Add("line", Line);
}
if (Column != 0)
{
- stackFrame["column"] = Column;
- }
-
- if (!string.IsNullOrEmpty(Address))
- {
- stackFrame["address"] = Address;
+ stackFrame.Add("column", Column);
}
if (!string.IsNullOrEmpty(SourceCode))
{
- stackFrame["sourceCode"] = SourceCode;
+ stackFrame.Add("sourceCode", SourceCode);
}
return stackFrame;
@@ -141,24 +155,17 @@ public BacktraceStackFrame(StackFrame frame, bool generatedByException)
}
}
-
- SourceCodeFullPath = frame.GetFileName();
-
FunctionName = GetMethodName(method);
+ SourceCodeFullPath = frame.GetFileName();
Line = frame.GetFileLineNumber();
- Il = frame.GetILOffset();
- ILOffset = Il;
+ ILOffset = frame.GetILOffset();
Assembly = assembly;
Library = string.IsNullOrEmpty(SourceCodeFullPath) ? method.DeclaringType.ToString() : SourceCodeFullPath;
- SourceCode = generatedByException
- ? Guid.NewGuid().ToString()
- : string.Empty;
-
Column = frame.GetFileColumnNumber();
try
{
- MemberInfo = method.MetadataToken;
+ MemberInfo = method.MetadataToken.ToString();
}
catch (InvalidOperationException)
{
@@ -176,13 +183,101 @@ public BacktraceStackFrame(StackFrame frame, bool generatedByException)
private string GetMethodName(MethodBase method)
{
var methodName = method.Name.StartsWith(".") ? method.Name.Substring(1, method.Name.Length - 1) : method.Name;
- string fullMethodName = string.Format("{0}.{1}()", method.DeclaringType == null ? null : method.DeclaringType.ToString(), methodName);
- return fullMethodName;
+ return string.Format("{0}.{1}()", method.DeclaringType == null ? null : method.DeclaringType.ToString(), methodName);
}
-
+
+ private string GetFileNameFromLibraryName()
+ {
+ var libraryName = Path.GetFileName(Library).Trim();
+
+ // detect namespace
+ var lastSeparatorIndex = libraryName.LastIndexOf(".");
+ if (lastSeparatorIndex == -1 || libraryName.IndexOf(".") == lastSeparatorIndex)
+ {
+ // detected full path to source code
+ return libraryName;
+ }
+
+ // omit '.' character that substring will return based on lastSeparatorIndex
+ libraryName = libraryName.Substring(lastSeparatorIndex + 1);
+ switch (StackFrameType)
+ {
+ case BacktraceStackFrameType.Dotnet:
+ return string.Format("{0}.cs", libraryName);
+ case BacktraceStackFrameType.Android:
+ return string.Format("{0}.java", libraryName);
+ default:
+ return libraryName;
+ }
+ }
+
+ ///
+ /// Generate file name based on full functiom name
+ ///
+ /// File name
+ private string GetFileNameFromFunctionName()
+ {
+ if (string.IsNullOrEmpty(FunctionName))
+ {
+ return string.Empty;
+ }
+ // use Function name instead and try to get last part of function
+ var lastSearchIndex = FunctionName.IndexOf('(');
+ if (lastSearchIndex == -1)
+ {
+ lastSearchIndex = FunctionName.Length - 1;
+ }
+
+ int separatorIndex = -1;
+ for (int arrayIndex = 0; arrayIndex < _frameSeparators.Length; arrayIndex++)
+ {
+ separatorIndex = FunctionName.LastIndexOf(_frameSeparators[arrayIndex], lastSearchIndex);
+ if (separatorIndex != -1)
+ {
+ break;
+ }
+ }
+
+ if (separatorIndex == -1)
+ {
+ return string.Empty;
+ }
+
+ var libraryPath = FunctionName.Substring(0, separatorIndex).Split(new char[] { '.' });
+ // handle situation when function name is a constructor path or specific module path
+ var currentIndex = libraryPath.Length - 1;
+ string fileName = libraryPath[currentIndex];
+
+ while (string.IsNullOrEmpty(fileName) && currentIndex > 0)
+ {
+ fileName = libraryPath[currentIndex - 1];
+ currentIndex--;
+
+ }
+ if (string.IsNullOrEmpty(fileName))
+ {
+ return Library;
+ }
+
+ if (fileName.IndexOfAny(Path.GetInvalidPathChars()) == -1 && Path.HasExtension(fileName) || StackFrameType == BacktraceStackFrameType.Unknown)
+ {
+ return fileName;
+ }
+
+ switch (StackFrameType)
+ {
+ case BacktraceStackFrameType.Dotnet:
+ return string.Format("{0}.cs", fileName);
+ case BacktraceStackFrameType.Android:
+ return string.Format("{0}.java", fileName);
+ default:
+ return fileName;
+ }
+ }
+
public override string ToString()
{
- return string.Format("{0} (at {1}:{2})", FunctionName, Library, Line);
+ return string.Format("{0} (at {1}:{2})", FunctionName, Library, Line.ToString());
}
}
}
diff --git a/Runtime/Model/BacktraceStackTrace.cs b/Runtime/Model/BacktraceStackTrace.cs
index b48db3d0..c91df20c 100644
--- a/Runtime/Model/BacktraceStackTrace.cs
+++ b/Runtime/Model/BacktraceStackTrace.cs
@@ -71,6 +71,7 @@ private void SetStacktraceInformation(StackFrame[] frames, bool generatedByExcep
{
continue;
}
+ backtraceFrame.StackFrameType = Types.BacktraceStackFrameType.Dotnet;
StackFrames.Insert(startingIndex, backtraceFrame);
startingIndex++;
}
diff --git a/Runtime/Model/BacktraceUnhandledException.cs b/Runtime/Model/BacktraceUnhandledException.cs
index 5108c2e5..6ba69362 100644
--- a/Runtime/Model/BacktraceUnhandledException.cs
+++ b/Runtime/Model/BacktraceUnhandledException.cs
@@ -54,7 +54,7 @@ public BacktraceUnhandledException(string message, string stacktrace) : base(mes
ConvertStackFrames();
}
- if (string.IsNullOrEmpty(stacktrace) || !StackFrames.Any())
+ if (string.IsNullOrEmpty(stacktrace) || StackFrames.Count == 0)
{
// make sure that for this kind of exception, this exception message will be always the same
// error message might be overriden by ConvertStackFrames method.
@@ -153,6 +153,7 @@ private BacktraceStackFrame SetJITStackTraceInformation(string frameString)
{
var stackFrame = new BacktraceStackFrame();
+ stackFrame.StackFrameType = Types.BacktraceStackFrameType.Native;
if (!frameString.StartsWith("#"))
{
//handle sitaution when we detected jit stack trace
@@ -202,6 +203,7 @@ private BacktraceStackFrame SetJITStackTraceInformation(string frameString)
private BacktraceStackFrame SetNativeStackTraceInformation(string frameString)
{
var stackFrame = new BacktraceStackFrame();
+ stackFrame.StackFrameType = Types.BacktraceStackFrameType.Native;
// parse address
var addressSubstringIndex = frameString.IndexOf(' ');
stackFrame.Address = frameString.Substring(0, addressSubstringIndex);
@@ -238,7 +240,7 @@ private BacktraceStackFrame SetNativeStackTraceInformation(string frameString)
sourceCodeStartIndex,
sourceCodeEndIndex - sourceCodeStartIndex);
- var sourceCodeParts = sourceCodeInformation.Split(':');
+ var sourceCodeParts = sourceCodeInformation.Split(new char[] { ':' }, 2);
if (sourceCodeParts.Length == 2)
{
int.TryParse(sourceCodeParts[1], out stackFrame.Line);
@@ -263,6 +265,7 @@ private BacktraceStackFrame SetAndroidStackTraceInformation(string frameString)
var parameterEnd = frameString.LastIndexOf(')');
var stackFrame = new BacktraceStackFrame();
+ stackFrame.StackFrameType = Types.BacktraceStackFrameType.Android;
if (parameterStart != -1 && parameterEnd != -1 && parameterEnd - parameterStart > 1)
{
stackFrame.FunctionName = frameString.Substring(0, parameterStart - 1);
@@ -307,7 +310,8 @@ private BacktraceStackFrame SetDefaultStackTraceInformation(string frameString)
{
return new BacktraceStackFrame()
{
- FunctionName = frameString
+ FunctionName = frameString,
+ StackFrameType = Types.BacktraceStackFrameType.Dotnet
};
}
@@ -321,7 +325,8 @@ private BacktraceStackFrame SetDefaultStackTraceInformation(string frameString)
var result = new BacktraceStackFrame()
{
- FunctionName = frameString.Substring(0, methodNameEndIndex + 1).Trim()
+ FunctionName = frameString.Substring(0, methodNameEndIndex + 1).Trim(),
+ StackFrameType = Types.BacktraceStackFrameType.Dotnet
};
if (endLineNumberSeparator > 0 && lineNumberSeparator > 0)
diff --git a/Runtime/Model/BacktraceUnityMessage.cs b/Runtime/Model/BacktraceUnityMessage.cs
index 7a352d61..f9802d3f 100644
--- a/Runtime/Model/BacktraceUnityMessage.cs
+++ b/Runtime/Model/BacktraceUnityMessage.cs
@@ -9,11 +9,10 @@ namespace Backtrace.Unity.Model
///
internal class BacktraceUnityMessage
{
- public readonly DateTime Date = DateTime.Now;
- private readonly bool _backtraceFrame = false;
- public string Message;
- public string StackTrace;
- public LogType Type;
+ private readonly string _formattedMessage;
+ public readonly string Message;
+ public readonly string StackTrace;
+ public readonly LogType Type;
public BacktraceUnityMessage(BacktraceReport report)
{
@@ -21,25 +20,54 @@ public BacktraceUnityMessage(BacktraceReport report)
{
throw new ArgumentException("report");
}
- _backtraceFrame = true;
Message = report.Message;
if (report.ExceptionTypeReport)
{
Type = LogType.Exception;
- StackTrace = report.Exception.StackTrace;
+ StackTrace = GetFormattedStackTrace(report.Exception.StackTrace);
+ _formattedMessage = GetFormattedMessage(true);
}
else
{
StackTrace = string.Empty;
Type = LogType.Warning;
+ _formattedMessage = GetFormattedMessage(true);
}
}
public BacktraceUnityMessage(string message, string stacktrace, LogType type)
{
Message = message;
- StackTrace = stacktrace;
+ StackTrace = GetFormattedStackTrace(stacktrace);
Type = type;
+ _formattedMessage = GetFormattedMessage(false);
+ }
+
+ private string GetFormattedMessage(bool backtraceFrame)
+ {
+ var stringBuilder = new StringBuilder();
+ stringBuilder.AppendFormat(
+ "[{0}] {1}<{2}>: {3}", new object[4] {
+ DateTime.Now.ToUniversalTime().ToString(),
+ backtraceFrame ? "(Backtrace)" : string.Empty,
+ Enum.GetName(typeof(LogType), Type),
+ Message}
+ );
+
+ // include stack trace if log was generated by exception/error
+ if (IsUnhandledException())
+ {
+ stringBuilder.AppendLine();
+ stringBuilder.Append(string.IsNullOrEmpty(StackTrace) ? "No stack trace available" : StackTrace);
+ }
+ return stringBuilder.ToString();
+ }
+
+ private string GetFormattedStackTrace(string stacktrace)
+ {
+ return !string.IsNullOrEmpty(stacktrace) && stacktrace.EndsWith("\n")
+ ? stacktrace.Remove(stacktrace.LastIndexOf("\n"))
+ : stacktrace;
}
///
@@ -58,36 +86,7 @@ public bool IsUnhandledException()
/// Source code string
public override string ToString()
{
- var stringBuilder = new StringBuilder();
- // default log format
- stringBuilder.Append(
- string.Format(
- "[{0}] {1}<{2}>: {3}",
- Date.ToUniversalTime(),
- _backtraceFrame ? "(Backtrace)" : string.Empty,
- Enum.GetName(typeof(LogType),
- Type),
- Message));
-
- // include stack trace if log was generated by exception/error
- if (IsUnhandledException())
- {
- var stackTrace = StackTrace;
- if (string.IsNullOrEmpty(StackTrace))
- {
- stackTrace = "No stack trace available";
- }
-
- stackTrace = stackTrace.Trim();
- // remove last \n that looks ugly in web debugger
- if (stackTrace.EndsWith("\n"))
- {
- stackTrace = stackTrace.Remove(stackTrace.LastIndexOf("\n"));
- }
- stringBuilder.AppendLine();
- stringBuilder.Append(stackTrace);
- }
- return stringBuilder.ToString();
+ return _formattedMessage;
}
}
}
diff --git a/Runtime/Model/Database/BacktraceDatabaseAttachmentManager.cs b/Runtime/Model/Database/BacktraceDatabaseAttachmentManager.cs
index 3b26108c..e0e9a3c0 100644
--- a/Runtime/Model/Database/BacktraceDatabaseAttachmentManager.cs
+++ b/Runtime/Model/Database/BacktraceDatabaseAttachmentManager.cs
@@ -23,15 +23,24 @@ public BacktraceDatabaseAttachmentManager(BacktraceDatabaseSettings settings)
public IEnumerable GetReportAttachments(BacktraceData data)
{
- return new List()
+ var attachmentPrefix = data.UuidString;
+
+ var result = new List();
+ AddIfPathIsNotEmpty(result, GetScreenshotPath(attachmentPrefix));
+ AddIfPathIsNotEmpty(result, GetUnityPlayerLogFile(data, attachmentPrefix));
+ AddIfPathIsNotEmpty(result, GetMinidumpPath(data, attachmentPrefix));
+ return result;
+ }
+
+ private void AddIfPathIsNotEmpty(List source, string attachmentPath)
+ {
+ if (!string.IsNullOrEmpty(attachmentPath))
{
- GetScreenshotPath(data),
- GetUnityPlayerLogFile(data),
- GetMinidumpPath(data)
- };
+ source.Add(attachmentPath);
+ }
}
- private string GetMinidumpPath(BacktraceData backtraceData)
+ private string GetMinidumpPath(BacktraceData backtraceData, string dataPrefix)
{
if (_settings.MinidumpType == MiniDumpType.None)
{
@@ -39,7 +48,7 @@ private string GetMinidumpPath(BacktraceData backtraceData)
}
//note that every minidump file generated by app ends with .dmp extension
//its important information if you want to clear minidump file
- string minidumpDestinationPath = Path.Combine(_settings.DatabasePath, string.Format("{0}-dump.dmp", backtraceData.Uuid));
+ string minidumpDestinationPath = Path.Combine(_settings.DatabasePath, string.Format("{0}-dump.dmp", dataPrefix));
var backtraceReport = backtraceData.Report;
if (backtraceReport == null)
{
@@ -63,13 +72,13 @@ private string GetMinidumpPath(BacktraceData backtraceData)
/// Get path to game screenshot when exception occured
///
/// Path to game screenshot
- private string GetScreenshotPath(BacktraceData backtraceData)
+ private string GetScreenshotPath(string dataPrefix)
{
if (!_settings.GenerateScreenshotOnException)
{
return string.Empty;
}
- var screenshotPath = Path.Combine(_settings.DatabasePath, string.Format("{0}.jpg", backtraceData.Uuid));
+ var screenshotPath = Path.Combine(_settings.DatabasePath, string.Format("{0}-screen.jpg", dataPrefix));
lock (_lock)
{
@@ -93,11 +102,12 @@ private string GetScreenshotPath(BacktraceData backtraceData)
tex.ReadPixels(new Rect(0, 0, width, height), 0, 0);
tex.Apply();
- // Encode texture into JPG
- byte[] bytes = tex.EncodeToJPG();
-
- // For testing purposes, also write to a file in the project folder
- File.WriteAllBytes(screenshotPath, bytes);
+ // Encode texture into JPG and save it on hard drive
+ var bytes = tex.EncodeToJPG();
+ using (var fs = new FileStream(screenshotPath, FileMode.Create, FileAccess.Write))
+ {
+ fs.Write(bytes, 0, bytes.Length);
+ }
_lastScreenTime = BacktraceDatabase.LastFrameTime;
_lastScreenPath = screenshotPath;
}
@@ -111,7 +121,7 @@ private string GetScreenshotPath(BacktraceData backtraceData)
/// Get path to Unity player logs.
///
/// Path to unity player log
- private string GetUnityPlayerLogFile(BacktraceData backtraceData)
+ private string GetUnityPlayerLogFile(BacktraceData backtraceData, string dataPrefix)
{
if (!_settings.AddUnityLogToReport)
{
@@ -136,7 +146,7 @@ private string GetUnityPlayerLogFile(BacktraceData backtraceData)
{
return string.Empty;
}
- var databaseLogPath = Path.Combine(_settings.DatabasePath, string.Format("{0}.log", backtraceData.Uuid));
+ var databaseLogPath = Path.Combine(_settings.DatabasePath, string.Format("{0}-lg.log", dataPrefix));
File.Copy(playerLogPath, databaseLogPath);
return databaseLogPath;
}
diff --git a/Runtime/Model/Database/BacktraceDatabaseRecord.cs b/Runtime/Model/Database/BacktraceDatabaseRecord.cs
index 94e812dd..4006d272 100644
--- a/Runtime/Model/Database/BacktraceDatabaseRecord.cs
+++ b/Runtime/Model/Database/BacktraceDatabaseRecord.cs
@@ -1,8 +1,6 @@
-using Backtrace.Unity.Interfaces.Database;
-using System;
+using System;
using System.Collections.Generic;
using System.IO;
-using System.Linq;
using System.Text;
using UnityEngine;
@@ -11,7 +9,7 @@ namespace Backtrace.Unity.Model.Database
///
/// Single record in BacktraceDatabase
///
- public class BacktraceDatabaseRecord : IDisposable
+ public class BacktraceDatabaseRecord
{
///
/// Id
@@ -60,6 +58,9 @@ public class BacktraceDatabaseRecord : IDisposable
private string _diagnosticDataJson;
+ ///
+ /// Determine if current record is duplicated
+ ///
public bool Duplicated
{
get
@@ -70,6 +71,9 @@ public bool Duplicated
private int _count = 1;
+ ///
+ /// Number of instances of the record
+ ///
public int Count
{
get
@@ -77,13 +81,10 @@ public int Count
return _count;
}
}
-
-
///
- /// Record writer
+ /// Return JSON diagnostic data
///
- internal IBacktraceDatabaseRecordWriter RecordWriter;
-
+ ///
public string BacktraceDataJson()
{
if (!string.IsNullOrEmpty(_diagnosticDataJson))
@@ -119,6 +120,10 @@ public BacktraceData BacktraceData
}
}
+ ///
+ /// Convert current record to JSON
+ ///
+ /// Record JSON representation
public string ToJson()
{
var rawRecord = new BacktraceDatabaseRawRecord()
@@ -130,9 +135,14 @@ public string ToJson()
hash = Hash,
attachments = Attachments
};
- return JsonUtility.ToJson(rawRecord, true);
+ return JsonUtility.ToJson(rawRecord, false);
}
+ ///
+ /// Convert JSON record to Record
+ ///
+ /// JSON record
+ /// Backtrace Database record
public static BacktraceDatabaseRecord Deserialize(string json)
{
var rawRecord = JsonUtility.FromJson(json);
@@ -162,21 +172,22 @@ public BacktraceDatabaseRecord(BacktraceData data, string path)
Record = data;
_path = path;
Attachments = data.Attachments;
- RecordWriter = new BacktraceDatabaseRecordWriter(path);
}
///
/// Save data to hard drive
///
- ///
+ /// True if record was successfully saved on hard drive
public bool Save()
{
try
{
+ var jsonPrefix = Record.UuidString;
_diagnosticDataJson = Record.ToJson();
- DiagnosticDataPath = Save(_diagnosticDataJson, string.Format("{0}-attachment", Id));
+ DiagnosticDataPath = Path.Combine(_path, string.Format("{0}-attachment.json", jsonPrefix));
+ Save(_diagnosticDataJson, DiagnosticDataPath);
- if (Attachments != null && Attachments.Any())
+ if (Attachments != null && Attachments.Count != 0)
{
foreach (var attachment in Attachments)
{
@@ -187,20 +198,14 @@ public bool Save()
}
}
//save record
- RecordPath = Path.Combine(_path, string.Format("{0}-record.json", Id));
- //check current record size
- var json = ToJson();
- byte[] file = Encoding.UTF8.GetBytes(json);
- //add record size
- Size += file.Length;
- RecordWriter.Write(json, string.Format("{0}-record", Id));
+ RecordPath = Path.Combine(_path, string.Format("{0}-record.json", jsonPrefix));
+ Save(ToJson(), RecordPath);
return true;
}
catch (IOException io)
{
- Debug.Log(string.Format("Received {0} while saving data to database.",
- "IOException"));
- Debug.Log(string.Format("Message {0}", io.Message));
+ Debug.Log("Received IOException while saving data to database.");
+ Debug.Log(io.Message);
return false;
}
catch (Exception ex)
@@ -218,24 +223,26 @@ public bool Save()
internal void DatabasePath(string path)
{
_path = path;
- RecordWriter = new BacktraceDatabaseRecordWriter(path);
}
///
/// Save single file from database record
///
/// single file (json/dmp)
- /// file prefix
- /// path to file
- private string Save(string json, string prefix)
+ /// file path
+ private void Save(string json, string destPath)
{
if (string.IsNullOrEmpty(json))
{
- return string.Empty;
+ return;
}
byte[] file = Encoding.UTF8.GetBytes(json);
Size += file.Length;
- return RecordWriter.Write(file, prefix);
+
+ using (var fs = new FileStream(destPath, FileMode.Create, FileAccess.Write))
+ {
+ fs.Write(file, 0, file.Length);
+ }
}
///
@@ -264,7 +271,7 @@ internal void Delete()
Delete(RecordPath);
//remove database attachments
- if (Attachments != null && Attachments.Any())
+ if (Attachments != null && Attachments.Count != 0)
{
foreach (var attachment in Attachments)
{
@@ -334,29 +341,15 @@ private bool IsInsideDatabaseDirectory(string path)
}
return Path.GetDirectoryName(path) == _path;
}
- #region dispose
-#pragma warning disable CA1063 // Implement IDisposable Correctly
- public virtual void Dispose()
-#pragma warning restore CA1063 // Implement IDisposable Correctly
+ public virtual void Unlock()
{
- Dispose(true);
- GC.SuppressFinalize(this);
+ Locked = false;
+ Record = null;
+ _diagnosticDataJson = string.Empty;
}
- protected virtual void Dispose(bool disposing)
- {
- if (disposing)
- {
- Locked = false;
- Record = null;
- _diagnosticDataJson = string.Empty;
- }
- }
-
- #endregion
-
[Serializable]
- private class BacktraceDatabaseRawRecord
+ private struct BacktraceDatabaseRawRecord
{
public string Id;
public string recordName;
diff --git a/Runtime/Model/Database/BacktraceDatabaseRecordWriter.cs b/Runtime/Model/Database/BacktraceDatabaseRecordWriter.cs
deleted file mode 100644
index bf624426..00000000
--- a/Runtime/Model/Database/BacktraceDatabaseRecordWriter.cs
+++ /dev/null
@@ -1,65 +0,0 @@
-using Backtrace.Unity.Interfaces.Database;
-using System.IO;
-using System.Text;
-
-namespace Backtrace.Unity.Model.Database
-{
- ///
- /// Database record writer
- ///
- internal class BacktraceDatabaseRecordWriter : IBacktraceDatabaseRecordWriter
- {
- ///
- /// Path to destination directory
- ///
- private readonly string _destinationPath;
-
- ///
- /// Initialize new database record writer
- ///
- /// Path to destination folder
- internal BacktraceDatabaseRecordWriter(string path)
- {
- _destinationPath = path;
- }
-
- public string Write(string json, string prefix)
- {
- byte[] file = Encoding.UTF8.GetBytes(json);
- return Write(file, prefix);
- }
-
- public string Write(byte[] data, string prefix)
- {
- string filename = string.Format("{0}.json", prefix);
- string tempFilePath = Path.Combine(_destinationPath, string.Format("temp_{0}", filename));
- SaveTemporaryFile(tempFilePath, data);
- string destFilePath = Path.Combine(_destinationPath, filename);
- SaveValidRecord(tempFilePath, destFilePath);
- return destFilePath;
- }
-
- ///
- /// Save valid diagnostic data from temporary file
- ///
- /// Temporary file path
- /// destination path
- public void SaveValidRecord(string sourcePath, string destinationPath)
- {
- File.Move(sourcePath, destinationPath);
- }
-
- ///
- /// Save temporary file to hard drive.
- ///
- /// Path to temporary file
- /// Current file
- public void SaveTemporaryFile(string path, byte[] file)
- {
- using (var fs = new FileStream(path, FileMode.Create, FileAccess.Write))
- {
- fs.Write(file, 0, file.Length);
- }
- }
- }
-}
diff --git a/Runtime/Model/JsonData/Annotations.cs b/Runtime/Model/JsonData/Annotations.cs
index 62310c1d..1a352805 100644
--- a/Runtime/Model/JsonData/Annotations.cs
+++ b/Runtime/Model/JsonData/Annotations.cs
@@ -2,7 +2,7 @@
using System;
using System.Collections;
using System.Collections.Generic;
-using System.Text.RegularExpressions;
+using System.Globalization;
using UnityEngine;
using UnityEngine.SceneManagement;
namespace Backtrace.Unity.Model.JsonData
@@ -22,9 +22,7 @@ public class Annotations
/// Exception object
///
public Exception Exception { get; set; }
- public Annotations()
- {
- }
+
///
/// Create new instance of Annotations class
///
@@ -35,41 +33,44 @@ public Annotations(Exception exception, int gameObjectDepth)
_gameObjectDepth = gameObjectDepth;
Exception = exception;
}
+
private static Dictionary SetEnvironmentVariables()
{
- var environmentVariables = new Dictionary();
- foreach (DictionaryEntry variable in Environment.GetEnvironmentVariables())
+ var result = new Dictionary();
+ var environmentVariables = Environment.GetEnvironmentVariables();
+ if (environmentVariables == null)
+ {
+ return result;
+ }
+
+
+ foreach (DictionaryEntry variable in environmentVariables)
{
- if (variable.Key == null)
+ var key = variable.Key as string;
+ if (string.IsNullOrEmpty(key))
{
continue;
}
- var value = variable.Value == null ? "NULL" : variable.Value.ToString();
- environmentVariables.Add(variable.Key.ToString(), value);
+
+ var rawValue = variable.Value as string;
+ result.Add(key, string.IsNullOrEmpty(rawValue) ? "NULL" : rawValue);
}
- return environmentVariables;
+ return result;
}
public BacktraceJObject ToJson()
{
var annotations = new BacktraceJObject();
- var envVariables = new BacktraceJObject();
- if (EnvironmentVariables != null)
- {
- foreach (var envVariable in EnvironmentVariables)
- {
- envVariables[envVariable.Key] = envVariable.Value;
- }
- annotations["Environment Variables"] = envVariables;
- }
+ annotations.Add("Environment Variables", new BacktraceJObject(EnvironmentVariables));
+
if (Exception != null)
{
- annotations["Exception properties"] = new BacktraceJObject()
+ annotations.Add("Exception properties", new BacktraceJObject(new Dictionary()
{
- ["message"] = Exception.Message,
- ["stackTrace"] = Exception.StackTrace,
- ["type"] = Exception.GetType().FullName,
- ["source"] = Exception.Source
- };
+ {"message", Exception.Message },
+ {"stackTrace",Exception.StackTrace},
+ {"type", Exception.GetType().FullName },
+ { "source",Exception.Source },
+ }));
}
if (_gameObjectDepth > -1)
{
@@ -81,7 +82,7 @@ public BacktraceJObject ToJson()
{
gameObjects.Add(ConvertGameObject(gameObject));
}
- annotations["Game objects"] = gameObjects;
+ annotations.Add("Game objects", gameObjects);
}
return annotations;
}
@@ -102,7 +103,7 @@ private BacktraceJObject ConvertGameObject(GameObject gameObject, int depth = 0)
}
innerObjects.Add(ConvertGameObject(transformChildObject, gameObject.name, depth + 1));
}
- jGameObject["children"] = innerObjects;
+ jGameObject.Add("children", innerObjects);
return jGameObject;
}
private BacktraceJObject ConvertGameObject(Component gameObject, string parentName, int depth)
@@ -126,36 +127,36 @@ private BacktraceJObject ConvertGameObject(Component gameObject, string parentNa
}
innerObjects.Add(ConvertGameObject(transformChildObject, gameObject.name, depth + 1));
}
- result["children"] = innerObjects;
+ result.Add("children", innerObjects);
return result;
}
private BacktraceJObject GetJObject(GameObject gameObject, string parentName = "")
{
- var o = new BacktraceJObject();
- o["name"] = gameObject.name;
- o["isStatic"] = gameObject.isStatic;
- o["layer"] = gameObject.layer;
- o["transform.position"] = gameObject.transform.position.ToString() ?? "";
- o["transform.rotation"] = gameObject.transform.rotation.ToString() ?? "";
- o["tag"] = gameObject.tag;
- o["activeInHierarchy"] = gameObject.activeInHierarchy;
- o["activeSelf"] = gameObject.activeSelf;
- o["hideFlags"] = (int)gameObject.hideFlags;
- o["instanceId"] = gameObject.GetInstanceID();
- o["parnetName"] = string.IsNullOrEmpty(parentName) ? "root object" : parentName;
- return o;
+ return new BacktraceJObject(new Dictionary()
+ {
+ { "name", gameObject.name},
+ {"isStatic", gameObject.isStatic.ToString().ToLower() },
+ {"layer", gameObject.layer.ToString(CultureInfo.InvariantCulture) },
+ {"transform.position", gameObject.transform.position.ToString()},
+ {"transform.rotation", gameObject.transform.rotation.ToString()},
+ {"tag",gameObject.tag},
+ {"activeInHierarchy", gameObject.activeInHierarchy.ToString().ToLower()},
+ {"activeSelf", gameObject.activeSelf.ToString().ToLower() },
+ {"instanceId", gameObject.GetInstanceID().ToString(CultureInfo.InvariantCulture) },
+ { "parnetName", string.IsNullOrEmpty(parentName) ? "root object" : parentName }
+ });
}
private BacktraceJObject GetJObject(Component gameObject, string parentName = "")
{
- var o = new BacktraceJObject();
- o["name"] = gameObject.name;
- o["transform.position"] = gameObject.transform.position.ToString() ?? "";
- o["transform.rotation"] = gameObject.transform.rotation.ToString() ?? "";
- o["tag"] = gameObject.tag;
- o["hideFlags"] = (int)gameObject.hideFlags;
- o["instanceId"] = gameObject.GetInstanceID();
- o["parnetName"] = string.IsNullOrEmpty(parentName) ? "root object" : parentName;
- return o;
+ return new BacktraceJObject(new Dictionary()
+ {
+ { "name", gameObject.name},
+ {"transform.position", gameObject.transform.position.ToString()},
+ {"transform.rotation", gameObject.transform.rotation.ToString()},
+ {"tag",gameObject.tag},
+ {"instanceId", gameObject.GetInstanceID().ToString(CultureInfo.InvariantCulture) },
+ { "parnetName", string.IsNullOrEmpty(parentName) ? "root object" : parentName }
+ });
}
}
}
\ No newline at end of file
diff --git a/Runtime/Model/JsonData/BacktraceAttributes.cs b/Runtime/Model/JsonData/BacktraceAttributes.cs
index f000b1ff..9f096ea0 100644
--- a/Runtime/Model/JsonData/BacktraceAttributes.cs
+++ b/Runtime/Model/JsonData/BacktraceAttributes.cs
@@ -36,7 +36,6 @@ private static string MachineId
}
- internal const string APPLICATION_ATTRIBUTE_NAME = "application";
///
/// Create instance of Backtrace Attribute
///
@@ -52,7 +51,14 @@ public BacktraceAttributes(BacktraceReport report, Dictionary cl
if (report != null)
{
- ConvertAttributes(report, clientAttributes);
+ // Add report attributes
+ if (report.Attributes != null)
+ {
+ foreach (var attribute in report.Attributes)
+ {
+ Attributes[attribute.Key] = attribute.Value;
+ }
+ }
SetExceptionAttributes(report);
}
//Environment attributes override user attributes
@@ -71,11 +77,17 @@ public BacktraceJObject ToJson()
private void SetScriptingBackend()
{
#if NET_STANDARD_2_0
- Attributes["scripting.backend"] = ".NET Standard 2.0";
+ Attributes["api.compatibility"] = ".NET Standard 2.0";
#elif NET_4_6
- Attributes["scripting.backend"] = ".NET Framework 4.5";
+ Attributes["api.compatibility"] = ".NET Framework 4.5";
+#else
+ Attributes["api.compatibility"] = ".NET Framework 3.5 equivalent";
+#endif
+
+#if ENABLE_IL2CPP
+ Attributes["scripting.backend"] = "IL2CPP";
#else
- Attributes["scripting.backend"] = ".NET Framework 3.5 equivalent";
+ Attributes["scripting.backend"] = "Mono";
#endif
}
///
@@ -96,10 +108,11 @@ private void SetLibraryAttributes(BacktraceReport report)
}
Attributes["guid"] = MachineId;
+ Attributes["backtrace.version"] = BacktraceClient.VERSION;
SetScriptingBackend();
//Base name of application generating the report
- Attributes[APPLICATION_ATTRIBUTE_NAME] = Application.productName;
+ Attributes["application"] = Application.productName;
Attributes["application.version"] = Application.version;
Attributes["application.url"] = Application.absoluteURL;
Attributes["application.company.name"] = Application.companyName;
@@ -158,21 +171,6 @@ private static string GenerateMachineId()
#endif
}
- ///
- /// Convert custom user attributes
- ///
- /// Received report
- /// Client's attributes (report and client)
- /// Dictionary of custom user attributes
- private void ConvertAttributes(BacktraceReport report, Dictionary clientAttributes)
- {
- var reportAttributes = BacktraceReport.ConcatAttributes(report, clientAttributes);
- foreach (var attribute in reportAttributes)
- {
- Attributes[attribute.Key] = attribute.Value;
- }
- }
-
///
/// Set attributes from exception
///
@@ -196,15 +194,21 @@ internal void SetExceptionAttributes(BacktraceReport report)
}
if (report.Exception is BacktraceUnhandledException)
{
- if ((report.Exception as BacktraceUnhandledException).Classifier == "ANRException")
+ var classifier = (report.Exception as BacktraceUnhandledException).Classifier;
+ if (classifier == "ANRException")
{
Attributes[errorType] = "Hang";
}
+ else if (classifier == "OOMException")
+ {
+ Attributes[errorType] = "Low Memory";
+ }
else
{
Attributes[errorType] = "Unhandled exception";
}
- } else
+ }
+ else
{
Attributes[errorType] = "Exception";
}
diff --git a/Runtime/Model/JsonData/ThreadData.cs b/Runtime/Model/JsonData/ThreadData.cs
index 544a8e65..5c111242 100644
--- a/Runtime/Model/JsonData/ThreadData.cs
+++ b/Runtime/Model/JsonData/ThreadData.cs
@@ -34,14 +34,13 @@ internal ThreadData(IEnumerable exceptionStack, bool faulti
//set currentThreadId
MainThread = generatedMainThreadId;
}
- private ThreadData() { }
public BacktraceJObject ToJson()
{
var threadData = new BacktraceJObject();
foreach (var threadInfo in ThreadInformations)
{
- threadData[threadInfo.Key] = threadInfo.Value.ToJson();
+ threadData.Add(threadInfo.Key, threadInfo.Value.ToJson());
}
return threadData;
}
diff --git a/Runtime/Model/JsonData/ThreadInformation.cs b/Runtime/Model/JsonData/ThreadInformation.cs
index b3eafd85..1ceadb88 100644
--- a/Runtime/Model/JsonData/ThreadInformation.cs
+++ b/Runtime/Model/JsonData/ThreadInformation.cs
@@ -1,6 +1,7 @@
using Backtrace.Unity.Extensions;
using Backtrace.Unity.Json;
using System.Collections.Generic;
+using System.Linq;
using System.Threading;
namespace Backtrace.Unity.Model.JsonData
@@ -25,15 +26,17 @@ public class ThreadInformation
public BacktraceJObject ToJson()
{
var stackFrames = new List();
- foreach (var stack in Stack)
+ for (int i = 0; i < Stack.Count(); i++)
{
- stackFrames.Add(stack.ToJson());
+ stackFrames.Add(Stack.ElementAt(i).ToJson());
}
- var o = new BacktraceJObject();
- o["name"] = Name;
- o["fault"] = Fault;
- o["stack"] = stackFrames;
+ var o = new BacktraceJObject(new Dictionary()
+ {
+ {"name", Name },
+ });
+ o.Add("fault", Fault);
+ o.ComplexObjects.Add("stack", stackFrames);
return o;
}
@@ -59,7 +62,7 @@ public ThreadInformation(string threadName, bool fault, IEnumerable stack, bool faultingThread = false)
: this(
threadName: thread.GenerateValidThreadName().ToLower(),
- fault: faultingThread,
+ fault: faultingThread,
stack: stack)
{ }
diff --git a/Runtime/Native/Android/NativeClient.cs b/Runtime/Native/Android/NativeClient.cs
index c45756e1..f2984f19 100644
--- a/Runtime/Native/Android/NativeClient.cs
+++ b/Runtime/Native/Android/NativeClient.cs
@@ -5,6 +5,7 @@
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
+using System.Threading;
using UnityEngine;
namespace Backtrace.Unity.Runtime.Native.Android
@@ -14,12 +15,22 @@ namespace Backtrace.Unity.Runtime.Native.Android
///
internal class NativeClient : INativeClient
{
+ // Last Backtrace client update time
+ internal float _lastUpdateTime;
+
+ private Thread _anrThread;
+
[DllImport("backtrace-native")]
private static extern bool Initialize(IntPtr submissionUrl, IntPtr databasePath, IntPtr handlerPath, IntPtr keys, IntPtr values);
[DllImport("backtrace-native")]
private static extern bool AddAttribute(IntPtr key, IntPtr value);
+ [DllImport("backtrace-native", EntryPoint = "DumpWithoutCrash")]
+ private static extern bool NativeReport(IntPtr message);
+
+
+
private readonly BacktraceConfiguration _configuration;
// Android native interface paths
private const string _namespace = "backtrace.io.backtrace_unity_android_plugin";
@@ -51,10 +62,9 @@ public NativeClient(string gameObjectName, BacktraceConfiguration configuration)
return;
}
#if UNITY_ANDROID
- _captureNativeCrashes = _configuration.CaptureNativeCrashes;
_handlerANR = _configuration.HandleANR;
- HandleAnr(gameObjectName, "OnAnrDetected");
HandleNativeCrashes();
+ HandleAnr(gameObjectName, "OnAnrDetected");
#endif
}
@@ -65,7 +75,13 @@ public NativeClient(string gameObjectName, BacktraceConfiguration configuration)
private void HandleNativeCrashes()
{
// make sure database is enabled
- if (!_captureNativeCrashes || !_configuration.Enabled)
+ var integrationDisabled =
+#if UNITY_ANDROID
+ !_configuration.CaptureNativeCrashes || !_configuration.Enabled;
+#else
+ true;
+#endif
+ if (integrationDisabled)
{
Debug.LogWarning("Backtrace native integration status: Disabled NDK integration");
return;
@@ -76,7 +92,7 @@ private void HandleNativeCrashes()
Debug.LogWarning("Backtrace native integration status: database path doesn't exist");
return;
}
- if(!Directory.Exists(databasePath))
+ if (!Directory.Exists(databasePath))
{
Directory.CreateDirectory(databasePath);
}
@@ -106,12 +122,9 @@ private void HandleNativeCrashes()
}
// get default built-in Backtrace-Unity attributes
var backtraceAttributes = new BacktraceAttributes(null, null, true);
- // add exception type to crashes handled by crashpad - all exception handled by crashpad
- // will be game crashes
- backtraceAttributes.Attributes["error.type"] = "Crash";
- backtraceAttributes.Attributes["backtrace.version"] = BacktraceClient.VERSION;
+
var minidumpUrl = new BacktraceCredentials(_configuration.GetValidServerUrl()).GetMinidumpSubmissionUrl().ToString();
-
+
// reassign to captureNativeCrashes
// to avoid doing anything on crashpad binary, when crashpad
// isn't available
@@ -125,18 +138,28 @@ private void HandleNativeCrashes()
{
Debug.LogWarning("Backtrace native integration status: Cannot initialize Crashpad client");
}
+ // add exception type to crashes handled by crashpad - all exception handled by crashpad
+ // by default we setting this option here, to set error.type when unexpected crash happen (so attribute will present)
+ // otherwise in other methods - ANR detection, OOM handler, we're overriding it and setting it back to "crash"
+
+ // warning
+ // don't add attributes that can change over the time to initialization method attributes. Crashpad will prevent from
+ // overriding them on game runtime. ANRs/OOMs methods can override error.type attribute, so we shouldn't pass error.type
+ // attribute via attributes parameters.
+ AddAttribute(
+ AndroidJNI.NewStringUTF("error.type"),
+ AndroidJNI.NewStringUTF("Crash"));
}
///
/// Retrieve Backtrace Attributes from the Android native code.
///
/// Backtrace Attributes from the Android build
- public Dictionary GetAttributes()
+ public void GetAttributes(Dictionary result)
{
- var result = new Dictionary();
if (!_enabled)
{
- return result;
+ return;
}
using (var unityPlayer = new AndroidJavaClass("com.unity3d.player.UnityPlayer"))
@@ -144,7 +167,7 @@ public Dictionary GetAttributes()
using (var context = activity.Call("getApplicationContext"))
using (var backtraceAttributes = new AndroidJavaObject(_nativeAttributesPath))
{
- var androidAttributes = backtraceAttributes.Call("GetAttributes", context);
+ var androidAttributes = backtraceAttributes.Call("GetAttributes", new object[] { context });
var entrySet = androidAttributes.Call("entrySet");
var iterator = entrySet.Call("iterator");
while (iterator.Call("hasNext"))
@@ -155,9 +178,6 @@ public Dictionary GetAttributes()
var value = pair.Call("getValue");
result[key] = value;
}
-
- return result;
-
}
}
@@ -181,6 +201,54 @@ public void HandleAnr(string gameObjectName, string callbackName)
Debug.LogWarning(string.Format("Cannot initialize ANR watchdog - reason: {0}", e.Message));
_enabled = false;
}
+
+ if (!_captureNativeCrashes)
+ {
+ return;
+ }
+
+ bool reported = false;
+ var mainThreadId = Thread.CurrentThread.ManagedThreadId;
+ _anrThread = new Thread(() =>
+ {
+ float lastUpdatedCache = 0;
+ while (true)
+ {
+ if (lastUpdatedCache == 0)
+ {
+ lastUpdatedCache = _lastUpdateTime;
+ }
+ else if (lastUpdatedCache == _lastUpdateTime)
+ {
+ if (!reported)
+ {
+
+ reported = true;
+ if (AndroidJNI.AttachCurrentThread() == 0)
+ {
+ // set temporary attribute to "Hang"
+ AddAttribute(
+ AndroidJNI.NewStringUTF("error.type"),
+ AndroidJNI.NewStringUTF("Hang"));
+
+ NativeReport(AndroidJNI.NewStringUTF("ANRException: Blocked thread detected."));
+ // update error.type attribute in case when crash happen
+ SetAttribute("error.type", "Crash");
+ }
+ }
+ }
+ else
+ {
+ reported = false;
+ }
+
+ lastUpdatedCache = _lastUpdateTime;
+ Thread.Sleep(5000);
+
+ }
+ });
+
+ _anrThread.Start();
}
///
@@ -204,5 +272,45 @@ public void SetAttribute(string key, string value)
AndroidJNI.NewStringUTF(key),
AndroidJNI.NewStringUTF(value));
}
+
+ ///
+ /// Report OOM via Backtrace native android library.
+ ///
+ /// true - if native crash reprorter is enabled. Otherwise false.
+ public bool OnOOM()
+ {
+ if (!_enabled || _captureNativeCrashes)
+ {
+ return false;
+ }
+
+ // set temporary attribute to "Hang"
+ SetAttribute("error.type", "Low Memory");
+ NativeReport(AndroidJNI.NewStringUTF("OOMException: Out of memory detected."));
+ // update error.type attribute in case when crash happen
+ SetAttribute("error.type", "Crash");
+
+ return true;
+ }
+
+ ///
+ /// Update native client internal timer.
+ ///
+ /// Current time
+ public void UpdateClientTime(float time)
+ {
+ _lastUpdateTime = time;
+ }
+
+ ///
+ /// Disable native client integration
+ ///
+ public void Disable()
+ {
+ if (_anrThread != null)
+ {
+ _anrThread.Abort();
+ }
+ }
}
}
\ No newline at end of file
diff --git a/Runtime/Native/INativeClient.cs b/Runtime/Native/INativeClient.cs
index d4aee7a1..2fea969c 100644
--- a/Runtime/Native/INativeClient.cs
+++ b/Runtime/Native/INativeClient.cs
@@ -2,13 +2,43 @@
namespace Backtrace.Unity.Runtime.Native
{
+ ///
+ /// Backtrace native client definition
+ ///
internal interface INativeClient
{
-#if UNITY_ANDROID || UNITY_IOS
+ ///
+ /// Handle ANR - Application not responding events
+ ///
void HandleAnr(string gameObjectName, string callbackName);
-#endif
- Dictionary GetAttributes();
+ ///
+ /// Set native attributes in attributes dictionary
+ ///
+ /// Attributes dictionary
+ void GetAttributes(Dictionary data);
+
+ ///
+ /// Set native attribute
+ ///
+ /// attribute key
+ /// attribute value
void SetAttribute(string key, string value);
+
+ ///
+ /// Report OOM via Backtrace native library.
+ ///
+ /// true - if native crash reprorter is enabled. Otherwise false.
+ bool OnOOM();
+
+ ///
+ /// Update native client internal ANR timer.
+ ///
+ void UpdateClientTime(float time);
+
+ ///
+ /// Disable native integration
+ ///
+ void Disable();
}
}
diff --git a/Runtime/Native/iOS/NativeClient.cs b/Runtime/Native/iOS/NativeClient.cs
index c069da03..8938942f 100644
--- a/Runtime/Native/iOS/NativeClient.cs
+++ b/Runtime/Native/iOS/NativeClient.cs
@@ -1,8 +1,10 @@
#if UNITY_IOS
using System;
using System.Collections.Generic;
+using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
+using System.Threading;
using Backtrace.Unity.Model;
using UnityEngine;
@@ -11,8 +13,13 @@ namespace Backtrace.Unity.Runtime.Native.iOS
///
/// iOS native client
///
- public class NativeClient : INativeClient
+ internal class NativeClient : INativeClient
{
+ // Last Backtrace client update time
+ internal float _lastUpdateTime;
+
+ private Thread _anrThread;
+
// NSDictinary entry used only for iOS native integration
internal struct Entry
{
@@ -23,6 +30,9 @@ internal struct Entry
[DllImport("__Internal", EntryPoint = "StartBacktraceIntegration")]
private static extern void Start(string plCrashReporterUrl, string[] attributeKeys, string[] attributeValues, int size);
+ [DllImport("__Internal", EntryPoint = "NativeReport")]
+ public static extern void NativeReport(string message);
+
[DllImport("__Internal", EntryPoint = "Crash")]
public static extern string Crash();
@@ -54,12 +64,12 @@ public NativeClient(string gameObjectName, BacktraceConfiguration configuration)
if (configuration.CaptureNativeCrashes)
{
HandleNativeCrashes(configuration);
- // get basic attributes to enable attributes bridge
- // otherwise first call to objective-c will take
- // a lot of time
- GetAttributes();
INITIALIZED = true;
}
+ if (configuration.HandleANR)
+ {
+ HandleAnr(gameObjectName, string.Empty);
+ }
}
@@ -69,6 +79,14 @@ public NativeClient(string gameObjectName, BacktraceConfiguration configuration)
private void HandleNativeCrashes(BacktraceConfiguration configuration)
{
+ var databasePath = configuration.GetFullDatabasePath();
+ // make sure database is enabled
+ if (string.IsNullOrEmpty(databasePath) || !Directory.Exists(databasePath))
+ {
+ Debug.LogWarning("Backtrace native integration status: database path doesn't exist");
+ return;
+ }
+
var plcrashreporterUrl = new BacktraceCredentials(configuration.GetValidServerUrl()).GetPlCrashReporterSubmissionUrl();
var backtraceAttributes = new Model.JsonData.BacktraceAttributes(null, null, true);
@@ -76,7 +94,6 @@ private void HandleNativeCrashes(BacktraceConfiguration configuration)
// The library will send PLCrashReporter crashes to Backtrace
// only when Crash occured
backtraceAttributes.Attributes["error.type"] = "Crash";
- backtraceAttributes.Attributes["backtrace.version"] = BacktraceClient.VERSION;
var attributeKeys = backtraceAttributes.Attributes.Keys.ToArray();
var attributeValues = backtraceAttributes.Attributes.Values.ToArray();
@@ -87,12 +104,11 @@ private void HandleNativeCrashes(BacktraceConfiguration configuration)
/// Retrieve Backtrace Attributes from the Android native code.
///
/// Backtrace Attributes from the Android build
- public Dictionary GetAttributes()
+ public void GetAttributes(Dictionary result)
{
- var result = new Dictionary();
if (!_enabled)
{
- return result;
+ return;
}
GetNativeAttibutes(out IntPtr pUnmanagedArray, out int keysCount);
@@ -104,19 +120,59 @@ public Dictionary GetAttributes()
}
Marshal.FreeHGlobal(pUnmanagedArray);
- return result;
}
///
/// Setup iOS ANR support and set callback function when ANR happened.
///
- /// Backtrace game object name
- /// Callback function name
public void HandleAnr(string gameObjectName, string callbackName)
{
- Debug.Log("ANR support on iOS is unsupported.");
+ // if INITIALIZED is equal to false, plcrashreporter instance is disabled
+ // so we can't generate native report
+ if (!_enabled || INITIALIZED == false)
+ {
+ return;
+ }
+
+ bool reported = false;
+ var mainThreadId = Thread.CurrentThread.ManagedThreadId;
+ _anrThread = new Thread(() =>
+ {
+ float lastUpdatedCache = 0;
+ while (true)
+ {
+ if (lastUpdatedCache == 0)
+ {
+ lastUpdatedCache = _lastUpdateTime;
+ }
+ else if (lastUpdatedCache == _lastUpdateTime)
+ {
+ if (!reported)
+ {
+ // set temporary attribute to "Hang"
+ SetAttribute("error.type", "Hang");
+ NativeReport("ANRException: Blocked thread detected.");
+ // update error.type attribute in case when crash happen
+ SetAttribute("error.type", "Crash");
+ reported = true;
+ }
+ }
+ else
+ {
+ reported = false;
+ }
+
+ lastUpdatedCache = _lastUpdateTime;
+ Thread.Sleep(5000);
+
+ }
+ });
+
+ _anrThread.Start();
}
+
+
///
/// Add attribute to native crash
///
@@ -137,6 +193,46 @@ public void SetAttribute(string key, string value)
}
AddAttribute(key, value);
}
+ ///
+ /// Report OOM via PlCrashReporter report.
+ ///
+ /// true - if native crash reprorter is enabled. Otherwise false.
+ public bool OnOOM()
+ {
+ // if INITIALIZED is equal to false, plcrashreporter instance is disabled
+ // so we can't generate native report
+ if (!_enabled || INITIALIZED == false)
+ {
+ return false;
+ }
+ // set temporary attribute to "Hang"
+ SetAttribute("error.type", "Low Memory");
+ NativeReport("OOMException: Out of memory detected.");
+ // update error.type attribute in case when crash happen
+ SetAttribute("error.type", "Crash");
+
+ return true;
+ }
+
+ ///
+ /// Update native client internal timer.
+ ///
+ /// Current time
+ public void UpdateClientTime(float time)
+ {
+ _lastUpdateTime = time;
+ }
+
+ ///
+ /// Disable native client integration
+ ///
+ public void Disable()
+ {
+ if (_anrThread != null)
+ {
+ _anrThread.Abort();
+ }
+ }
}
}
#endif
diff --git a/Runtime/Services/BacktraceApi.cs b/Runtime/Services/BacktraceApi.cs
index 58773c2f..01bd42de 100644
--- a/Runtime/Services/BacktraceApi.cs
+++ b/Runtime/Services/BacktraceApi.cs
@@ -300,7 +300,7 @@ public IEnumerator Send(string json, List attachments, Dictionary n.Value)
- .FirstOrDefault(n => n.Hash == hash);
-
- if (existRecord != null)
+ var existingRecord = GetRecordByHash(hash);
+ if (existingRecord != null)
{
- existRecord.Locked = true;
- existRecord.Increment();
+ existingRecord.Locked = true;
+ existingRecord.Increment();
TotalRecords++;
- return existRecord;
+ return existingRecord;
}
}
//add built-in attachments
var attachments = _attachmentManager.GetReportAttachments(backtraceData);
- foreach (var attachment in attachments)
+ for (int attachmentIndex = 0; attachmentIndex < attachments.Count(); attachmentIndex++)
{
- if (!string.IsNullOrEmpty(attachment))
+ if (!string.IsNullOrEmpty(attachments.ElementAt(attachmentIndex)))
{
- backtraceData.Report.AttachmentPaths.Add(attachment);
- backtraceData.Attachments.Add(attachment);
+ backtraceData.Report.AttachmentPaths.Add(attachments.ElementAt(attachmentIndex));
+ backtraceData.Attachments.Add(attachments.ElementAt(attachmentIndex));
}
}
@@ -147,6 +145,21 @@ public BacktraceDatabaseRecord Add(BacktraceData backtraceData)
return Add(record);
}
+ private BacktraceDatabaseRecord GetRecordByHash(string hash)
+ {
+ for (int batchIndex = 0; batchIndex < BatchRetry.Count; batchIndex++)
+ {
+ for (int recordIndex = 0; recordIndex < BatchRetry[batchIndex].Count; recordIndex++)
+ {
+ if (BatchRetry[batchIndex][recordIndex].Hash == hash)
+ {
+ return BatchRetry[batchIndex][recordIndex];
+ }
+ }
+ }
+ return null;
+ }
+
///
/// Convert Backtrace data to Backtrace record and save it.
///
@@ -300,12 +313,7 @@ private void RemoveMaxRetries()
{
TotalRecords--;
}
- //decrement total size of database
- System.Diagnostics.Debug.WriteLine(string.Format(
- "[RemoveMaxRetries]::BeforeDelete Total size: {0}. Record Size: {1} ", TotalSize, value.Size));
TotalSize -= value.Size;
- System.Diagnostics.Debug.WriteLine(string.Format("[RemoveMaxRetries]::AfterDelete Total size: {0} ",
- TotalSize));
}
}
}
@@ -325,7 +333,15 @@ public IEnumerable Get()
///
public int Count()
{
- return BatchRetry.SelectMany(n => n.Value).Sum(n => n.Count);
+ var result = 0;
+ for (int batchIndex = 0; batchIndex < BatchRetry.Count; batchIndex++)
+ {
+ for (int reportIndex = 0; reportIndex < BatchRetry[batchIndex].Count; reportIndex++)
+ {
+ result += BatchRetry[batchIndex][reportIndex].Count;
+ }
+ }
+ return result;
}
///
@@ -467,7 +483,7 @@ internal virtual string GenerateMiniDump(BacktraceReport backtraceReport, MiniDu
}
//note that every minidump file generated by app ends with .dmp extension
//its important information if you want to clear minidump file
- string minidumpDestinationPath = Path.Combine(_path, string.Format("{0}-dump.dmp", backtraceReport.Uuid));
+ string minidumpDestinationPath = Path.Combine(_path, string.Format("{0}-dump.dmp", backtraceReport.Uuid.ToString()));
MinidumpException minidumpExceptionType = backtraceReport.ExceptionTypeReport
? MinidumpException.Present
: MinidumpException.None;
diff --git a/Runtime/Services/ReportLimitWatcher.cs b/Runtime/Services/ReportLimitWatcher.cs
index 509d57ce..77614a1a 100644
--- a/Runtime/Services/ReportLimitWatcher.cs
+++ b/Runtime/Services/ReportLimitWatcher.cs
@@ -129,8 +129,7 @@ private void DisplayReportLimitHitMessage()
if (ShouldDisplayMessage())
{
_displayMessage = false;
- Debug.LogWarning(string.Format("Backtrace report limit hit({0}/min) – Ignoring errors for 1 minute",
- _reportPerMin));
+ Debug.LogWarning(string.Format("Backtrace report limit hit({0}/min) – Ignoring errors for 1 minute", _reportPerMin));
}
}
diff --git a/Runtime/Types/BacktraceStackFrameType.cs b/Runtime/Types/BacktraceStackFrameType.cs
new file mode 100644
index 00000000..cac46476
--- /dev/null
+++ b/Runtime/Types/BacktraceStackFrameType.cs
@@ -0,0 +1,10 @@
+namespace Backtrace.Unity.Types
+{
+ enum BacktraceStackFrameType
+ {
+ Unknown,
+ Dotnet,
+ Android,
+ Native
+ }
+}
diff --git a/Runtime/Model/Database/BacktraceDatabaseRecordWriter.cs.meta b/Runtime/Types/BacktraceStackFrameType.cs.meta
similarity index 83%
rename from Runtime/Model/Database/BacktraceDatabaseRecordWriter.cs.meta
rename to Runtime/Types/BacktraceStackFrameType.cs.meta
index 86388d64..f462da40 100644
--- a/Runtime/Model/Database/BacktraceDatabaseRecordWriter.cs.meta
+++ b/Runtime/Types/BacktraceStackFrameType.cs.meta
@@ -1,5 +1,5 @@
fileFormatVersion: 2
-guid: 166056be486b865498bfa5ee8ef8d1c2
+guid: aef9235bf1f511649b4455dfa95563fe
MonoImporter:
externalObjects: {}
serializedVersion: 2
diff --git a/Tests/Runtime/BacktraceClientTests.cs b/Tests/Runtime/BacktraceClientTests.cs
index 89dd2f99..c51390a0 100644
--- a/Tests/Runtime/BacktraceClientTests.cs
+++ b/Tests/Runtime/BacktraceClientTests.cs
@@ -9,7 +9,7 @@
namespace Backtrace.Unity.Tests.Runtime
{
- public class BacktraceClientTests: BacktraceBaseTest
+ public class BacktraceClientTests : BacktraceBaseTest
{
[SetUp]
public void Setup()
@@ -18,7 +18,7 @@ public void Setup()
AfterSetup(false);
}
-
+
[UnityTest]
public IEnumerator TestClientCreation_ValidBacktraceConfiguration_ValidClientCreation()
{
@@ -29,7 +29,7 @@ public IEnumerator TestClientCreation_ValidBacktraceConfiguration_ValidClientCre
yield return null;
}
-
+
[UnityTest]
public IEnumerator TestClientCreation_EmptyConfiguration_DisabledClientCreation()
{
@@ -135,7 +135,7 @@ public IEnumerator TestSendingReport_ValidConfiguration_ValidSend()
yield return null;
}
- [UnityTest]
+ [UnityTest]
public IEnumerator TestFingerprintBehaviorForNormalizedExceptionMessage_ShouldGenerateFingerprintForExceptionReportWithoutStackTrace_ShouldIncludeFingerprintInBacktraceReport()
{
BacktraceClient.Configuration = GetValidClientConfiguration();
@@ -152,16 +152,20 @@ public IEnumerator TestFingerprintBehaviorForNormalizedExceptionMessage_ShouldGe
bool eventFired = false;
BacktraceClient.BeforeSend = (BacktraceData data) =>
{
- eventFired = true;
+ Assert.IsNotNull(data.Attributes.Attributes["_mod_fingerprint"]);
Assert.AreEqual(expectedNormalizedMessage.GetSha(), data.Attributes.Attributes["_mod_fingerprint"]);
+ eventFired = true;
// prevent backtrace data from sending to Backtrace.
return null;
};
- BacktraceClient.Send(report);
- yield return new WaitForEndOfFrame();
+
+ yield return BacktraceClient.StartCoroutine(CallBacktraceClientAndWait(report));
+
+
Assert.IsTrue(eventFired);
}
+
[UnityTest]
public IEnumerator TestFingerprintBehaviorForNormalizedExceptionMessage_ShouldntGenerateFingerprintForDisabledOption_FingerprintDoesntExist()
{
@@ -178,13 +182,15 @@ public IEnumerator TestFingerprintBehaviorForNormalizedExceptionMessage_Shouldnt
bool eventFired = false;
BacktraceClient.BeforeSend = (BacktraceData data) =>
{
- eventFired = true;
Assert.IsFalse(data.Attributes.Attributes.ContainsKey("_mod_fingerprint"));
+ eventFired = true;
// prevent backtrace data from sending to Backtrace.
return null;
};
- BacktraceClient.Send(report);
- yield return new WaitForEndOfFrame();
+
+ yield return BacktraceClient.StartCoroutine(CallBacktraceClientAndWait(report));
+
+
Assert.IsTrue(eventFired);
}
@@ -200,13 +206,14 @@ public IEnumerator TestFingerprintBehaviorForNormalizedExceptionMessage_ShouldUs
// exception below has empty exception stack trace
var exception = new BacktraceUnhandledException("00:00:00 00/00/00 Unhandled exception", string.Empty);
var report = new BacktraceReport(exception);
- var expectedFingerprint = "foo-bar";
+ const string expectedFingerprint = "foo-bar";
report.Fingerprint = expectedFingerprint;
bool eventFired = false;
BacktraceClient.BeforeSend = (BacktraceData data) =>
{
eventFired = true;
+ Assert.IsNotNull(data.Attributes.Attributes["_mod_fingerprint"]);
Assert.AreEqual(expectedFingerprint, data.Attributes.Attributes["_mod_fingerprint"]);
// prevent backtrace data from sending to Backtrace.
return null;
@@ -242,16 +249,19 @@ public IEnumerator TestFingerprintBehaviorForNormalizedExceptionMessage_ShouldGe
var normalizedMessage = "Unhandledexception";
var exception = new BacktraceUnhandledException(normalizedMessage, string.Empty);
var report = new BacktraceReport(exception);
+
bool eventFired = false;
BacktraceClient.BeforeSend = (BacktraceData data) =>
{
- eventFired = true;
+ Assert.IsNotNull(data.Attributes.Attributes["_mod_fingerprint"]);
Assert.AreEqual(normalizedMessage.GetSha(), data.Attributes.Attributes["_mod_fingerprint"]);
// prevent backtrace data from sending to Backtrace.
+ eventFired = true;
+ // prevent backtrace data from sending to Backtrace.
return null;
};
- BacktraceClient.Send(report);
- yield return new WaitForEndOfFrame();
+
+ yield return BacktraceClient.StartCoroutine(CallBacktraceClientAndWait(report));
Assert.IsTrue(eventFired);
}
@@ -278,5 +288,12 @@ public IEnumerator TestFingerprintBehaviorForNormalizedExceptionMessage_Shouldnt
yield return new WaitForEndOfFrame();
Assert.IsTrue(eventFired);
}
+
+
+ private IEnumerator CallBacktraceClientAndWait(BacktraceReport report)
+ {
+ BacktraceClient.Send(report);
+ yield return new WaitForEndOfFrame();
+ }
}
}
diff --git a/Tests/Runtime/BacktraceJObjectTests.cs b/Tests/Runtime/BacktraceJObjectTests.cs
index 2f3ed36a..1e6b6205 100644
--- a/Tests/Runtime/BacktraceJObjectTests.cs
+++ b/Tests/Runtime/BacktraceJObjectTests.cs
@@ -34,20 +34,57 @@ public IEnumerator TestDataSerialization_BasicVariableUsage_DataSerializeCorrect
AgentName = "Backtrace-unity",
IntNumber = 1,
FloatNumber = 12.123f,
- DoubleNumber = 123.123d,
- LongNumber = 123
+ DoubleNumber = 555.432d,
+ LongNumber = 999
};
- var jObject = new BacktraceJObject()
+ var jObject = new BacktraceJObject();
+ jObject.Add("AgentName", sampleObject.AgentName);
+ jObject.Add("Active", sampleObject.Active);
+ jObject.Add("IntNumber", sampleObject.IntNumber);
+ jObject.Add("FloatNumber", sampleObject.FloatNumber);
+ jObject.Add("DoubleNumber", sampleObject.DoubleNumber);
+ jObject.Add("LongNumber", sampleObject.LongNumber);
+
+
+ var json = jObject.ToJson();
+
+ var jsonObject = JsonUtility.FromJson(json);
+
+ Assert.AreEqual(sampleObject.AgentName, jsonObject.AgentName);
+ Assert.AreEqual(sampleObject.Active, jsonObject.Active);
+ Assert.AreEqual(sampleObject.IntNumber, jsonObject.IntNumber);
+ Assert.AreEqual(sampleObject.FloatNumber, jsonObject.FloatNumber);
+ Assert.AreEqual(sampleObject.DoubleNumber, jsonObject.DoubleNumber);
+ Assert.AreEqual(sampleObject.LongNumber, jsonObject.LongNumber);
+ yield return null;
+ }
+
+ [UnityTest]
+ public IEnumerator TestDataSerialization_BasicVariableUsageWithPreinitializedVariables_DataSerializeCorrectly()
+ {
+ var sampleObject = new SampleObject()
{
- ["AgentName"] = sampleObject.AgentName,
- ["Active"] = sampleObject.Active,
- ["IntNumber"] = sampleObject.IntNumber,
- ["FloatNumber"] = sampleObject.FloatNumber,
- ["DoubleNumber"] = sampleObject.DoubleNumber,
- ["LongNumber"] = sampleObject.LongNumber,
+ Active = true,
+ AgentName = "Backtrace-unity",
+ IntNumber = 1,
+ FloatNumber = 12.123f,
+ DoubleNumber = 555.432d,
+ LongNumber = 999
};
+ var jObject = new BacktraceJObject(new Dictionary()
+ {
+ { "AgentName", sampleObject.AgentName }
+ });
+
+ jObject.Add("Active", sampleObject.Active);
+ jObject.Add("IntNumber", sampleObject.IntNumber);
+ jObject.Add("FloatNumber", sampleObject.FloatNumber);
+ jObject.Add("DoubleNumber", sampleObject.DoubleNumber);
+ jObject.Add("LongNumber", sampleObject.LongNumber);
+
+
var json = jObject.ToJson();
var jsonObject = JsonUtility.FromJson(json);
@@ -62,20 +99,101 @@ public IEnumerator TestDataSerialization_BasicVariableUsage_DataSerializeCorrect
}
[UnityTest]
- public IEnumerator TestDataSerialization_ShouldEscapeInvalidStringKey_DataSerializeCorrectly()
+ public IEnumerator TestDataSerialization_WithComplexValues_DataSerializeCorrectly()
{
- var invalidValue = string.Format("\"{0}\"", "foo");
- var jObject = new BacktraceJObject()
+ var sampleObject = new SampleObject()
{
- [invalidValue] = "foo"
+ Active = true,
+ AgentName = "Backtrace-unity",
+ LongNumber = 999,
+ NumberArray = new int[] { 1, 2, 3, 4 },
+ StringArray = new string[] { string.Empty, null, "foo", "bar" },
+ StringList = new List { string.Empty, null, "foo", "bar" }
};
+ var jObject = new BacktraceJObject();
+ jObject.Add("AgentName", sampleObject.AgentName);
+ jObject.Add("Active", sampleObject.Active);
+ jObject.Add("LongNumber", sampleObject.LongNumber);
+ jObject.Add("NumberArray", sampleObject.NumberArray);
+ jObject.Add("StringArray", sampleObject.StringArray);
+ jObject.Add("StringList", sampleObject.StringList);
+
+
var json = jObject.ToJson();
- foreach (var keyValuePair in jObject.Source)
+
+ var jsonObject = JsonUtility.FromJson(json);
+
+ Assert.AreEqual(sampleObject.AgentName, jsonObject.AgentName);
+ // validate number array
+ for (int i = 0; i < sampleObject.NumberArray.Length; i++)
+ {
+ Assert.AreEqual(jsonObject.NumberArray[i], sampleObject.NumberArray[i]);
+ }
+ // validate string array
+ for (int i = 0; i < sampleObject.StringArray.Length; i++)
+ {
+ // handle empty strings
+ var expectedValue = string.IsNullOrEmpty(sampleObject.StringArray[i]) ? string.Empty : sampleObject.StringArray[i];
+ Assert.AreEqual(jsonObject.StringArray[i], expectedValue);
+ }
+
+ // validate string list
+ for (int i = 0; i < sampleObject.StringList.Count; i++)
+ {
+ var expectedValue = string.IsNullOrEmpty(sampleObject.StringList[i]) ? string.Empty : sampleObject.StringList[i];
+ Assert.AreEqual(jsonObject.StringList[i], expectedValue);
+ }
+
+ yield return null;
+ }
+
+
+ [UnityTest]
+ public IEnumerator TestDataSerialization_WithInnerJObjectAndComplexArray_DataSerializeCorrectly()
+ {
+ // this test should validate if we can start analysing new data type without previous data types
+ var sampleObject = new BaseJObject()
+ {
+ InnerObject = new SampleObject()
+ {
+ NumberArray = new int[] { 1, 2, 3, 4 }
+ }
+ };
+
+ var sampleJObject = new BacktraceJObject();
+ var innerJObject = new BacktraceJObject();
+ innerJObject.Add("NumberArray", sampleObject.InnerObject.NumberArray);
+ sampleJObject.Add("InnerObject", innerJObject);
+
+
+ var json = sampleJObject.ToJson();
+ var jsonObject = JsonUtility.FromJson(json);
+
+ Assert.IsNotNull(jsonObject);
+ Assert.IsNotNull(jsonObject.InnerObject);
+ // validate number array
+ for (int i = 0; i < jsonObject.InnerObject.NumberArray.Length; i++)
+ {
+ Assert.AreEqual(jsonObject.InnerObject.NumberArray[i], jsonObject.InnerObject.NumberArray[i]);
+ }
+ yield return null;
+ }
+
+ [UnityTest]
+ public IEnumerator TestDataSerialization_ShouldEscapeInvalidStringKey_DataSerializeCorrectly()
+ {
+ var invalidValue = string.Format("\"{0}\"", "foo");
+ var jObject = new BacktraceJObject();
+ jObject.Add(invalidValue, "foo");
+
+ var json = jObject.ToJson();
+ foreach (var keyValuePair in jObject.PrimitiveValues)
{
Assert.IsTrue(json.Contains(keyValuePair.Key));
Assert.IsTrue(json.Contains(Regex.Escape(keyValuePair.Value.ToString())));
}
+
yield return null;
}
@@ -88,10 +206,8 @@ public IEnumerator TestDataSerialization_ShouldEscapeInvalidStringValues_DataSer
AgentName = invalidValue
};
- var jObject = new BacktraceJObject()
- {
- ["AgentName"] = invalidValue
- };
+ var jObject = new BacktraceJObject();
+ jObject.Add("AgentName", invalidValue);
var json = jObject.ToJson();
var deserializedObject = JsonUtility.FromJson(json);
Assert.AreEqual(sampleObject.AgentName, deserializedObject.AgentName);
@@ -103,7 +219,7 @@ public IEnumerator TestDataSerialization_ShouldSerializeDictionary_ShouldntSeria
{
var classifiers = new Dictionary { { "foo", "bar" } };
var jObject = new BacktraceJObject();
- jObject["classifier"] = classifiers;
+ jObject.Add("classifier", classifiers);
var json = jObject.ToJson();
Assert.IsNotEmpty(json);
@@ -121,8 +237,8 @@ public IEnumerator TestDataSerialization_ShouldSerializeArray_DataSerializeCorre
};
var jObject = new BacktraceJObject();
- jObject["StringArray"] = sampleObject.StringArray;
- jObject["NumberArray"] = sampleObject.NumberArray;
+ jObject.Add("StringArray", sampleObject.StringArray);
+ jObject.Add("NumberArray", sampleObject.NumberArray);
var json = jObject.ToJson();
var deserializedObject = JsonUtility.FromJson(json);
@@ -150,7 +266,7 @@ public IEnumerator TestDataSerialization_ShouldSerializeList_DataSerializeCorrec
};
var jObject = new BacktraceJObject();
- jObject["StringList"] = sampleObject.StringList;
+ jObject.Add("StringList", sampleObject.StringList);
var json = jObject.ToJson();
var deserializedObject = JsonUtility.FromJson(json);
@@ -167,18 +283,39 @@ public IEnumerator TestDataSerialization_ShouldSerializeList_DataSerializeCorrec
public IEnumerator TestDataSerialization_ShouldSerializeEmptyOrNullableValues_DataSerializeCorrectly()
{
var jObject = new BacktraceJObject();
- jObject["foo"] = null;
- jObject["bar"] = string.Empty;
+ jObject.Add("bar", string.Empty);
+ jObject.Add("foo", null as string);
+
+ var json = jObject.ToJson();
+
+ var expectedResult = "{" +
+ "\"bar\":null," +
+ "\"foo\":null" +
+ "}";
+ Assert.AreEqual(expectedResult, json);
+ yield return null;
+ }
+
+ [UnityTest]
+ public IEnumerator TestDataSerialization_ShouldEscapeCorrectlyAllKeys_DataSerializeCorrectly()
+ {
+ var jObject = new BacktraceJObject();
+ jObject.Add(@"foo""", string.Empty);
+ jObject.Add("\\bar".ToString(), null as string);
+ jObject.Add("b\naz".ToString(), null as string);
var json = jObject.ToJson();
+
var expectedResult = "{" +
- "\"foo\": null," +
- "\"bar\": \"\"" +
- "}";
+ "\"foo\\\"\":null," +
+ "\"\\\\bar\":null," +
+ "\"b\\naz\":null" +
+ "}";
Assert.AreEqual(expectedResult, json);
yield return null;
}
+
[UnityTest]
public IEnumerator TestDataSerialization_JsonWithCharactersToEscape_ShouldEscapeCorrectly()
{
@@ -192,11 +329,12 @@ public IEnumerator TestDataSerialization_JsonWithCharactersToEscape_ShouldEscape
};
var jObject = new BacktraceJObject();
- jObject["doubleQuote"] = expected.doubleQuote;
- jObject["slash"] = expected.slash;
- jObject["newLine"] = expected.newLine;
- jObject["tab"] = expected.tab;
- jObject["carriageReturn"] = expected.carriageReturn;
+
+ jObject.Add("doubleQuote", expected.doubleQuote);
+ jObject.Add("slash", expected.slash);
+ jObject.Add("newLine", expected.newLine);
+ jObject.Add("tab", expected.tab);
+ jObject.Add("carriageReturn", expected.carriageReturn);
var json = jObject.ToJson();
@@ -219,16 +357,277 @@ public IEnumerator TestDataSerialization_ShouldSerializeInnerJObject_DataSeriali
};
var jObject = new BacktraceJObject();
- jObject["StringArray"] = sampleObject.StringArray;
- jObject["NumberArray"] = sampleObject.NumberArray;
+ jObject.Add("StringArray", sampleObject.StringArray);
+ jObject.Add("NumberArray", sampleObject.NumberArray);
var inner = new BacktraceJObject();
- inner["foo"] = "bar";
- jObject["inner"] = inner;
+ inner.Add("foo", "bar");
+ jObject.Add("inner", inner);
var json = jObject.ToJson();
Assert.IsNotEmpty(json);
yield return null;
}
+
+
+ [UnityTest]
+ public IEnumerator TestDataSerialization_WithOnlyComplexValues_DataSerializeCorrectly()
+ {
+ // this test should validate if we can start analysing new data type without previous data types
+ var sampleObject = new SampleObject()
+ {
+ NumberArray = new int[] { 1, 2, 3, 4 }
+ };
+
+ var jObject = new BacktraceJObject();
+ jObject.Add("NumberArray", sampleObject.NumberArray);
+
+
+ var json = jObject.ToJson();
+ var jsonObject = JsonUtility.FromJson(json);
+ // validate number array
+ for (int i = 0; i < sampleObject.NumberArray.Length; i++)
+ {
+ Assert.AreEqual(jsonObject.NumberArray[i], sampleObject.NumberArray[i]);
+ }
+ yield return null;
+ }
+
+ [UnityTest]
+ public IEnumerator TestDataSerialization_WithOnlyJObject_DataSerializeCorrectly()
+ {
+ // this test should validate if we can start analysing new data type without previous data types
+ var sampleObject = new BaseJObject()
+ {
+ InnerObject = new SampleObject()
+ {
+ Active = true
+ }
+ };
+
+ var sampleJObject = new BacktraceJObject();
+ var innerJObject = new BacktraceJObject();
+ innerJObject.Add("Active", sampleObject.InnerObject.Active);
+ sampleJObject.Add("InnerObject", innerJObject);
+
+
+ var json = sampleJObject.ToJson();
+ var jsonObject = JsonUtility.FromJson(json);
+
+ Assert.IsNotNull(jsonObject);
+ Assert.IsNotNull(jsonObject.InnerObject);
+ Assert.IsTrue(jsonObject.InnerObject.Active);
+ yield return null;
+ }
+
+ [UnityTest]
+ public IEnumerator TestDataSerialization_WithUserPredefinedValues_DataSerializeCorrectly()
+ {
+ // this test should validate if we can start analysing new data type without previous data types
+
+ var sampleObject = new SampleObject()
+ {
+ AgentName = "Backtrace-unity",
+ TestString = "Test string"
+ };
+ var jObject = new BacktraceJObject(new Dictionary()
+ {
+ {"AgentName", sampleObject.AgentName },
+ {"TestString", sampleObject.TestString }
+ });
+
+
+ var json = jObject.ToJson();
+ var jsonObject = JsonUtility.FromJson(json);
+ Assert.IsNotNull(jsonObject);
+ Assert.AreEqual(sampleObject.AgentName, jsonObject.AgentName);
+ Assert.AreEqual(sampleObject.TestString, jsonObject.TestString);
+ yield return null;
+ }
+
+ [UnityTest]
+ public IEnumerator TestDataSerialization_WithAllTypeOfPossibleJsonTypes_DataSerializeCorrectly()
+ {
+ var sampleObject = new BaseJObject()
+ {
+ InnerObject = new SampleObject()
+ {
+ Active = true,
+ AgentName = "Backtrace-unity",
+ IntNumber = 1,
+ FloatNumber = 12.123f,
+ DoubleNumber = 555.432d,
+ LongNumber = 999,
+ NumberArray = new int[] { 1, 2, 3, 4 },
+ StringArray = new string[] { string.Empty, null, "foo", "bar" },
+ StringList = new List { string.Empty, null, "foo", "bar" }
+ }
+ };
+
+ var sampleJObject = new BacktraceJObject();
+ var innerJObject = new BacktraceJObject();
+ innerJObject.Add("AgentName", sampleObject.InnerObject.AgentName);
+ innerJObject.Add("Active", sampleObject.InnerObject.Active);
+ innerJObject.Add("IntNumber", sampleObject.InnerObject.IntNumber);
+ innerJObject.Add("FloatNumber", sampleObject.InnerObject.FloatNumber);
+ innerJObject.Add("DoubleNumber", sampleObject.InnerObject.DoubleNumber);
+ innerJObject.Add("LongNumber", sampleObject.InnerObject.LongNumber);
+ innerJObject.Add("NumberArray", sampleObject.InnerObject.NumberArray);
+ innerJObject.Add("StringArray", sampleObject.InnerObject.StringArray);
+ innerJObject.Add("StringList", sampleObject.InnerObject.StringList);
+
+ sampleJObject.Add("InnerObject", innerJObject);
+
+
+ var json = sampleJObject.ToJson();
+ var jsonObject = JsonUtility.FromJson(json);
+
+ // validate number array
+ var jsonInnerObject = jsonObject.InnerObject;
+ for (int i = 0; i < sampleObject.InnerObject.NumberArray.Length; i++)
+ {
+ Assert.AreEqual(sampleObject.InnerObject.NumberArray[i], jsonInnerObject.NumberArray[i]);
+ }
+ // validate string array
+ for (int i = 0; i < sampleObject.InnerObject.StringArray.Length; i++)
+ {
+ // handle empty strings
+ var expectedValue = string.IsNullOrEmpty(sampleObject.InnerObject.StringArray[i]) ? string.Empty : sampleObject.InnerObject.StringArray[i];
+ Assert.AreEqual(expectedValue, jsonInnerObject.StringArray[i]);
+ }
+
+ // validate string list
+ for (int i = 0; i < sampleObject.InnerObject.StringList.Count; i++)
+ {
+ // handle empty strings
+ var expectedValue = string.IsNullOrEmpty(sampleObject.InnerObject.StringList[i]) ? string.Empty : sampleObject.InnerObject.StringList[i];
+ Assert.AreEqual(expectedValue, jsonInnerObject.StringList[i]);
+ }
+
+
+ Assert.AreEqual(sampleObject.InnerObject.AgentName, jsonObject.InnerObject.AgentName);
+ Assert.AreEqual(sampleObject.InnerObject.Active, jsonObject.InnerObject.Active);
+ Assert.AreEqual(sampleObject.InnerObject.IntNumber, jsonObject.InnerObject.IntNumber);
+ Assert.AreEqual(sampleObject.InnerObject.FloatNumber, jsonObject.InnerObject.FloatNumber);
+ Assert.AreEqual(sampleObject.InnerObject.DoubleNumber, jsonObject.InnerObject.DoubleNumber);
+ Assert.AreEqual(sampleObject.InnerObject.LongNumber, jsonObject.InnerObject.LongNumber);
+ yield return null;
+ }
+
+
+ [UnityTest]
+ public IEnumerator TestStringEscapingInSerialization_WithUserPredefinedValues_DataSerializeCorrectly()
+ {
+ // this test should validate if we can escape correctly strings from user predefined values
+
+ var sampleObject = new SampleObject()
+ {
+ AgentName = "\"\\!@#$%^&*()+_=-{}{[]:\\\n\r\f\r ",
+ TestString = "\b\t\\\n\"''"
+ };
+ var jObject = new BacktraceJObject(new Dictionary()
+ {
+ {"AgentName", sampleObject.AgentName },
+ {"TestString", sampleObject.TestString }
+ });
+
+
+ var json = jObject.ToJson();
+ var jsonObject = JsonUtility.FromJson(json);
+ Assert.IsNotNull(jsonObject);
+ Assert.AreEqual(sampleObject.AgentName, jsonObject.AgentName);
+ Assert.AreEqual(sampleObject.TestString, jsonObject.TestString);
+ yield return null;
+ }
+
+ [UnityTest]
+ public IEnumerator TestStringEscapingInSerialization_WithPrimitiveValues_DataSerializeCorrectly()
+ {
+ // this test should validate if we can escape correctly strings from user predefined values
+
+ var sampleObject = new SampleObject()
+ {
+ AgentName = "\"\\!@#$%^&*()+_=-{}{[]:\\\n\r\f\r ",
+ TestString = "\b\t\\\n\"''"
+ };
+ var jObject = new BacktraceJObject();
+ jObject.Add("AgentName", sampleObject.AgentName);
+ jObject.Add("TestString", sampleObject.TestString);
+
+
+ var json = jObject.ToJson();
+ var jsonObject = JsonUtility.FromJson(json);
+ Assert.IsNotNull(jsonObject);
+ Assert.AreEqual(sampleObject.AgentName, jsonObject.AgentName);
+ Assert.AreEqual(sampleObject.TestString, jsonObject.TestString);
+ yield return null;
+ }
+
+ [UnityTest]
+ public IEnumerator TestStringEscapingInSerialization_WithJObject_DataSerializeCorrectly()
+ {
+ // this test should validate if we can escape correctly strings from user predefined values
+
+ var sampleObject = new BaseJObject()
+ {
+ InnerObject = new SampleObject()
+ {
+ AgentName = "\"\\!@#$%^&*()+_=-{}{[]:\\\n\r\f\r ",
+ TestString = "\b\t\\\n\"''"
+ }
+ };
+ var jObject = new BacktraceJObject();
+ var innerJObject = new BacktraceJObject();
+ innerJObject.Add("AgentName", sampleObject.InnerObject.AgentName);
+ innerJObject.Add("TestString", sampleObject.InnerObject.TestString);
+ jObject.Add("InnerObject", innerJObject);
+
+
+ var json = jObject.ToJson();
+ var jsonObject = JsonUtility.FromJson(json);
+ Assert.IsNotNull(jsonObject);
+ Assert.AreEqual(sampleObject.InnerObject.AgentName, jsonObject.InnerObject.AgentName);
+ Assert.AreEqual(sampleObject.InnerObject.TestString, jsonObject.InnerObject.TestString);
+ yield return null;
+ }
+
+ [UnityTest]
+ public IEnumerator TestStringEscapingInSerialization_WithComplexValues_DataSerializeCorrectly()
+ {
+ // this test should validate if we can escape correctly strings from user predefined values
+
+ var sampleObject = new SampleObject()
+ {
+ StringArray = new string[] { "\"\\!@#$%^&*()+_=-{}{[]:\\\n\r\f\r ", "\b\t\\\n\"''" },
+ StringList = new List() { "\"\\!@#$%^&*()+_=-{}{[]:\\\n\r\f\r ", "\b\t\\\n\"''" },
+ NumberArray = null
+ };
+
+ var jObject = new BacktraceJObject();
+ jObject.Add("StringArray", sampleObject.StringArray);
+ jObject.Add("StringList", sampleObject.StringList);
+ jObject.Add("NumberArray", sampleObject.NumberArray);
+
+
+ var json = jObject.ToJson();
+ var jsonObject = JsonUtility.FromJson(json);
+ Assert.IsNotNull(jsonObject);
+ Assert.IsEmpty(jsonObject.NumberArray);
+
+ for (int i = 0; i < sampleObject.StringArray.Length; i++)
+ {
+ // handle empty strings
+ Assert.AreEqual(jsonObject.StringArray[i], sampleObject.StringArray[i]);
+ }
+
+ // validate string list
+ for (int i = 0; i < sampleObject.StringList.Count; i++)
+ {
+
+ Assert.AreEqual(jsonObject.StringList[i], sampleObject.StringList[i]);
+ }
+
+ yield return null;
+ }
}
}
diff --git a/Tests/Runtime/BacktraceStackTraceTests.cs b/Tests/Runtime/BacktraceStackTraceTests.cs
index 185f5186..98a1d9af 100644
--- a/Tests/Runtime/BacktraceStackTraceTests.cs
+++ b/Tests/Runtime/BacktraceStackTraceTests.cs
@@ -22,24 +22,28 @@ public class BacktraceStackTraceTests
{
FunctionName ="UnityEngine.TestTools.TestRunner.TestEnumeratorWrapper.GetEnumerator(NUnit.Framework.Internal.ITestExecutionContext context)",
Line = 31,
+ FileName = "TestEnumeratorWrapper.cs",
Library = "C:/ buildslave / unity / build / Extensions / TestRunner / UnityEngine.TestRunner / TestRunner / TestEnumeratorWrapper.cs"
},
new SampleStackFrame()
{
FunctionName ="UnityEngine.TestTools.EnumerableTestMethodCommand +< ExecuteEnumerable > c__Iterator0.MoveNext()",
Line = 112,
+ FileName = "EnumerableTestMethodCommand.cs",
Library = "C:/ buildslave / unity / build / Extensions / TestRunner / UnityEngine.TestRunner / NUnitExtensions / Commands / EnumerableTestMethodCommand.cs"
},
new SampleStackFrame()
{
FunctionName = "UnityEngine.TestTools.EnumerableSetUpTearDownCommand +< ExecuteEnumerable > c__Iterator0.MoveNext()",
Line = 71,
+ FileName = "EnumerableTestMethodCommand.cs",
Library = "C:/ buildslave / unity / build / Extensions / TestRunner / UnityEngine.TestRunner / NUnitExtensions / Commands / EnumerableTestMethodCommand.cs"
},
new SampleStackFrame()
{
FunctionName = "UnityEngine.TestRunner.NUnitExtensions.Runner.UnityLogCheckDelegatingCommand +< ExecuteEnumerable > c__Iterator0.MoveNext()",
Line = 67,
+ FileName = "UnityLogCheckDelegatingCommand.cs",
Library = "C:/ buildslave / unity / build / Extensions / TestRunner / UnityEngine.TestRunner / NUnitExtensions / Runner / UnityLogCheckDelegatingCommand.cs"
},
};
@@ -50,42 +54,49 @@ public class BacktraceStackTraceTests
{
FunctionName ="Startup.GetRandomFileStream ()",
Line = 104,
+ FileName = "Startup.cs",
Library = "Assets/Startup.cs"
},
new SampleStackFrame()
{
FunctionName ="Startup.GetRandomFile ()",
Line = 99,
+ FileName = "Startup.cs",
Library = "Assets/Startup.cs"
},
new SampleStackFrame()
{
FunctionName ="Startup.ReadRandomFile ()",
Line = 94,
+ FileName = "Startup.cs",
Library = "Assets/Startup.cs"
},
new SampleStackFrame()
{
FunctionName ="Startup.DoSomethingDifferent ()",
Line = 89,
+ FileName = "Startup.cs",
Library = "Assets/Startup.cs"
},
new SampleStackFrame()
{
FunctionName ="Startup.DoSomethingElse ()",
Line = 84,
+ FileName = "Startup.cs",
Library = "Assets/Startup.cs"
},
new SampleStackFrame()
{
FunctionName ="Startup.DoSomething ()",
Line = 80,
+ FileName = "Startup.cs",
Library = "Assets/Startup.cs"
},
new SampleStackFrame()
{
FunctionName ="Startup.Update ()",
Line = 116,
+ FileName = "Startup.cs",
Library = "Assets/Startup.cs"
}
};
@@ -97,6 +108,7 @@ public class BacktraceStackTraceTests
{
Type = StackTraceType.Android,
FunctionName ="java.lang.Thread.sleep",
+ FileName= "Thread.java",
Library = "NativeMethod"
},
new SampleStackFrame()
@@ -104,6 +116,7 @@ public class BacktraceStackTraceTests
Type = StackTraceType.Android,
FunctionName ="java.lang.Thread.sleep",
Line = 440,
+ FileName = "Thread.java",
Library = "Thread.java"
},
new SampleStackFrame()
@@ -111,6 +124,7 @@ public class BacktraceStackTraceTests
Type = StackTraceType.Android,
FunctionName ="java.lang.Thread.sleep",
Line = 356,
+ FileName = "Thread.java",
Library = "Thread.java"
},
new SampleStackFrame()
@@ -118,6 +132,7 @@ public class BacktraceStackTraceTests
Type = StackTraceType.Android,
FunctionName ="backtrace.io.backtrace_unity_android_plugin.BacktraceCrashHelper$1.run",
Line = 16,
+ FileName = "BacktraceCrashHelper.java",
Library = "BacktraceCrashHelper.java"
},
new SampleStackFrame()
@@ -125,6 +140,7 @@ public class BacktraceStackTraceTests
Type = StackTraceType.Android,
FunctionName ="android.os.Handler.handleCallback",
Line = 883,
+ FileName = "Handler.java",
Library = "Handler.java"
},
new SampleStackFrame()
@@ -132,6 +148,7 @@ public class BacktraceStackTraceTests
Type = StackTraceType.Android,
FunctionName ="android.os.Handler.dispatchMessage",
Line = 100,
+ FileName = "Handler.java",
Library = "Handler.java"
},
new SampleStackFrame()
@@ -139,6 +156,7 @@ public class BacktraceStackTraceTests
Type = StackTraceType.Android,
FunctionName ="com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run",
Line = 493,
+ FileName = "RuntimeInit.java",
Library = "RuntimeInit.java"
}
};
@@ -156,6 +174,7 @@ public class BacktraceStackTraceTests
{
Type = StackTraceType.Android,
FunctionName ="java.lang.Thread.sleep",
+ FileName= "Thread.java",
Library = "NativeMethod"
},
// android source code stack frame
@@ -164,6 +183,7 @@ public class BacktraceStackTraceTests
Type = StackTraceType.Android,
FunctionName ="backtrace.io.backtrace_unity_android_plugin.BacktraceCrashHelper.InternalCall",
Library = "BacktraceCrashHelper.java",
+ FileName = "BacktraceCrashHelper.java",
Line = 31
},
new SampleStackFrame() {
@@ -176,18 +196,21 @@ public class BacktraceStackTraceTests
Type = StackTraceType.Android,
FunctionName = "com.unity3d.player.UnityPlayer.access$300",
Line = 0,
+ FileName = "UnityPlayer.java",
Library = "Unknown Source"
},
// csharp layer
new SampleStackFrame() {
FunctionName = "UnityEngine.AndroidJNISafe.CheckException ()",
Line = 24,
+ FileName = "AndroidJNISafe.cs",
Library = "/Users/builduser/buildslave/unity/build/Modules/AndroidJNI/AndroidJNISafe.cs"
},
// csharp layer with arguments
new SampleStackFrame() {
FunctionName = "UnityEngine.AndroidJavaObject.CallStatic (System.String methodName, System.Object[] args)",
Line = 252,
+ FileName = "AndroidJava.cs",
Library = "/Users/builduser/buildslave/unity/build/Modules/AndroidJNI/AndroidJava.cs"
}
};
@@ -201,6 +224,7 @@ public class BacktraceStackTraceTests
Line = 0,
Library = "Unity",
Address = "0x00007FF6661B40EC",
+ FileName = "StackWalker",
FunctionName = "StackWalker::GetCurrentCallstack"
},
new SampleStackFrame()
@@ -228,6 +252,7 @@ public class BacktraceStackTraceTests
Line = 0,
Library = "Mono JIT Code",
Address = "0x00000266BD679AEB",
+ FileName = "DebugLogHandler",
FunctionName = "UnityEngine.DebugLogHandler:Internal_Log (UnityEngine.LogType,UnityEngine.LogOption,string,UnityEngine.Object)"
},
new SampleStackFrame()
@@ -235,6 +260,7 @@ public class BacktraceStackTraceTests
Type = StackTraceType.Native,
StackFrame = "0x00000266BD67295B (Mono JIT Code) [firstSceneButtons.cs:41] firstSceneButtons:Start ()",
Line = 41,
+ FileName = "firstSceneButtons.cs",
Library = "firstSceneButtons.cs",
Address = "0x00000266BD67295B",
FunctionName = "firstSceneButtons:Start ()"
@@ -253,6 +279,7 @@ public class BacktraceStackTraceTests
Type = StackTraceType.Native,
StackFrame = "0x00007FFFEEB8CBB0 (mono-2.0-bdwgc) [mini-runtime.c:2809] mono_jit_runtime_invoke",
Line = 2809,
+ FileName = "mini-runtime.c",
Library = "mini-runtime.c",
Address = "0x00007FFFEEB8CBB0",
FunctionName = "mono_jit_runtime_invoke"
@@ -313,6 +340,7 @@ public void TestStackTraceCreation_AndroidException_ValidStackTraceObject()
Assert.AreEqual(anrStackFrame.FunctionName, backtraceStackFrame.FunctionName);
Assert.AreEqual(anrStackFrame.Line, backtraceStackFrame.Line);
Assert.AreEqual(anrStackFrame.Library, backtraceStackFrame.Library);
+ Assert.AreEqual(anrStackFrame.FileName, backtraceStackFrame.FileName);
}
}
@@ -337,6 +365,7 @@ public void TestStackTraceCreation_AndroidMixModeCallStack_ValidStackTraceObject
{
Assert.AreEqual(mixModeCallStack.FunctionName, backtraceStackFrame.FunctionName);
Assert.AreEqual(mixModeCallStack.Line, backtraceStackFrame.Line);
+ Assert.AreEqual(mixModeCallStack.FileName, backtraceStackFrame.FileName);
Assert.AreEqual(mixModeCallStack.Library, backtraceStackFrame.Library);
}
}
@@ -363,6 +392,7 @@ public void TestStackTraceCreation_UnityEngineException_ValidStackTraceObject()
Assert.AreEqual(realStackFrame.FunctionName, backtraceStackFrame.FunctionName);
Assert.AreEqual(realStackFrame.Line, backtraceStackFrame.Line);
Assert.AreEqual(realStackFrame.Library, backtraceStackFrame.Library);
+ Assert.AreEqual(realStackFrame.FileName, backtraceStackFrame.FileName);
}
// -1 because we include header in stack trace
Assert.AreEqual(data.Count - startIndex, backtraceStackTrace.StackFrames.Count);
@@ -436,6 +466,7 @@ public void TestNativeStackTraceParser_ParseNativeStackTrace_ShouldSetCorrectSta
Assert.AreEqual(expectedStackFrame.Library, backtraceStackFrame.Library);
Assert.AreEqual(expectedStackFrame.Line, backtraceStackFrame.Line);
Assert.AreEqual(expectedStackFrame.FunctionName, backtraceStackFrame.FunctionName);
+ Assert.AreEqual(expectedStackFrame.FileName, backtraceStackFrame.FileName);
}
}
@@ -526,6 +557,7 @@ internal class SampleStackFrame
public StackTraceType Type = StackTraceType.Default;
public string Custom { get; set; }
public string StackFrame { get; set; }
+ public string FileName { get; set; } = string.Empty;
public string FunctionName { get; set; }
public string Library { get; set; }
public int Line { get; set; }
diff --git a/Tests/Runtime/RateLimit/RateLimitTests.cs b/Tests/Runtime/RateLimit/RateLimitTests.cs
index bb72f535..67463388 100644
--- a/Tests/Runtime/RateLimit/RateLimitTests.cs
+++ b/Tests/Runtime/RateLimit/RateLimitTests.cs
@@ -63,7 +63,6 @@ public void TestReportLimitFromMultipleThreads_ShouldDeclineReportAfterLimitHit_
var result = reportLimitWatcher.WatchReport(new DateTime().Timestamp());
if (result)
{
- UnityEngine.Debug.Log($"Thread {i} - added report");
acceptedReports++;
}
else
diff --git a/Tests/Runtime/Serialization/Mocks/SampleObject.cs b/Tests/Runtime/Serialization/Mocks/SampleObject.cs
index 8d40e222..930a9418 100644
--- a/Tests/Runtime/Serialization/Mocks/SampleObject.cs
+++ b/Tests/Runtime/Serialization/Mocks/SampleObject.cs
@@ -1,10 +1,18 @@
-using System.Collections.Generic;
+using System;
+using System.Collections.Generic;
namespace Backtrace.Unity.Tests.Runtime
{
+ [Serializable]
+ internal class BaseJObject
+ {
+ public SampleObject InnerObject;
+ }
+ [Serializable]
internal class SampleObject
{
public string AgentName;
+ public string TestString;
public bool Active;
public int IntNumber;
public float FloatNumber;
diff --git a/Tests/Runtime/Serialization/SerializationTests.cs b/Tests/Runtime/Serialization/SerializationTests.cs
index 4bf85d60..48a5bb8b 100644
--- a/Tests/Runtime/Serialization/SerializationTests.cs
+++ b/Tests/Runtime/Serialization/SerializationTests.cs
@@ -44,7 +44,8 @@ public IEnumerator TestDataSerialization_ReportWithCustomAttribtues_ShouldGenera
var json = data.ToJson();
foreach (var keyValuePair in attributes)
{
- Assert.IsTrue(json.Contains(string.Format("\"{0}\": \"{1}\"", keyValuePair.Key, keyValuePair.Value)));
+ var value = string.Format("\"{0}\":{1}", keyValuePair.Key, string.IsNullOrEmpty(keyValuePair.Value) ? "null" : string.Format("\"{0}\"", keyValuePair.Value));
+ Assert.IsTrue(json.Contains(value));
}
diff --git a/Tests/Runtime/SourceCode/LogManagerTests.cs b/Tests/Runtime/SourceCode/LogManagerTests.cs
index f8158ca3..e754005e 100644
--- a/Tests/Runtime/SourceCode/LogManagerTests.cs
+++ b/Tests/Runtime/SourceCode/LogManagerTests.cs
@@ -51,7 +51,9 @@ public void TestMessageQueue_AddMultipleLosgToEnabledManager_ShouldEnqueueLimitM
logManager.Enqueue(enqueuedMessage, string.Empty, LogType.Log);
Assert.AreEqual(expectedNumberOfMessages, logManager.Size);
- Assert.AreEqual(enqueuedMessage, logManager.LogQueue.First().Message);
+ // validate if log ends with enqueueMessage to validate if message is there,
+ // without checking other message content (date, log type etc..)
+ Assert.IsTrue(logManager.LogQueue.First().EndsWith(enqueuedMessage));
}
diff --git a/Tests/Runtime/SourceCode/SourceCodeFlowWithLogManagerTests.cs b/Tests/Runtime/SourceCode/SourceCodeFlowWithLogManagerTests.cs
index 082f66f8..00971788 100644
--- a/Tests/Runtime/SourceCode/SourceCodeFlowWithLogManagerTests.cs
+++ b/Tests/Runtime/SourceCode/SourceCodeFlowWithLogManagerTests.cs
@@ -42,7 +42,7 @@ public IEnumerator TestSourceCodeAssignment_EnabledLogManagerAndSendExceptionRep
Assert.IsNotNull(lastData.SourceCode);
var threadName = lastData.ThreadData.MainThread;
- Assert.AreEqual(lastData.SourceCode.Id, lastData.ThreadData.ThreadInformations[threadName].Stack.First().SourceCode);
+ Assert.AreEqual(BacktraceSourceCode.SOURCE_CODE_PROPERTY, lastData.ThreadData.ThreadInformations[threadName].Stack.First().SourceCode);
}
@@ -62,7 +62,7 @@ public IEnumerator TestSourceCodeAssignment_EnabledLogManagerAndSendMessageRepor
Assert.IsNotNull(lastData.SourceCode);
var threadName = lastData.ThreadData.MainThread;
- Assert.AreEqual(lastData.SourceCode.Id, lastData.ThreadData.ThreadInformations[threadName].Stack.First().SourceCode);
+ Assert.AreEqual(BacktraceSourceCode.SOURCE_CODE_PROPERTY, lastData.ThreadData.ThreadInformations[threadName].Stack.First().SourceCode);
}
@@ -82,7 +82,7 @@ public IEnumerator TestSourceCodeAssignment_EnabledLogManagerAndSendUnhandledExc
Assert.IsNotNull(lastData.SourceCode);
var threadName = lastData.ThreadData.MainThread;
- Assert.AreEqual(lastData.SourceCode.Id, lastData.ThreadData.ThreadInformations[threadName].Stack.First().SourceCode);
+ Assert.AreEqual(BacktraceSourceCode.SOURCE_CODE_PROPERTY, lastData.ThreadData.ThreadInformations[threadName].Stack.First().SourceCode);
}
[UnityTest]
@@ -100,7 +100,7 @@ public IEnumerator TestSourceCodeAssignment_EnabledLogManagerAndSendUnhandledErr
Assert.IsNotNull(lastData.SourceCode);
var threadName = lastData.ThreadData.MainThread;
- Assert.AreEqual(lastData.SourceCode.Id, lastData.ThreadData.ThreadInformations[threadName].Stack.First().SourceCode);
+ Assert.AreEqual(BacktraceSourceCode.SOURCE_CODE_PROPERTY, lastData.ThreadData.ThreadInformations[threadName].Stack.First().SourceCode);
}
[UnityTest]
diff --git a/Tests/Runtime/SourceCode/SourceCodeFlowWithoutLogManagerTests.cs b/Tests/Runtime/SourceCode/SourceCodeFlowWithoutLogManagerTests.cs
index bf88a948..c0b7c4be 100644
--- a/Tests/Runtime/SourceCode/SourceCodeFlowWithoutLogManagerTests.cs
+++ b/Tests/Runtime/SourceCode/SourceCodeFlowWithoutLogManagerTests.cs
@@ -3,7 +3,6 @@
using System;
using System.Collections;
using System.Linq;
-using UnityEditor.PackageManager;
using UnityEngine;
using UnityEngine.TestTools;
@@ -36,7 +35,7 @@ public IEnumerator TestSourceCodeAssignment_DisabledLogManagerAndSendExceptionRe
Assert.IsNotNull(lastData.SourceCode);
var threadName = lastData.ThreadData.MainThread;
- Assert.AreEqual(lastData.SourceCode.Id, lastData.ThreadData.ThreadInformations[threadName].Stack.First().SourceCode);
+ Assert.AreEqual(BacktraceSourceCode.SOURCE_CODE_PROPERTY, lastData.ThreadData.ThreadInformations[threadName].Stack.First().SourceCode);
return lastData;
};
BacktraceClient.Send(new Exception("foo"));
@@ -56,7 +55,7 @@ public IEnumerator TestSourceCodeAssignment_DisabledLogManagerAndSendMessageRepo
Assert.IsNotNull(lastData.SourceCode);
var threadName = lastData.ThreadData.MainThread;
- Assert.AreEqual(lastData.SourceCode.Id, lastData.ThreadData.ThreadInformations[threadName].Stack.First().SourceCode);
+ Assert.AreEqual(BacktraceSourceCode.SOURCE_CODE_PROPERTY, lastData.ThreadData.ThreadInformations[threadName].Stack.First().SourceCode);
return lastData;
};
BacktraceClient.Send("foo");
@@ -75,7 +74,7 @@ public IEnumerator TestSourceCodeAssignment_DisabledLogManagerAndSendUnhandledEx
Assert.IsNotNull(lastData.SourceCode);
var threadName = lastData.ThreadData.MainThread;
- Assert.AreEqual(lastData.SourceCode.Id, lastData.ThreadData.ThreadInformations[threadName].Stack.First().SourceCode);
+ Assert.AreEqual(BacktraceSourceCode.SOURCE_CODE_PROPERTY, lastData.ThreadData.ThreadInformations[threadName].Stack.First().SourceCode);
return lastData;
};
@@ -96,13 +95,13 @@ public IEnumerator TestSourceCodeAssignment_DisabledLogManagerAndSendUnhandledEr
Assert.IsNotNull(lastData.SourceCode);
var threadName = lastData.ThreadData.MainThread;
- Assert.AreEqual(lastData.SourceCode.Id, lastData.ThreadData.ThreadInformations[threadName].Stack.First().SourceCode);
+ Assert.AreEqual(BacktraceSourceCode.SOURCE_CODE_PROPERTY, lastData.ThreadData.ThreadInformations[threadName].Stack.First().SourceCode);
return lastData;
};
BacktraceClient.HandleUnityMessage("foo", string.Empty, LogType.Error);
yield return new WaitForEndOfFrame();
- Assert.IsTrue(invoked);
+ Assert.IsTrue(invoked);
}
[UnityTest]
diff --git a/iOS/libBacktrace-Unity-Cocoa.a b/iOS/libBacktrace-Unity-Cocoa.a
index 5566df89..9220e9c4 100644
Binary files a/iOS/libBacktrace-Unity-Cocoa.a and b/iOS/libBacktrace-Unity-Cocoa.a differ
diff --git a/package.json b/package.json
index 3230c52a..1c5dc397 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"name": "io.backtrace.unity",
"displayName": "Backtrace",
- "version": "3.2.6",
+ "version": "3.3.0",
"unity": "2017.1",
"description": "Backtrace's integration with Unity games allows customers to capture and report handled and unhandled Unity exceptions to their Backtrace instance, instantly offering the ability to prioritize and debug software errors.",
"keywords": [