diff --git a/.gitignore b/.gitignore index 0210746b..94c0825f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,13 +1,30 @@ -[Ll]ibrary/ -[Tt]emp/ -[Oo]bj/ -[Bb]uild/ -[Bb]uilds/ -Assets/AssetStoreTools* +# This .gitignore file should be placed at the root of your Unity project directory +# +# Get latest from https://github.com/github/gitignore/blob/master/Unity.gitignore +# +/[Ll]ibrary/ +/[Tt]emp/ +/[Oo]bj/ +/[Bb]uild/ +/[Bb]uilds/ +/[Ll]ogs/ +/[Mm]emoryCaptures/ + +# Asset meta data should only be ignored when the corresponding asset is also ignored +!/[Aa]ssets/**/*.meta + +# Uncomment this line if you wish to ignore the asset store tools plugin +# /[Aa]ssets/AssetStoreTools* + +# Autogenerated Jetbrains Rider plugin +/[Aa]ssets/Plugins/Editor/JetBrains* # Visual Studio cache directory .vs/ +# Gradle cache directory +.gradle/ + # Autogenerated VS/MD/Consulo solution and project files ExportedObj/ .consulo/ @@ -22,15 +39,21 @@ ExportedObj/ *.booproj *.svd *.pdb +*.mdb *.opendb +*.VC.db # Unity3D generated meta files *.pidb.meta *.pdb.meta +*.mdb.meta -# Unity3D Generated File On Crash Reports +# Unity3D generated file on crash reports sysinfo.txt # Builds *.apk *.unitypackage + +# Crashlytics generated file +crashlytics-build.properties diff --git a/CHANGELOG.md b/CHANGELOG.md index be75207e..d0d8afc1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Backtrace Unity Release Notes +## Version 2.0.0 +- Backtrace-Unity plugin will set `"Destroy object on new scene"` by default to false. +- Backtrace stack trace improvements, +- `BacktraceDatabase` retry method now respect correctly `BacktraceDatabase` `retryInterval` property, +- New `Backtrace Configuration` won't override existing `Backtrace Configuration` in configuration directory. +- Backtrace-Unity plugin tests now won't override some files in Backtrace-Database unit tests, +- Backtrace-Unity plugin now allows you to setup client side deduplication rules via `Fingerprint`. By using this field you can limit reporting of an error that occurs many times over a few frames. +- Backtrace report limit watcher feature now will validate limits before BacktraceReport creation. +- `BacktraceClient` and `BacktraceDatabase` now expose `Reload` method. You can use this method do dynamically change `BacktraceClient`/`BacktraceDatabase` configurations. + ## Version 1.1.5 - 09.01.2019 - Added support to DontDestroyOnLoad property. Right now users might use this property to store `BacktraceClient`/`BacktraceDatabase` instances between all game scenes. - Added more attributes to `BacktraceReport` object, diff --git a/Editor/Menu/BacktraceClientConfigurationEditor.cs b/Editor/Menu/BacktraceClientConfigurationEditor.cs index 4c6b6372..d2f072f7 100644 --- a/Editor/Menu/BacktraceClientConfigurationEditor.cs +++ b/Editor/Menu/BacktraceClientConfigurationEditor.cs @@ -1,5 +1,6 @@ #if UNITY_EDITOR using Backtrace.Unity.Model; +using Backtrace.Unity.Types; using System.IO; using UnityEditor; using UnityEngine; @@ -13,7 +14,9 @@ public class BacktraceClientConfigurationEditor : UnityEditor.Editor public const string LABEL_REPORT_PER_MIN = "Reports per minute"; public const string LABEL_HANDLE_UNHANDLED_EXCEPTION = "Handle unhandled exceptions"; public const string LABEL_IGNORE_SSL_VALIDATION = "Ignore SSL validation"; - public const string LABEL_DONT_DESTROY_BACKTRACE_ON_SCENE_LOAD = "Don't destroy Backtrace client on scene load"; + public const string LABEL_DEDUPLICATION_RULES = "Deduplication rules"; + public const string LABEL_DONT_DESTROY_BACKTRACE_ON_SCENE_LOAD = "Don't destroy on scene load"; + @@ -29,10 +32,12 @@ public override void OnInspectorGUI() { EditorGUILayout.HelpBox("Detected different pattern of url. Please make sure its a valid Backtrace url!", MessageType.Warning); } + + settings.DestroyOnLoad = EditorGUILayout.Toggle(LABEL_DONT_DESTROY_BACKTRACE_ON_SCENE_LOAD, settings.DestroyOnLoad); settings.ReportPerMin = EditorGUILayout.IntField(LABEL_REPORT_PER_MIN, settings.ReportPerMin); settings.HandleUnhandledExceptions = EditorGUILayout.Toggle(LABEL_HANDLE_UNHANDLED_EXCEPTION, settings.HandleUnhandledExceptions); settings.IgnoreSslValidation = EditorGUILayout.Toggle(LABEL_IGNORE_SSL_VALIDATION, settings.IgnoreSslValidation); - settings.DestroyOnLoad = EditorGUILayout.Toggle(LABEL_DONT_DESTROY_BACKTRACE_ON_SCENE_LOAD, settings.DestroyOnLoad); + settings.DeduplicationStrategy = (DeduplicationStrategy)EditorGUILayout.EnumPopup(LABEL_DEDUPLICATION_RULES, settings.DeduplicationStrategy); } } diff --git a/Editor/Menu/BacktraceClientEditor.cs b/Editor/Menu/BacktraceClientEditor.cs index e0c4108a..08d28257 100644 --- a/Editor/Menu/BacktraceClientEditor.cs +++ b/Editor/Menu/BacktraceClientEditor.cs @@ -19,7 +19,7 @@ public override void OnInspectorGUI() false); if (component.Configuration != null) { - BacktraceConfigurationEditor editor = (BacktraceConfigurationEditor)CreateEditor(component.Configuration); + var editor = (BacktraceConfigurationEditor)CreateEditor(component.Configuration); editor.OnInspectorGUI(); if (component.Configuration.Enabled && component.gameObject.GetComponent() == null) { diff --git a/Editor/Menu/BacktraceConfigurationEditor.cs b/Editor/Menu/BacktraceConfigurationEditor.cs index 2524b4f7..9eb8dd6f 100644 --- a/Editor/Menu/BacktraceConfigurationEditor.cs +++ b/Editor/Menu/BacktraceConfigurationEditor.cs @@ -13,6 +13,8 @@ public class BacktraceConfigurationEditor : UnityEditor.Editor public const string LABEL_HANDLE_UNHANDLED_EXCEPTION = "Handle unhandled exceptions"; public const string LABEL_ENABLE_DATABASE = "Enable Database"; public const string LABEL_IGNORE_SSL_VALIDATION = "Ignore SSL validation"; + public const string LABEL_DEDUPLICATION_RULES = "Deduplication rules"; + public const string LABEL_DESTROY_CLIENT_ON_SCENE_LOAD = "Destroy client on new scene load"; public const string LABEL_PATH = "Backtrace database path"; @@ -44,11 +46,15 @@ public override void OnInspectorGUI() SerializedProperty sslValidation = serializedObject.FindProperty("IgnoreSslValidation"); EditorGUILayout.PropertyField(sslValidation, new GUIContent(LABEL_IGNORE_SSL_VALIDATION)); + + SerializedProperty deduplicationStrategy = serializedObject.FindProperty("DeduplicationStrategy"); + EditorGUILayout.PropertyField(deduplicationStrategy, new GUIContent(LABEL_DEDUPLICATION_RULES)); + SerializedProperty destroyOnLoad = serializedObject.FindProperty("DestroyOnLoad"); EditorGUILayout.PropertyField(destroyOnLoad, new GUIContent(LABEL_DESTROY_CLIENT_ON_SCENE_LOAD)); SerializedProperty enabled = serializedObject.FindProperty("Enabled"); - EditorGUILayout.PropertyField(enabled, new GUIContent(LABEL_ENABLE_DATABASE)); + EditorGUILayout.PropertyField(enabled, new GUIContent(LABEL_ENABLE_DATABASE)); if (enabled.boolValue) { diff --git a/Editor/Menu/BacktraceMenu.cs b/Editor/Menu/BacktraceMenu.cs index 09441674..3e042976 100644 --- a/Editor/Menu/BacktraceMenu.cs +++ b/Editor/Menu/BacktraceMenu.cs @@ -1,14 +1,17 @@ #if UNITY_EDITOR +using Backtrace.Unity.Model; using System.IO; +using System.Linq; using UnityEditor; using UnityEngine; -using Backtrace.Unity.Model; namespace Backtrace.Unity.Editor { public class BacktraceMenu : MonoBehaviour { - public const string DEFAULT_CLIENT_CONFIGURATION_NAME = "Backtrace Configuration.asset"; + public const string DEFAULT_CONFIGURATION_NAME = "Backtrace Configuration"; + public const string DEFAULT_EXTENSION_NAME = ".asset"; + public const string DEFAULT_CLIENT_CONFIGURATION_NAME = DEFAULT_CONFIGURATION_NAME + DEFAULT_EXTENSION_NAME; [MenuItem("Assets/Backtrace/Configuration", false, 1)] public static void CreateClientConfigurationFile() @@ -19,7 +22,7 @@ public static void CreateClientConfigurationFile() private static void CreateAsset(string fileName) where T : ScriptableObject { T asset = ScriptableObject.CreateInstance(); - string currentProjectPath = AssetDatabase.GetAssetPath(Selection.activeObject); + var currentProjectPath = AssetDatabase.GetAssetPath(Selection.activeObject); if (string.IsNullOrEmpty(currentProjectPath)) { currentProjectPath = "Assets"; @@ -28,11 +31,32 @@ private static void CreateAsset(string fileName) where T : ScriptableObject { currentProjectPath = Path.GetDirectoryName(currentProjectPath); } - AssetDatabase.CreateAsset(asset, "Assets/" + fileName); - AssetDatabase.SaveAssets(); - var destinationPath = Path.Combine(currentProjectPath, fileName); - AssetDatabase.MoveAsset("Assets/" + fileName, destinationPath); + if (File.Exists(destinationPath)) + { + var files = Directory.GetFiles(currentProjectPath); + var lastFileIndex = files + .Where(n => + Path.GetFileNameWithoutExtension(n).StartsWith(DEFAULT_CONFIGURATION_NAME) && + Path.GetExtension(n) == DEFAULT_EXTENSION_NAME) + .Select(n => + { + int startIndex = n.IndexOf('(') + 1; + int endIndex = n.IndexOf(')'); + if (startIndex != 0 && endIndex != -1 && int.TryParse(n.Substring(startIndex, endIndex - startIndex), out int result)) + { + return result; + } + return 0; + }) + .DefaultIfEmpty().Max(); + + lastFileIndex++; + destinationPath = Path.Combine(currentProjectPath, $"{DEFAULT_CONFIGURATION_NAME}({lastFileIndex}){DEFAULT_EXTENSION_NAME}"); + } + Debug.Log($"Generating new Backtrace configuration file available in path: {destinationPath}"); + AssetDatabase.CreateAsset(asset, destinationPath); + AssetDatabase.SaveAssets(); Selection.activeObject = asset; } } diff --git a/Editor/Tests/BacktraceBaseTest.cs b/Editor/Tests/BacktraceBaseTest.cs new file mode 100644 index 00000000..fed76743 --- /dev/null +++ b/Editor/Tests/BacktraceBaseTest.cs @@ -0,0 +1,41 @@ +using UnityEngine; +using System.Collections; +using Backtrace.Unity; +using Backtrace.Unity.Model; +using NUnit.Framework; + +public class BacktraceBaseTest : MonoBehaviour +{ + protected GameObject GameObject; + protected BacktraceClient BacktraceClient; + protected void BeforeSetup() + { + Debug.unityLogger.logEnabled = false; + GameObject = new GameObject(); + GameObject.SetActive(false); + BacktraceClient = GameObject.AddComponent(); + } + + protected void AfterSetup(bool refresh = true) + { + if (refresh) + { + BacktraceClient.Refresh(); + } + GameObject.SetActive(true); + } + + protected BacktraceConfiguration GetBasicConfiguration() + { + var configuration = ScriptableObject.CreateInstance(); + configuration.ServerUrl = "https://submit.backtrace.io/test/token/json"; + configuration.DestroyOnLoad = true; + return configuration; + } + + [TearDown] + public void Cleanup() + { + DestroyImmediate(GameObject); + } +} diff --git a/Editor/Tests/BacktraceBaseTest.cs.meta b/Editor/Tests/BacktraceBaseTest.cs.meta new file mode 100644 index 00000000..84ad68fd --- /dev/null +++ b/Editor/Tests/BacktraceBaseTest.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 8adcb9ee0d4b3414c888a23ebd851db5 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/Tests/BacktraceClientTests.cs b/Editor/Tests/BacktraceClientTests.cs index 75f71e7d..2a1d21dc 100644 --- a/Editor/Tests/BacktraceClientTests.cs +++ b/Editor/Tests/BacktraceClientTests.cs @@ -8,59 +8,56 @@ namespace Tests { - public class BacktraceClientTests + public class BacktraceClientTests: BacktraceBaseTest { - private BacktraceClient client; - [SetUp] public void Setup() { - var gameObject = new GameObject(); - gameObject.SetActive(false); - client = gameObject.AddComponent(); - client.Configuration = null; - gameObject.SetActive(true); + BeforeSetup(); + AfterSetup(false); } - + [UnityTest] public IEnumerator TestClientCreation_ValidBacktraceConfiguration_ValidClientCreation() { var clientConfiguration = GetValidClientConfiguration(); - client.Configuration = clientConfiguration; - client.Refresh(); - Assert.IsTrue(client.Enabled); + BacktraceClient.Configuration = clientConfiguration; + BacktraceClient.Refresh(); + Assert.IsTrue(BacktraceClient.Enabled); yield return null; } - + [UnityTest] public IEnumerator TestClientCreation_EmptyConfiguration_DisabledClientCreation() { - Assert.IsFalse(client.Enabled); + Assert.IsFalse(BacktraceClient.Enabled); yield return null; } [UnityTest] public IEnumerator TestClientEvents_EmptyConfiguration_ShouldntThrowExceptionForDisabledClient() { - Assert.IsFalse(client.Enabled); + Assert.IsFalse(BacktraceClient.Enabled); - client.HandleUnhandledExceptions(); - Assert.IsNull(client.OnServerError); - Assert.IsNull(client.OnServerResponse); - Assert.IsNull(client.BeforeSend); - Assert.IsNull(client.RequestHandler); - Assert.IsNull(client.OnUnhandledApplicationException); + BacktraceClient.HandleUnhandledExceptions(); + Assert.IsNull(BacktraceClient.OnServerError); + Assert.IsNull(BacktraceClient.OnServerResponse); + Assert.IsNull(BacktraceClient.BeforeSend); + Assert.IsNull(BacktraceClient.RequestHandler); + Assert.IsNull(BacktraceClient.OnUnhandledApplicationException); yield return null; } [UnityTest] public IEnumerator TestUnvailableEvents_EmptyConfiguration_ShouldntThrowException() { - client.OnServerError = (Exception e) => { }; - client.OnServerResponse = (BacktraceResult r) => { }; - client.BeforeSend = (BacktraceData d) => d; - client.OnUnhandledApplicationException = (Exception e) => { }; + BacktraceClient.Configuration = null; + BacktraceClient.Refresh(); + BacktraceClient.OnServerError = (Exception e) => { }; + BacktraceClient.OnServerResponse = (BacktraceResult r) => { }; + BacktraceClient.BeforeSend = (BacktraceData d) => d; + BacktraceClient.OnUnhandledApplicationException = (Exception e) => { }; yield return null; } @@ -68,7 +65,9 @@ public IEnumerator TestUnvailableEvents_EmptyConfiguration_ShouldntThrowExceptio [UnityTest] public IEnumerator TestSendEvent_DisabledApi_NotSendingEvent() { - Assert.DoesNotThrow(() => client.Send(new Exception("test exception"))); + BacktraceClient.Configuration = GetValidClientConfiguration(); + BacktraceClient.Refresh(); + Assert.DoesNotThrow(() => BacktraceClient.Send(new Exception("test exception"))); yield return null; } @@ -76,12 +75,14 @@ public IEnumerator TestSendEvent_DisabledApi_NotSendingEvent() public IEnumerator TestBeforeSendEvent_ValidConfiguration_EventTrigger() { var trigger = false; - client.BeforeSend = (BacktraceData d) => + BacktraceClient.Configuration = GetValidClientConfiguration(); + BacktraceClient.Refresh(); + BacktraceClient.BeforeSend = (BacktraceData backtraceData) => { trigger = true; - return d; + return backtraceData; }; - client.Send(new Exception("test exception")); + BacktraceClient.Send(new Exception("test exception")); Assert.IsTrue(trigger); yield return null; } @@ -90,30 +91,29 @@ public IEnumerator TestBeforeSendEvent_ValidConfiguration_EventTrigger() public IEnumerator TestSendingReport_ValidConfiguration_ValidSend() { var trigger = false; - client.Configuration = GetValidClientConfiguration(); - client.Refresh(); + BacktraceClient.Configuration = GetValidClientConfiguration(); + BacktraceClient.Refresh(); - client.RequestHandler = (string url, BacktraceData data) => - { + BacktraceClient.RequestHandler = (string url, BacktraceData data) => + { Assert.IsNotNull(data); Assert.IsFalse(string.IsNullOrEmpty(data.ToJson())); trigger = true; return new BacktraceResult(); }; - client.Send(new Exception("test exception")); + BacktraceClient.Send(new Exception("test exception")); Assert.IsTrue(trigger); yield return null; } private BacktraceConfiguration GetValidClientConfiguration() { - return new BacktraceConfiguration() + var configuration = GetBasicConfiguration(); + BacktraceClient.RequestHandler = (string url, BacktraceData backtraceData) => { - ServerUrl = "https://test.sp.backtrace.io:6097/", - //backtrace configuration require 64 characters - Token = "1234123412341234123412341234123412341234123412341234123412341234" + return new BacktraceResult(); }; + return configuration; } - } } diff --git a/Editor/Tests/BacktraceCredentialsTests.cs b/Editor/Tests/BacktraceCredentialsTests.cs index 3cc3a3ec..f4c19b1e 100644 --- a/Editor/Tests/BacktraceCredentialsTests.cs +++ b/Editor/Tests/BacktraceCredentialsTests.cs @@ -6,21 +6,6 @@ namespace Tests { internal class BacktraceCredentialsTests { - [TestCase("http://backtrace.sp.backtrace.io")] - [TestCase("http://backtrace.sp.backtrace.io:6098")] - [TestCase("http://backtrace.sp.backtrace.io:7777")] - [TestCase("http://backtrace.sp.backtrace.io:7777/")] - [TestCase("http://backtrace.sp.backtrace.io/")] - [Test(Author = "Konrad Dysput", Description = "Test valid submission url")] - public void GenerateSubmissionUrl_FromValidHostName_ValidSubmissionUrl(string test) - { - const string token = "1234"; - var credentials = new BacktraceCredentials(test, token); - - string expectedUrl = $"{credentials.BacktraceHostUri.AbsoluteUri}post?format=json&token={credentials.Token}"; - Assert.AreEqual(credentials.GetSubmissionUrl(), expectedUrl); - } - [TestCase("https://www.submit.backtrace.io")] [TestCase("http://www.submit.backtrace.io")] [TestCase("https://submit.backtrace.io")] diff --git a/Editor/Tests/BacktraceDatabaseTests.cs b/Editor/Tests/BacktraceDatabaseTests.cs index 13fd44c9..57a25008 100644 --- a/Editor/Tests/BacktraceDatabaseTests.cs +++ b/Editor/Tests/BacktraceDatabaseTests.cs @@ -7,23 +7,17 @@ namespace Tests { - public class BacktraceDatabaseTests + public class BacktraceDatabaseTests: BacktraceBaseTest { - private GameObject _gameObject; private BacktraceDatabase database; - private BacktraceClient client; [SetUp] public void Setup() { - _gameObject = new GameObject(); - _gameObject.SetActive(false); - client = _gameObject.AddComponent(); - client.Configuration = null; - database = _gameObject.AddComponent(); + BeforeSetup(); + database = GameObject.AddComponent(); database.Configuration = null; - - _gameObject.SetActive(true); + AfterSetup(false); } [UnityTest] @@ -37,20 +31,17 @@ public IEnumerator TestDbCreation_EmptyBacktraceConfiguration_ValidDbCreation() [UnityTest] public IEnumerator TestDbCreation_ValidConfiguration_EnabledDb() { - var configuration = new BacktraceConfiguration() - { - ServerUrl = "https://test.sp.backtrace.io:6097/", - //backtrace configuration require 64 characters - Token = "1234123412341234123412341234123412341234123412341234123412341234", - DatabasePath = Application.dataPath, - CreateDatabase = false, - AutoSendMode = false, - Enabled = true - }; + var configuration = GetBasicConfiguration(); + configuration.DatabasePath = Application.temporaryCachePath; + configuration.CreateDatabase = false; + configuration.AutoSendMode = false; + configuration.Enabled = true; + database.Configuration = configuration; database.Reload(); Assert.IsTrue(database.Enable); yield return null; } + } } diff --git a/Editor/Tests/BacktraceReportTests.cs b/Editor/Tests/BacktraceReportTests.cs index aa9f6d74..564df31c 100644 --- a/Editor/Tests/BacktraceReportTests.cs +++ b/Editor/Tests/BacktraceReportTests.cs @@ -11,11 +11,12 @@ namespace Tests { public class BacktraceReportTests { - private readonly Exception exception = new DivideByZeroException(); + private readonly Exception exception = new DivideByZeroException("fake exception message"); private readonly Dictionary reportAttributes = new Dictionary() { { "test_attribute", "test_attribute_value" }, - { "temporary_attribute", "temp" } + { "temporary_attribute", 123 }, + { "temporary_attribute_bool", true} }; private readonly List attachemnts = new List() { "path", "path2" }; @@ -56,13 +57,34 @@ public IEnumerator TestReportSerialization_SerializeValidReport_MessageReport() return TestSerialization(report); } + + [UnityTest] + public IEnumerator TestReportValues_ShouldAssignCorrectExceptionInformation_ExceptionReport() + { + var report = new BacktraceReport( + exception: exception, + attributes: reportAttributes, + attachmentPaths: attachemnts); + + Assert.AreEqual(exception.Message, report.Message); + Assert.AreEqual(exception.GetType().Name, report.Classifier); + Assert.AreEqual(attachemnts.Count(), report.AttachmentPaths.Count()); + Assert.AreEqual(reportAttributes["test_attribute"], report.Attributes["test_attribute"]); + Assert.AreEqual(reportAttributes["temporary_attribute"], report.Attributes["temporary_attribute"]); + Assert.AreEqual(reportAttributes["temporary_attribute_bool"], report.Attributes["temporary_attribute_bool"]); + + yield return null; + } + private IEnumerator TestSerialization(BacktraceReport report) { var json = report.ToJson(); var deserializedReport = BacktraceReport.Deserialize(json); foreach (var attribute in deserializedReport.Attributes) { - var attributeValue = (reportAttributes[attribute.Key] as string); + // ignore validating types - tests already validating it. + // here we want to be sure that we correctly assigned json data + var attributeValue = reportAttributes[attribute.Key].ToString(); Assert.AreEqual(attributeValue, attribute.Value); } Assert.IsTrue(attachemnts.SequenceEqual(deserializedReport.AttachmentPaths)); diff --git a/Editor/Tests/BacktraceStackTraceTests.cs b/Editor/Tests/BacktraceStackTraceTests.cs index ece98e61..70134ac3 100644 --- a/Editor/Tests/BacktraceStackTraceTests.cs +++ b/Editor/Tests/BacktraceStackTraceTests.cs @@ -13,8 +13,6 @@ namespace Tests { public class BacktraceStackTraceTests { - private BacktraceClient client; - private static readonly List _advancedStack = new List() { new SampleStackFrame(){ @@ -92,15 +90,7 @@ public class BacktraceStackTraceTests } }; - [SetUp] - public void Setup() - { - var gameObject = new GameObject(); - gameObject.SetActive(false); - client = gameObject.AddComponent(); - client.Configuration = null; - gameObject.SetActive(true); - } + [UnityTest] public IEnumerator TestReportStackTrace_StackTraceShouldBeTheSameLikeExceptionStackTrace_ShouldReturnCorrectStackTrace() diff --git a/Editor/Tests/ClientSendTests.cs b/Editor/Tests/ClientSendTests.cs index fa7e7a4a..cb2c8457 100644 --- a/Editor/Tests/ClientSendTests.cs +++ b/Editor/Tests/ClientSendTests.cs @@ -19,10 +19,9 @@ public void Setup() var gameObject = new GameObject(); gameObject.SetActive(false); client = gameObject.AddComponent(); - client.Configuration = new BacktraceConfiguration() - { - ServerUrl = "https://submit.backtrace.io/test/1234123412341234123412341234123412341234123412341234123412341234/json" - }; + client.Configuration = ScriptableObject.CreateInstance(); + client.Configuration.ServerUrl = "https://submit.backtrace.io/test/1234123412341234123412341234123412341234123412341234123412341234/json"; + client.Configuration.DestroyOnLoad = true; gameObject.SetActive(true); client.Refresh(); } diff --git a/Editor/Tests/DeduplicationTests.cs b/Editor/Tests/DeduplicationTests.cs new file mode 100644 index 00000000..4dde53ca --- /dev/null +++ b/Editor/Tests/DeduplicationTests.cs @@ -0,0 +1,108 @@ +using Backtrace.Unity; +using Backtrace.Unity.Model; +using Backtrace.Unity.Types; +using NUnit.Framework; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using UnityEngine; +using UnityEngine.TestTools; + +namespace Tests +{ + public class DeduplicaitonTests : BacktraceBaseTest + { + private BacktraceDatabaseMock _database; + + [SetUp] + public void Setup() + { + BeforeSetup(); + var configuration = GenerateDefaultConfiguration(); + BacktraceClient.Configuration = configuration; + _database = GameObject.AddComponent(); + _database.Configuration = configuration; + _database.Reload(); + AfterSetup(); + } + + /// + /// Generate specific backtrace configuration object for deduplication testing + /// + private BacktraceConfiguration GenerateDefaultConfiguration() + { + var configuration = GetBasicConfiguration(); + configuration.DatabasePath = Application.temporaryCachePath; + configuration.CreateDatabase = false; + configuration.AutoSendMode = false; + configuration.Enabled = true; + + return configuration; + } + + [UnityTest] + public IEnumerator TestDisabledDeduplicationStrategy_DeduplicationNone_ShouldntMergeReports() + { + _database.DeduplicationStrategy = DeduplicationStrategy.None; + _database.Clear(); + var report = new BacktraceReport(new Exception("Exception Message")); + + // validate total number of reports + // Count method should return all reports (include reports after deduplicaiton) + int totalNumberOfReports = 2; + for (int i = 0; i < totalNumberOfReports; i++) + { + _database.Add(report, new Dictionary(), MiniDumpType.None); + } + Assert.AreEqual(totalNumberOfReports, _database.Count()); + Assert.AreEqual(totalNumberOfReports, _database.Get().Count()); + yield return null; + } + + [TestCase(DeduplicationStrategy.Default)] + [TestCase(DeduplicationStrategy.Classifier)] + [TestCase(DeduplicationStrategy.Message)] + [TestCase(DeduplicationStrategy.Classifier | DeduplicationStrategy.Message)] + [TestCase(DeduplicationStrategy.Default | DeduplicationStrategy.Classifier | DeduplicationStrategy.Message)] + public void TestDeduplicationStrategy_TestDifferentStrategies_ReportShouldMerge(DeduplicationStrategy deduplicationStrategy) + { + _database.DeduplicationStrategy = deduplicationStrategy; + _database.Clear(); + var report = new BacktraceReport(new Exception("Exception Message")); + + // validate total number of reports + // Count method should return all reports (include reports after deduplicaiton) + int totalNumberOfReports = 2; + for (int i = 0; i < totalNumberOfReports; i++) + { + _database.Add(report, new Dictionary(), MiniDumpType.None); + } + Assert.AreEqual(totalNumberOfReports, _database.Count()); + var records = _database.Get(); + int expectedNumberOfReports = 1; + Assert.AreEqual(expectedNumberOfReports, records.Count()); + } + + //avoid testing default as a single parameter because default will analyse stack trace, which will be the same + // for both exceptions + [TestCase(DeduplicationStrategy.Classifier)] + [TestCase(DeduplicationStrategy.Message)] + [TestCase(DeduplicationStrategy.Classifier | DeduplicationStrategy.Message)] + [TestCase(DeduplicationStrategy.Classifier | DeduplicationStrategy.Default)] + [TestCase(DeduplicationStrategy.Default | DeduplicationStrategy.Message)] + public void TestDeduplicaiton_DifferentExceptions_ShouldGenerateDifferentHashForDifferentRerports(DeduplicationStrategy strategy) + { + var report1 = new BacktraceReport(new Exception("test")); + var report2 = new BacktraceReport(new ArgumentException("argument test")); + + var deduplicationStrategy1 = new DeduplicationModel(new BacktraceData(report1, null), strategy); + var deduplicationStrategy2 = new DeduplicationModel(new BacktraceData(report2, null), strategy); + + var sha1 = deduplicationStrategy1.GetSha(); + var sha2 = deduplicationStrategy2.GetSha(); + + Assert.AreNotEqual(sha1, sha2); + } + } +} \ No newline at end of file diff --git a/Editor/Tests/DeduplicationTests.cs.meta b/Editor/Tests/DeduplicationTests.cs.meta new file mode 100644 index 00000000..614e6236 --- /dev/null +++ b/Editor/Tests/DeduplicationTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 00e74762dcf2f4642874800367ad01d5 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/Tests/RateLimitTests.cs b/Editor/Tests/RateLimitTests.cs index 6f7155b2..3e25601a 100644 --- a/Editor/Tests/RateLimitTests.cs +++ b/Editor/Tests/RateLimitTests.cs @@ -1,60 +1,176 @@ using Backtrace.Unity; using Backtrace.Unity.Model; using NUnit.Framework; +using System; using System.Collections; using UnityEngine; using UnityEngine.TestTools; namespace Tests { - public class RateLimitTests + public class RateLimitTests: BacktraceBaseTest { - private BacktraceClient client; + private const int CLIENT_RATE_LIMIT = 3; [SetUp] public void Setup() { - var gameObject = new GameObject(); - gameObject.SetActive(false); - client = gameObject.AddComponent(); - client.Configuration = new BacktraceConfiguration() + BeforeSetup(); + BacktraceClient.Configuration = GetBasicConfiguration(); + BacktraceClient.SetClientReportLimit(CLIENT_RATE_LIMIT); + AfterSetup(); + } + [TestCase(5)] + [TestCase(10)] + [TestCase(20)] + public void TestReportLimit_ShouldntHitRateLimit_AllReportsShouldBeInBacktrace(int reportPerMin) + { + uint rateLimit = Convert.ToUInt32(reportPerMin); + BacktraceClient.SetClientReportLimit(rateLimit); + int maximumNumberOfRetries = 0; + BacktraceClient.RequestHandler = (string url, BacktraceData data) => + { + maximumNumberOfRetries++; + return new BacktraceResult(); + }; + int skippedReports = 0; + BacktraceClient.OnClientReportLimitReached = (BacktraceReport report) => + { + skippedReports++; + }; + for (int i = 0; i < rateLimit; i++) + { + BacktraceClient.Send("test"); + } + Assert.AreEqual(maximumNumberOfRetries, rateLimit); + Assert.AreEqual(0, skippedReports); + } + + [UnityTest] + public IEnumerator TestReportLimit_TestSendingMessage_SkippProcessingReports() + { + BacktraceClient.SetClientReportLimit(CLIENT_RATE_LIMIT); + int totalNumberOfReports = 5; + int maximumNumberOfRetries = 0; + BacktraceClient.RequestHandler = (string url, BacktraceData data) => + { + maximumNumberOfRetries++; + return new BacktraceResult(); + }; + int skippedReports = 0; + BacktraceClient.OnClientReportLimitReached = (BacktraceReport report) => + { + skippedReports++; + }; + + for (int i = 0; i < totalNumberOfReports; i++) + { + BacktraceClient.Send("test"); + } + Assert.AreEqual(totalNumberOfReports, maximumNumberOfRetries + skippedReports); + Assert.AreEqual(maximumNumberOfRetries, CLIENT_RATE_LIMIT); + Assert.AreEqual(totalNumberOfReports - CLIENT_RATE_LIMIT, skippedReports); + yield return null; + } + + [UnityTest] + public IEnumerator TestReportLimit_TestSendingError_SkippProcessingReports() + { + BacktraceClient.SetClientReportLimit(CLIENT_RATE_LIMIT); + int totalNumberOfReports = 5; + int maximumNumberOfRetries = 0; + BacktraceClient.RequestHandler = (string url, BacktraceData data) => + { + maximumNumberOfRetries++; + return new BacktraceResult(); + }; + int skippedReports = 0; + BacktraceClient.OnClientReportLimitReached = (BacktraceReport report) => + { + skippedReports++; + }; + + for (int i = 0; i < totalNumberOfReports; i++) + { + BacktraceClient.Send(new Exception("Exception")); + + } + Assert.AreEqual(totalNumberOfReports, maximumNumberOfRetries + skippedReports); + Assert.AreEqual(maximumNumberOfRetries, CLIENT_RATE_LIMIT); + Assert.AreEqual(totalNumberOfReports - CLIENT_RATE_LIMIT, skippedReports); + yield return null; + } + + + [UnityTest] + public IEnumerator TestReportLimit_TestSendingBacktraceReport_SkippProcessingReports() + { + BacktraceClient.SetClientReportLimit(CLIENT_RATE_LIMIT); + int totalNumberOfReports = 5; + int maximumNumberOfRetries = 0; + BacktraceClient.RequestHandler = (string url, BacktraceData data) => + { + maximumNumberOfRetries++; + return new BacktraceResult(); + }; + int skippedReports = 0; + BacktraceClient.OnClientReportLimitReached = (BacktraceReport report) => { - ServerUrl = "https://submit.backtrace.io/test/1234123412341234123412341234123412341234123412341234123412341234/json" + skippedReports++; }; - client.SetClientReportLimit(3); - gameObject.SetActive(true); - client.Refresh(); + + for (int i = 0; i < totalNumberOfReports; i++) + { + var report = new BacktraceReport(new Exception("Exception")); + BacktraceClient.Send(report); + } + Assert.AreEqual(totalNumberOfReports, maximumNumberOfRetries + skippedReports); + Assert.AreEqual(maximumNumberOfRetries, CLIENT_RATE_LIMIT); + Assert.AreEqual(totalNumberOfReports - CLIENT_RATE_LIMIT, skippedReports); + yield return null; } [UnityTest] public IEnumerator TestReportLimit_InvalidReportNumber_IgnoreAdditionalReports() { + BacktraceClient.SetClientReportLimit(CLIENT_RATE_LIMIT); + int totalNumberOfReports = 5; int maximumNumberOfRetries = 0; - client.RequestHandler = (string url, BacktraceData data) => + BacktraceClient.RequestHandler = (string url, BacktraceData data) => { maximumNumberOfRetries++; return new BacktraceResult(); }; - for (int i = 0; i < 5; i++) + int skippedReports = 0; + BacktraceClient.OnClientReportLimitReached = (BacktraceReport report) => { - client.Send("test"); + skippedReports++; + }; + + for (int i = 0; i < totalNumberOfReports; i++) + { + BacktraceClient.Send("test"); + } - Assert.AreEqual(5, maximumNumberOfRetries); + Assert.AreEqual(totalNumberOfReports, maximumNumberOfRetries + skippedReports); + Assert.AreEqual(maximumNumberOfRetries, CLIENT_RATE_LIMIT); + Assert.AreEqual(totalNumberOfReports - CLIENT_RATE_LIMIT, skippedReports); yield return null; } [UnityTest] public IEnumerator TestReportLimit_ValidReportNumber_AddAllReports() { + BacktraceClient.SetClientReportLimit(CLIENT_RATE_LIMIT); int maximumNumberOfRetries = 0; - client.RequestHandler = (string url, BacktraceData data) => + BacktraceClient.RequestHandler = (string url, BacktraceData data) => { maximumNumberOfRetries++; return new BacktraceResult(); }; for (int i = 0; i < 2; i++) { - client.Send("test"); + BacktraceClient.Send("test"); } Assert.AreEqual(2, maximumNumberOfRetries); yield return null; diff --git a/Editor/Tests/SerializationTests.cs b/Editor/Tests/SerializationTests.cs index ba4d1916..712274ad 100644 --- a/Editor/Tests/SerializationTests.cs +++ b/Editor/Tests/SerializationTests.cs @@ -1,33 +1,14 @@ -using Backtrace.Unity; -using Backtrace.Unity.Model; +using Backtrace.Unity.Model; using Backtrace.Unity.Model.JsonData; using NUnit.Framework; using System; using System.Collections; -using UnityEngine; using UnityEngine.TestTools; namespace Tests { public class SerializationTests { - private BacktraceClient client; - - [SetUp] - public void Setup() - { - var gameObject = new GameObject(); - gameObject.SetActive(false); - client = gameObject.AddComponent(); - client.Configuration = new BacktraceConfiguration() - { - ServerUrl = "https://test.sp.backtrace.io:6097/", - //backtrace configuration require 64 characters - Token = "1234123412341234123412341234123412341234123412341234123412341234" - }; - gameObject.SetActive(true); - } - [UnityTest] public IEnumerator TestDataSerialization_ValidReport_DataSerializeAndDeserialize() { @@ -58,8 +39,6 @@ public IEnumerator TestAttributes_ValidReport_AttributesExists() var json = data.Attributes.ToJson(); var deserializedData = BacktraceAttributes.Deserialize(json); Assert.IsTrue(deserializedData.Attributes.Count == data.Attributes.Attributes.Count); - //Assert.IsTrue(deserializedData.ComplexAttributes.Count == data.Attributes.ComplexAttributes.Count); - yield return null; } diff --git a/src/artifacts.meta b/Mocks.meta similarity index 77% rename from src/artifacts.meta rename to Mocks.meta index 6063e658..8bbfdd46 100644 --- a/src/artifacts.meta +++ b/Mocks.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: 7ef800f62cc6bcb4db7fed401b03f0e7 +guid: f7dba5d0875ce61448f80de4f31d8f88 folderAsset: yes DefaultImporter: externalObjects: {} diff --git a/Mocks/BacktraceDatabaseContextMock.cs b/Mocks/BacktraceDatabaseContextMock.cs new file mode 100644 index 00000000..f079632e --- /dev/null +++ b/Mocks/BacktraceDatabaseContextMock.cs @@ -0,0 +1,21 @@ +using Backtrace.Unity.Model; +using Backtrace.Unity.Model.Database; +using Backtrace.Unity.Services; + +public class BacktraceDatabaseContextMock : BacktraceDatabaseContext +{ + private BacktraceDatabaseSettings _settings; + public BacktraceDatabaseContextMock(BacktraceDatabaseSettings settings) : base(settings) + { + _settings = settings; + } + + protected override BacktraceDatabaseRecord ConvertToRecord(BacktraceData backtraceData, string hash) + { + //create new record and return it to AVOID storing data on hard drive + return new BacktraceDatabaseRecord(backtraceData, _settings.DatabasePath) + { + Hash = hash + }; + } +} diff --git a/Mocks/BacktraceDatabaseContextMock.cs.meta b/Mocks/BacktraceDatabaseContextMock.cs.meta new file mode 100644 index 00000000..60ec65d5 --- /dev/null +++ b/Mocks/BacktraceDatabaseContextMock.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: bcd80cd59dfcd3a45b27676da641d9b7 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Mocks/BacktraceDatabaseMock.cs b/Mocks/BacktraceDatabaseMock.cs new file mode 100644 index 00000000..e3cb65ee --- /dev/null +++ b/Mocks/BacktraceDatabaseMock.cs @@ -0,0 +1,41 @@ +using Backtrace.Unity; +using Backtrace.Unity.Interfaces; +using UnityEngine; + +public class BacktraceDatabaseMock : BacktraceDatabase +{ + /// + /// Make sure we won't remove any file/directory from unit-test code. + /// + protected override void RemoveOrphaned() + { + Debug.Log("Removing old reports"); + } + + internal override void LoadReports() + { + Debug.Log("Loading reports"); + } + + /// + /// Make sure we don't store any data on hard drive. + /// + protected override void CreateDatabaseDirectory() + { + Debug.Log("Creating database directory"); + } + + protected override IBacktraceDatabaseContext BacktraceDatabaseContext + { + get + { + return base.BacktraceDatabaseContext; + } + + set + { + // mock should only create one type of backtrace database context. + base.BacktraceDatabaseContext = new BacktraceDatabaseContextMock(DatabaseSettings); + } + } +} \ No newline at end of file diff --git a/Mocks/BacktraceDatabaseMock.cs.meta b/Mocks/BacktraceDatabaseMock.cs.meta new file mode 100644 index 00000000..38f5f7b2 --- /dev/null +++ b/Mocks/BacktraceDatabaseMock.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 0e50076ca3555014ab09c66d9374dd44 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/README.md b/README.md index 736cadbc..0d32b447 100644 --- a/README.md +++ b/README.md @@ -59,7 +59,10 @@ The following is a reference guide to the Backtrace Client fields: * Server Address: This field is required to submit exceptions from your Unity project to your Backtrace instance. More information about how to retrieve this value for your instance is our docs at What is a submission URL and What is a submission token? NOTE: the backtrace-unity plugin will expect full URL with token to your Backtrace instance, * Reports per minute: Limits the number of reports the client will send per minutes. If set to 0, there is no limit. If set to a higher value and the value is reached, the client will not send any reports until the next minute. Further, the BacktraceClient.Send/BacktraceClient.SendAsync method will return false. +* Destroy client on new scene load - Backtrace-client by default will be available on each scene. Once you initialize Backtrace integration, you can fetch Backtrace game object from every scene. In case if you don't want to have Backtrace-unity integration available by default in each scene, please set this value to true. * Handle unhandled exceptions: Toggle this on or off to set the library to handle unhandled exceptions that are not captured by try-catch blocks. +* 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. +* Deduplication rules: Backtrace-unity plugin allows you to combine the same reports. By using deduplication rules, you can tell backtrace-unity plugin how we should merge reports. * 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. NOTE: Backtrace database will remove all existing files on database start * Create database directory toggle: If toggled, the library will create the offline database directory if the provided path doesn't exists, @@ -94,6 +97,19 @@ catch(Exception exception){ } ``` +If you would like to change Backtrace client/database options, we recommend to change these values on the Unity UI via Backtrace Configuration file. However, if you would like to change these values dynamically, please use method `Refresh` to apply new configuration changes. + +For example: +```csharp + //Read from manager BacktraceClient instance +var backtraceClient = GameObject.Find("manager name").GetComponent(); + +//Change configuration value +backtraceClient.Configuration.DeduplicationStrategy = deduplicationStrategy; +//Refresh configuraiton +backtraceClient.Refresh(); + +``` ## Sending an error report `BacktraceClient.Send` method will send an error report to the Backtrace endpoint specified. There `Send` method is overloaded, see examples below: @@ -119,7 +135,7 @@ catch (Exception exception) ``` Notes: - if you setup `BacktraceClient` with `BacktraceDatabase` and your application is offline or you pass invalid credentials to `Backtrace server`, reports will be stored in database directory path, -- `BacktraceReport` allows you to change default fingerprint generation algorithm. You can use `Fingerprint` property if you want to change fingerprint value. Keep in mind - fingerprint should be valid sha256 string., +- `BacktraceReport` allows you to change default Fingerprint generation algorithm. You can use `Fingerprint` property if you want to change Fingerprint value. Keep in mind - Fingerprint should be valid sha256 string. By setting `Fingerprint` you are instructing the client reporting library to only write a single report for the exception as it is encountered, and maintain a counter for every additional time it is encountered, instead of creating a new report. This will allow better control over the volume of reports being generated and sent to Backtrace. The counter is reset when the offline database is cleared (usually when the reports are sent to the server). A new single report will be created the next time the error is encountered. - `BacktraceReport` allows you to change grouping strategy in Backtrace server. If you want to change how algorithm group your reports in Backtrace server please override `Factor` property. If you want to use `Fingerprint` and `Factor` property you have to override default property values. See example below to check how to use these properties: @@ -132,7 +148,7 @@ try catch (Exception exception) { var report = new BacktraceReport(...){ - FingerPrint = "sha256 string", + Fingerprint = "sha256 string", Factor = exception.GetType().Name }; .... @@ -189,6 +205,23 @@ You can clear all data from database without sending it to server by using `Clea backtraceDatabase.Clear(); ``` +#### Deduplication +Backtrace unity integration allows you to aggregate the same reports and send only one message to Backtrace Api. As a developer you can choose deduplication options. Please use `DeduplicationStrategy` enum to setup possible deduplication rules in Unity UI: +![Backtrace deduplicaiton setup](./images/deduplication-setup.PNG) + +Deduplication strategy types: +* Ignore - ignore deduplication strategy, +* Default - deduplication strategy will only use current strack trace to find duplicated reports, +* Classifier - deduplication strategy will use stack trace and exception type to find duplicated reports, +* Message - deduplication strategy will use stack trace and exception message to find duplicated reports, + +Notes: +* When you aggregate reports via Backtrace C# library, `BacktraceDatabase` will increase number of reports in BacktraceDatabaseRecord counter property. +* Deduplication algorithm will include `BacktraceReport` `Fingerprint` and `Factor` properties. `Fingerprint` property will overwrite deduplication algorithm result. `Factor` property will change hash generated by deduplication algorithm. +* If Backtrace unity integration combine multiple reports and user will close a game before plugin will send data to Backtrace, you will lose coutner information. +* `BacktraceDatabase` methods allows you to use aggregated diagnostic data together. You can check `Hash` property of `BacktraceDatabaseRecord` to check generated hash for diagnostic data and `Counter` to check how much the same records we detect. +* `BacktraceDatabase` `Count` method will return number of all records stored in database (included deduplicated records), +* `BacktarceDatabase` `Delete` method will remove record (with multiple deduplicated records) at the same time. # Architecture description diff --git a/images/client-setup.PNG b/images/client-setup.PNG index c61cf129..05d4b3dc 100644 Binary files a/images/client-setup.PNG and b/images/client-setup.PNG differ diff --git a/images/deduplication-setup.PNG b/images/deduplication-setup.PNG new file mode 100644 index 00000000..57f4284a Binary files /dev/null and b/images/deduplication-setup.PNG differ diff --git a/images/deduplication-setup.PNG.meta b/images/deduplication-setup.PNG.meta new file mode 100644 index 00000000..d86c8f12 --- /dev/null +++ b/images/deduplication-setup.PNG.meta @@ -0,0 +1,91 @@ +fileFormatVersion: 2 +guid: 52eaea5e7667d6946a38b629e77ed0d6 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 10 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: -1 + aniso: -1 + mipBias: -100 + wrapU: -1 + wrapV: -1 + wrapW: -1 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 0 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + platformSettings: + - serializedVersion: 3 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spritePackingTag: + pSDRemoveMatte: 0 + pSDShowRemoveMatteOption: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/src/BacktraceClient.cs b/src/BacktraceClient.cs index 4ba163a4..57101906 100644 --- a/src/BacktraceClient.cs +++ b/src/BacktraceClient.cs @@ -1,4 +1,5 @@ -using Backtrace.Unity.Interfaces; +using Backtrace.Unity.Common; +using Backtrace.Unity.Interfaces; using Backtrace.Unity.Model; using Backtrace.Unity.Services; using Backtrace.Unity.Types; @@ -41,6 +42,8 @@ public static BacktraceClient Instance public IBacktraceDatabase Database; private IBacktraceApi _backtraceApi; + + private ReportLimitWatcher _reportLimitWatcher; /// /// Set an event executed when received bad request, unauthorize request or other information from server /// @@ -96,6 +99,11 @@ public Action OnServerResponse /// public MiniDumpType MiniDumpType { get; set; } = MiniDumpType.None; + /// + /// Set event executed when client site report limit reached + /// + internal Action _onClientReportLimitReached = null; + /// /// Set event executed when client site report limit reached /// @@ -105,7 +113,7 @@ public Action OnClientReportLimitReached { if (ValidClientConfiguration()) { - BacktraceApi.SetClientRateLimitEvent(value); + _onClientReportLimitReached = value; } } } @@ -121,7 +129,9 @@ public Action OnClientReportLimitReached public Action OnUnhandledApplicationException = null; /// - /// Get custom client attributes. Every argument stored in dictionary will be send to Backtrace API + /// Get custom client attributes. Every argument stored in dictionary will be send to Backtrace API. + /// Backtrace unity-plugin allows you to store in attributes only primitive values. Plugin will skip loading + /// complex object. /// public readonly Dictionary Attributes; @@ -142,9 +152,28 @@ internal IBacktraceApi BacktraceApi Database?.SetApi(_backtraceApi); } } + + internal ReportLimitWatcher ReportLimitWatcher + { + get + { + return _reportLimitWatcher; + } + set + { + _reportLimitWatcher = value; + Database?.SetReportWatcher(_reportLimitWatcher); + } + } + + public void OnDisable() + { + Debug.LogWarning("Disabling BacktraceClient integration"); + Enabled = false; + } + public void Refresh() { - Database = GetComponent(); if (Configuration == null || !Configuration.IsValid()) { Debug.LogWarning("Configuration doesn't exists or provided serverurl/token are invalid"); @@ -152,22 +181,27 @@ public void Refresh() } Enabled = true; - if (Configuration.HandleUnhandledExceptions) - { - HandleUnhandledExceptions(); - } + + HandleUnhandledExceptions(); + _reportLimitWatcher = new ReportLimitWatcher(Convert.ToUInt32(Configuration.ReportPerMin)); BacktraceApi = new BacktraceApi( credentials: new BacktraceCredentials(Configuration.GetValidServerUrl()), - reportPerMin: Convert.ToUInt32(Configuration.ReportPerMin), ignoreSslValidation: Configuration.IgnoreSslValidation); - Database?.SetApi(BacktraceApi); - if (Configuration.DestroyOnLoad == false) { DontDestroyOnLoad(gameObject); _instance = this; } + Database = GetComponent(); + if (Database == null) + { + return; + } + Database.Reload(); + Database.SetApi(BacktraceApi); + Database.SetReportWatcher(_reportLimitWatcher); + } private void Awake() @@ -181,40 +215,111 @@ private void Awake() /// Number of reports sending per one minute. If value is equal to zero, there is no request sending to API. Value have to be greater than or equal to 0 public void SetClientReportLimit(uint reportPerMin) { - BacktraceApi?.SetClientRateLimit(reportPerMin); + if (Enabled == false) + { + Debug.LogWarning("Please enable BacktraceClient first - Please validate Backtrace client initializaiton in Unity IDE."); + return; + } + _reportLimitWatcher.SetClientReportLimit(reportPerMin); } /// - /// Send a report to Backtrace API + /// Send a message report to Backtrace API /// - /// Report to send + /// Report message + /// List of attachments + /// List of report attributes attachmentPaths = null, Dictionary attributes = null) { + if (Enabled == false) + { + Debug.LogWarning("Please enable BacktraceClient first - Please validate Backtrace client initializaiton in Unity IDE."); + return; + } + //check rate limiting + bool limitHit = _reportLimitWatcher.WatchReport(new DateTime().Timestamp()); + if (limitHit == false && _onClientReportLimitReached == null) + { + Debug.LogWarning("Report limit hit."); + return; + } var report = new BacktraceReport( - message: message, - attachmentPaths: attachmentPaths, - attributes: attributes); - Send(report); + message: message, + attachmentPaths: attachmentPaths, + attributes: attributes); + if (!limitHit) + { + _onClientReportLimitReached?.Invoke(report); + Debug.LogWarning("Report limit hit."); + return; + } + + SendReport(report); } /// - /// Send a report to Backtrace API + /// Send an exception to Backtrace API /// - /// Report to send + /// Report exception + /// List of attachments + /// List of report attributes attachmentPaths = null, Dictionary attributes = null) { + if (Enabled == false) + { + Debug.LogWarning("Please enable BacktraceClient first - Please validate Backtrace client initializaiton in Unity IDE."); + return; + } + //check rate limiting + bool limitHit = _reportLimitWatcher.WatchReport(new DateTime().Timestamp()); + if (limitHit == false && _onClientReportLimitReached == null) + { + Debug.LogWarning("Report limit hit."); + return; + } var report = new BacktraceReport( - exception: exception, - attributes: attributes, - attachmentPaths: attachmentPaths); - Send(report); + exception: exception, + attachmentPaths: attachmentPaths, + attributes: attributes); + if (!limitHit) + { + _onClientReportLimitReached?.Invoke(report); + Debug.LogWarning("Report limit hit."); + return; + } + SendReport(report); } /// /// Send a report to Backtrace API /// /// Report to send + /// Send report callback public void Send(BacktraceReport report, Action sendCallback = null) + { + if (Enabled == false) + { + Debug.LogWarning("Please enable BacktraceClient first - Please validate Backtrace client initializaiton in Unity IDE."); + return; + } + //check rate limiting + bool watcherValidation = _reportLimitWatcher.WatchReport(report); + if (!watcherValidation) + { + _onClientReportLimitReached?.Invoke(report); + sendCallback?.Invoke(BacktraceResult.OnLimitReached(report)); + Debug.LogWarning("Report limit hit."); + return; + } + SendReport(report, sendCallback); + } + + /// + /// Send a report to Backtrace API after first type of report validation rules + /// + /// Backtrace report + /// send callback + private void SendReport(BacktraceReport report, Action sendCallback = null) { var record = Database?.Add(report, Attributes, MiniDumpType); //create a JSON payload instance @@ -226,13 +331,16 @@ public void Send(BacktraceReport report, Action sendCallback = if (BacktraceApi == null) { record?.Dispose(); - Debug.LogWarning("Backtrace API not exisits. Please validate client token or server url!"); + Debug.LogWarning("Backtrace API doesn't exist. Please validate client token or server url!"); return; } - StartCoroutine(BacktraceApi.Send(data, (BacktraceResult result) => { - record?.Dispose(); + if (record != null) + { + record.Dispose(); + //Database?.IncrementRecordRetryLimit(record); + } if (result?.Status == BacktraceResultStatus.Ok) { Database?.Delete(record); @@ -245,22 +353,42 @@ public void Send(BacktraceReport report, Action sendCallback = }); sendCallback?.Invoke(result); })); + } + /// + /// Handle Untiy unhandled exceptions + /// public void HandleUnhandledExceptions() - { - Application.logMessageReceived += HandleException; + { + if (Enabled == false) + { + Debug.LogWarning("Cannot set unhandled exception handler for not enabled Backtrace client instance"); + return; + } + if (Configuration.HandleUnhandledExceptions) + { + Application.logMessageReceived += HandleException; + } } + + + + /// + /// Catch Unity logger data and create Backtrace reports for log type that represents exception or error + /// + /// Log message + /// Log stack trace + /// log type private void HandleException(string message, string stackTrace, LogType type) { - if ((type == LogType.Exception || type == LogType.Error ) + if ((type == LogType.Exception || type == LogType.Error) && (!string.IsNullOrEmpty(message) && !message.StartsWith("[Backtrace]::"))) { var exception = new BacktraceUnhandledException(message, stackTrace); OnUnhandledApplicationException?.Invoke(exception); - var report = new BacktraceReport(exception); - Send(report); + Send(exception); } } @@ -292,5 +420,7 @@ private bool ValidClientConfiguration() } return !invalidConfiguration; } + + } } diff --git a/src/BacktraceDatabase.cs b/src/BacktraceDatabase.cs index 0110bef1..ac083f00 100644 --- a/src/BacktraceDatabase.cs +++ b/src/BacktraceDatabase.cs @@ -1,5 +1,4 @@ -using Backtrace.Unity.Common; -using Backtrace.Unity.Interfaces; +using Backtrace.Unity.Interfaces; using Backtrace.Unity.Model; using Backtrace.Unity.Model.Database; using Backtrace.Unity.Services; @@ -40,13 +39,33 @@ public static BacktraceDatabase Instance } } + /// + /// Backtrace Database deduplication strategy + /// + public DeduplicationStrategy DeduplicationStrategy + { + get + { + return BacktraceDatabaseContext?.DeduplicationStrategy ?? DeduplicationStrategy.None; + } + set + { + if (!Enable) + { + throw new InvalidOperationException("Backtrace Database is disabled"); + } + BacktraceDatabaseContext.DeduplicationStrategy = value; + } + } /// /// Database settings /// - private BacktraceDatabaseSettings DatabaseSettings { get; set; } + protected BacktraceDatabaseSettings DatabaseSettings { get; set; } + /// + /// Last update timestamp + /// private float _lastConnection; - /// /// Backtrace Api instance. Use BacktraceApi to send data to Backtrace server @@ -56,7 +75,7 @@ public static BacktraceDatabase Instance /// /// Database context - in memory cache and record operations /// - internal IBacktraceDatabaseContext BacktraceDatabaseContext { get; set; } + protected virtual IBacktraceDatabaseContext BacktraceDatabaseContext { get; set; } /// /// File context - file collection operations @@ -79,8 +98,15 @@ private string DatabasePath /// public bool Enable { get; private set; } + + /// + /// Reload Backtrace database configuration. Reloading configuration is required, when you change + /// BacktraceDatabase configuration options. + /// public void Reload() { + + // validate configuration if (Configuration == null) { Configuration = GetComponent().Configuration; @@ -93,41 +119,47 @@ public void Reload() } + //setup database object DatabaseSettings = new BacktraceDatabaseSettings(Configuration); - if (DatabaseSettings == null) - { - Enable = false; - return; - } - if (Configuration.CreateDatabase) - { - Directory.CreateDirectory(Configuration.DatabasePath); - } - if (Configuration.DestroyOnLoad == false) - { - DontDestroyOnLoad(gameObject); - _instance = this; - } Enable = Configuration.Enabled && BacktraceConfiguration.ValidateDatabasePath(Configuration.DatabasePath); - if (!Enable) { - + Debug.LogWarning("Cannot initialize database - invalid database configuration. Database is disabled"); return; } - + CreateDatabaseDirectory(); + SetupMultisceneSupport(); _lastConnection = Time.time; - BacktraceDatabaseContext = new BacktraceDatabaseContext(DatabasePath, DatabaseSettings.RetryLimit, DatabaseSettings.RetryOrder); + //Setup database context + BacktraceDatabaseContext = new BacktraceDatabaseContext(DatabasePath, DatabaseSettings.RetryLimit, DatabaseSettings.RetryOrder, DatabaseSettings.DeduplicationStrategy); BacktraceDatabaseFileContext = new BacktraceDatabaseFileContext(DatabasePath, DatabaseSettings.MaxDatabaseSize, DatabaseSettings.MaxRecordCount); - BacktraceApi = new BacktraceApi(Configuration.ToCredentials(), Convert.ToUInt32(Configuration.ReportPerMin)); + BacktraceApi = new BacktraceApi(Configuration.ToCredentials()); + _reportLimitWatcher = new ReportLimitWatcher(Convert.ToUInt32(Configuration.ReportPerMin)); + } + + /// + /// Backtrace database on disable event + /// + public void OnDisable() + { + Debug.LogWarning("Disabling BacktraceDatabase integration"); + Enable = false; + } + + /// + /// Backtrace database awake event + /// private void Awake() { Reload(); } + /// + /// Backtrace database update event + /// private void Update() { if (!Enable) @@ -137,7 +169,7 @@ private void Update() if (Time.time - _lastConnection > DatabaseSettings.RetryInterval) { _lastConnection = Time.time; - if (!BacktraceDatabaseContext.Any() || _timerBackgroundWork) + if (_timerBackgroundWork || !BacktraceDatabaseContext.Any()) { return; } @@ -151,7 +183,7 @@ private void Update() private void Start() { if (!Enable) - { + { return; } if (DatabaseSettings.AutoSendMode) @@ -209,17 +241,8 @@ public BacktraceDatabaseRecord Add(BacktraceReport backtraceReport, Dictionary - /// Create new minidump file in database directory path. Minidump file name is a random Guid - /// - /// Current report - /// Generated minidump type - /// Path to minidump file - private string GenerateMiniDump(BacktraceReport backtraceReport, MiniDumpType miniDumpType) - { - //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(DatabaseSettings.DatabasePath, $"{backtraceReport.Uuid}-dump.dmp"); - MinidumpException minidumpExceptionType = backtraceReport.ExceptionTypeReport - ? MinidumpException.Present - : MinidumpException.None; - - bool minidumpSaved = MinidumpHelper.Write( - filePath: minidumpDestinationPath, - options: miniDumpType, - exceptionType: minidumpExceptionType); - - return minidumpSaved - ? minidumpDestinationPath - : string.Empty; - } - /// /// Get total number of records in database /// /// Total number of records - internal int Count() + public int Count() { return BacktraceDatabaseContext.Count(); } @@ -339,28 +339,65 @@ internal int Count() /// /// Detect all orphaned minidump and files /// - private void RemoveOrphaned() + protected virtual void RemoveOrphaned() { var records = BacktraceDatabaseContext.Get(); BacktraceDatabaseFileContext.RemoveOrphaned(records); } + /// + /// Setup multiscene support + /// + protected virtual void SetupMultisceneSupport() + { + if (Configuration.DestroyOnLoad == true) + { + return; + } + DontDestroyOnLoad(gameObject); + _instance = this; + } + + + /// + /// Create database directory + /// + protected virtual void CreateDatabaseDirectory() + { + if (Configuration.CreateDatabase != true) + { + return; + } + if (string.IsNullOrEmpty(Configuration.DatabasePath)) + { + Enable = false; + throw new InvalidOperationException("Cannot create Backtrace datase directory. Database directory is null or empty"); + } + Directory.CreateDirectory(Configuration.DatabasePath); + } + /// /// Load all records stored in database path /// - private void LoadReports() + internal virtual void LoadReports() { var files = BacktraceDatabaseFileContext.GetRecords(); foreach (var file in files) { var record = BacktraceDatabaseRecord.ReadFromFile(file); + if (record == null) + { + continue; + } + record.DatabasePath(DatabasePath); if (!record.Valid()) { try { + Debug.Log("Removing record from Backtrace Database path"); record.Delete(); } - catch(Exception) + catch (Exception) { Debug.LogWarning($"Cannot remove file from database. File name: {file.FullName}"); } @@ -427,5 +464,12 @@ public long GetDatabaseSize() { return BacktraceDatabaseContext.GetSize(); } + + private ReportLimitWatcher _reportLimitWatcher; + public void SetReportWatcher(ReportLimitWatcher reportLimitWatcher) + { + _reportLimitWatcher = reportLimitWatcher; + } + } } diff --git a/src/Extensions/DateTimeExtensions.cs b/src/Extensions/DateTimeExtensions.cs new file mode 100644 index 00000000..8b4466b8 --- /dev/null +++ b/src/Extensions/DateTimeExtensions.cs @@ -0,0 +1,14 @@ +using System; + +namespace Backtrace.Unity.Common +{ + public static class DateTimeExtensions + { + + public static int Timestamp(this DateTime dateTime) + { + return (int)(DateTime.UtcNow.Subtract(new DateTime(1970, 1, 1))).TotalSeconds; + } + } + +} \ No newline at end of file diff --git a/src/Extensions/DateTimeExtensions.cs.meta b/src/Extensions/DateTimeExtensions.cs.meta new file mode 100644 index 00000000..0a8c805e --- /dev/null +++ b/src/Extensions/DateTimeExtensions.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 279528ffbca9ca5439d18486d7041e53 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/src/Interfaces/IBacktraceAPI.cs b/src/Interfaces/IBacktraceAPI.cs index 02e4022d..72760645 100644 --- a/src/Interfaces/IBacktraceAPI.cs +++ b/src/Interfaces/IBacktraceAPI.cs @@ -25,10 +25,6 @@ public interface IBacktraceApi /// Action OnServerResponse { get; set; } - void SetClientRateLimitEvent(Action onClientReportLimitReached); - - void SetClientRateLimit(uint rateLimit); - /// /// Setup custom request method /// diff --git a/src/Interfaces/IBacktraceDatabase.cs b/src/Interfaces/IBacktraceDatabase.cs index 635f78d1..e31ca7d6 100644 --- a/src/Interfaces/IBacktraceDatabase.cs +++ b/src/Interfaces/IBacktraceDatabase.cs @@ -1,7 +1,7 @@ using Backtrace.Unity.Model; using Backtrace.Unity.Model.Database; +using Backtrace.Unity.Services; using Backtrace.Unity.Types; -using System; using System.Collections.Generic; namespace Backtrace.Unity.Interfaces @@ -22,8 +22,13 @@ public interface IBacktraceDatabase /// void Flush(); + /// + /// Set Backtrace API instance + /// + /// Backtrace API object instance void SetApi(IBacktraceApi backtraceApi); + /// /// Remove all existing reports in BacktraceDatabase /// @@ -61,5 +66,17 @@ public interface IBacktraceDatabase /// Get database size /// long GetDatabaseSize(); + + /// + /// Set report limit watcher - object responsible to validate number of events per time unit + /// + /// Report limit watcher instance + void SetReportWatcher(ReportLimitWatcher reportLimitWatcher); + + /// + /// Reload Backtrace database configuration. Reloading configuration is required, when you change + /// BacktraceDatabase configuration options. + /// + void Reload(); } } diff --git a/src/Interfaces/IBacktraceDatabaseContext.cs b/src/Interfaces/IBacktraceDatabaseContext.cs index bdb50d5e..f5ca92c5 100644 --- a/src/Interfaces/IBacktraceDatabaseContext.cs +++ b/src/Interfaces/IBacktraceDatabaseContext.cs @@ -1,17 +1,18 @@ using Backtrace.Unity.Model; using Backtrace.Unity.Model.Database; +using Backtrace.Unity.Types; using System; using System.Collections.Generic; namespace Backtrace.Unity.Interfaces { - internal interface IBacktraceDatabaseContext : IDisposable + public interface IBacktraceDatabaseContext : IDisposable { /// - /// Add new record to Database + /// Add new data to database /// - /// Diagnostic data - BacktraceDatabaseRecord Add(BacktraceData backtraceData); + /// Database record + BacktraceDatabaseRecord Add(BacktraceData backtraceData, MiniDumpType miniDumpType = MiniDumpType.None); /// /// Add new data to database @@ -25,6 +26,12 @@ internal interface IBacktraceDatabaseContext : IDisposable /// First existing record in database store BacktraceDatabaseRecord FirstOrDefault(); + /// + /// Get first record or null + /// + /// First existing record in database store + BacktraceDatabaseRecord FirstOrDefault(Func predicate); + /// /// Get last record or null /// @@ -79,6 +86,7 @@ internal interface IBacktraceDatabaseContext : IDisposable /// Get total number of records stored in database /// /// Total number of records + [Obsolete("Please use Count method instead")] int GetTotalNumberOfRecords(); /// @@ -86,5 +94,10 @@ internal interface IBacktraceDatabaseContext : IDisposable /// /// If algorithm can remove last record, method return true. Otherwise false bool RemoveLastRecord(); + + /// + /// Context deduplication strategy + /// + DeduplicationStrategy DeduplicationStrategy { get; set; } } } diff --git a/src/Model/BacktraceClientConfiguration.cs b/src/Model/BacktraceClientConfiguration.cs index 2209688b..0d00f62a 100644 --- a/src/Model/BacktraceClientConfiguration.cs +++ b/src/Model/BacktraceClientConfiguration.cs @@ -1,4 +1,5 @@ -using System; +using Backtrace.Unity.Types; +using System; using UnityEngine; namespace Backtrace.Unity.Model @@ -11,14 +12,15 @@ public class BacktraceClientConfiguration : ScriptableObject public bool HandleUnhandledExceptions = true; public bool IgnoreSslValidation = false; public bool DestroyOnLoad = true; - + public DeduplicationStrategy DeduplicationStrategy = DeduplicationStrategy.None; + public void UpdateServerUrl() { if (string.IsNullOrEmpty(ServerUrl)) { return; } - + Uri serverUri; var result = Uri.TryCreate(ServerUrl, UriKind.RelativeOrAbsolute, out serverUri); if (result) diff --git a/src/Model/BacktraceConfiguration.cs b/src/Model/BacktraceConfiguration.cs index 35c4319a..d925fd58 100644 --- a/src/Model/BacktraceConfiguration.cs +++ b/src/Model/BacktraceConfiguration.cs @@ -66,7 +66,7 @@ public class BacktraceConfiguration : ScriptableObject /// How much seconds library should wait before next retry. /// public int RetryInterval = 60; - + /// /// Maximum number of retries /// @@ -75,7 +75,13 @@ public class BacktraceConfiguration : ScriptableObject /// /// Destroy Backtrace instances on new scene load. /// - public bool DestroyOnLoad = true; + public bool DestroyOnLoad = false; + + /// + /// Backtrace client deduplication strategy. + /// + public DeduplicationStrategy DeduplicationStrategy = DeduplicationStrategy.None; + /// /// Retry order @@ -96,7 +102,7 @@ public static string UpdateServerUrl(string value) { return value; } - + if (!value.StartsWith("http")) { value = $"https://{value}"; diff --git a/src/Model/BacktraceData.cs b/src/Model/BacktraceData.cs index 69946c70..38b61a59 100644 --- a/src/Model/BacktraceData.cs +++ b/src/Model/BacktraceData.cs @@ -85,6 +85,12 @@ public class BacktraceData public Annotations Annotation = null; public ThreadData ThreadData = null; + + /// + /// Number of deduplications + /// + public int Deduplication { get; set; } = 0; + /// /// Empty constructor for serialization purpose /// @@ -117,7 +123,7 @@ public string ToJson() ["lang"] = "csharp", ["langVersion"] = "Unity", ["agent"] = "backtrace-unity", - ["agentVersion"] = "1.1.5", + ["agentVersion"] = "2.0.0", ["mainThread"] = MainThread, ["classifiers"] = new JArray(Classifier), ["attributes"] = Attributes.ToJson(), @@ -163,7 +169,7 @@ private void SetReportInformation() Uuid = Report.Uuid; Timestamp = Report.Timestamp; LangVersion = "Mono/IL2CPP"; - AgentVersion = "1.1.5"; + AgentVersion = "2.0.0"; Classifier = Report.ExceptionTypeReport ? new[] { Report.Classifier } : null; } } diff --git a/src/Model/BacktraceReport.cs b/src/Model/BacktraceReport.cs index 1eb088e6..d4c295a5 100644 --- a/src/Model/BacktraceReport.cs +++ b/src/Model/BacktraceReport.cs @@ -31,7 +31,7 @@ public class BacktraceReport /// /// UTC timestamp in seconds /// - public long Timestamp { get; private set; } = (int)(DateTime.UtcNow.Subtract(new DateTime(1970, 1, 1))).TotalSeconds; + public long Timestamp { get; private set; } = new DateTime().Timestamp(); /// /// Get information aboout report type. If value is true the BacktraceReport has an error information @@ -178,6 +178,10 @@ public BacktraceReport( AttachmentPaths = attachmentPaths ?? new List(); Exception = exception; ExceptionTypeReport = exception != null; + if (ExceptionTypeReport) + { + Message = exception.Message; + } Classifier = ExceptionTypeReport ? exception.GetType().Name : string.Empty; SetStacktraceInformation(); } diff --git a/src/Model/BacktraceStackFrame.cs b/src/Model/BacktraceStackFrame.cs index eea3186f..c3e5ecc1 100644 --- a/src/Model/BacktraceStackFrame.cs +++ b/src/Model/BacktraceStackFrame.cs @@ -11,7 +11,7 @@ public class BacktraceStackFrame /// Function where exception occurs /// [JsonProperty(PropertyName = "funcName")] - public string FunctionName; + public string FunctionName = "unknown"; /// /// Line number in source code where exception occurs @@ -123,7 +123,7 @@ public BacktraceStackFrame(StackFrame frame, bool generatedByException) Il = frame.GetILOffset(); ILOffset = Il; - Library = SourceCodeFullPath; + Library = string.IsNullOrEmpty(SourceCodeFullPath) ? frame.GetMethod()?.DeclaringType.ToString() : SourceCodeFullPath; SourceCode = generatedByException ? Guid.NewGuid().ToString() @@ -148,8 +148,9 @@ public BacktraceStackFrame(StackFrame frame, bool generatedByException) private string GetMethodName(StackFrame frame) { var method = frame.GetMethod(); - string methodName = method.Name; - return methodName; + var methodName = method.Name.StartsWith(".") ? method.Name.Substring(1, method.Name.Length - 1) : method.Name; + string fullMethodName = $"{method?.DeclaringType.ToString()}.{methodName}()"; + return fullMethodName; } } } diff --git a/src/Model/BacktraceStackTrace.cs b/src/Model/BacktraceStackTrace.cs index 8613a93e..289fc696 100644 --- a/src/Model/BacktraceStackTrace.cs +++ b/src/Model/BacktraceStackTrace.cs @@ -61,9 +61,6 @@ private void SetStacktraceInformation(StackFrame[] frames, bool generatedByExcep return; } int startingIndex = 0; - //determine stack frames generated by Backtrace library - //if we get stack frame from Backtrace we ignore them and reset stack frame stack - var executedAssemblyName = Assembly.GetExecutingAssembly().GetName().Name; foreach (var frame in frames) { string name = frame?.GetMethod()?.DeclaringType.ToString() ?? string.Empty; diff --git a/src/Model/Database/BacktraceDatabaseRecord.cs b/src/Model/Database/BacktraceDatabaseRecord.cs index e867efd4..394ff5fa 100644 --- a/src/Model/Database/BacktraceDatabaseRecord.cs +++ b/src/Model/Database/BacktraceDatabaseRecord.cs @@ -56,6 +56,12 @@ public class BacktraceDatabaseRecord : IDisposable [JsonProperty(PropertyName = "size")] internal long Size { get; set; } + /// + /// Record hash + /// + [JsonProperty(PropertyName = "hash")] + internal string Hash = string.Empty; + /// /// Stored record /// @@ -66,7 +72,18 @@ public class BacktraceDatabaseRecord : IDisposable /// Path to database directory /// [JsonIgnore] - private readonly string _path = string.Empty; + private string _path = string.Empty; + + private int _count = 1; + + public int Count + { + get + { + return _count; + } + } + /// /// Record writer @@ -84,6 +101,7 @@ public BacktraceData BacktraceData { if (Record != null) { + Record.Deduplication = Count; return Record; } if (!Valid()) @@ -109,6 +127,8 @@ public BacktraceData BacktraceData //because we have easier way to serialize and deserialize data //and no problem/condition with serialization when BacktraceApi want to send diagnostic data to API diagnosticData.Report = report; + diagnosticData.Attachments = report.AttachmentPaths; + diagnosticData.Deduplication = Count; return diagnosticData; } catch (SerializationException) @@ -212,6 +232,16 @@ public bool Save() } } + /// + /// Setup RecordWriter and database path after deserialization event + /// + /// Path to database + internal void DatabasePath(string path) + { + _path = path; + RecordWriter = new BacktraceDatabaseRecordWriter(path); + } + /// /// Save single file from database record /// @@ -229,6 +259,14 @@ private string Save(string json, string prefix) return RecordWriter.Write(file, prefix); } + /// + /// Increment number of the same records in database + /// + public virtual void Increment() + { + _count++; + } + /// /// Check if all necessary files declared on record exists /// @@ -239,7 +277,7 @@ internal bool Valid() } /// - /// Delete all record files + /// Delete all records from hard drive. /// internal void Delete() { diff --git a/src/Model/Database/BacktraceDatabaseSettings.cs b/src/Model/Database/BacktraceDatabaseSettings.cs index 57fd6645..f44adaa2 100644 --- a/src/Model/Database/BacktraceDatabaseSettings.cs +++ b/src/Model/Database/BacktraceDatabaseSettings.cs @@ -22,6 +22,7 @@ public BacktraceDatabaseSettings(BacktraceConfiguration configuration) RetryInterval = Convert.ToUInt32(configuration.RetryInterval); RetryLimit = Convert.ToUInt32(configuration.RetryLimit); RetryOrder = configuration.RetryOrder; + DeduplicationStrategy = configuration.DeduplicationStrategy; } /// /// Directory path where reports and minidumps are stored @@ -74,6 +75,11 @@ public long MaxDatabaseSize /// public uint RetryLimit { get; set; } = 3; + /// + /// Deduplication strategy + /// + public DeduplicationStrategy DeduplicationStrategy { get; set; } = DeduplicationStrategy.None; + public RetryOrder RetryOrder { get; set; } = RetryOrder.Queue; } } \ No newline at end of file diff --git a/src/Model/DeduplicationModel.cs b/src/Model/DeduplicationModel.cs new file mode 100644 index 00000000..1136a4ae --- /dev/null +++ b/src/Model/DeduplicationModel.cs @@ -0,0 +1,93 @@ +using Backtrace.Unity.Types; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Cryptography; +using System.Text; +using UnityEngine; + +namespace Backtrace.Unity.Model +{ + public class DeduplicationModel + { + private readonly BacktraceData _backtraceData; + private readonly DeduplicationStrategy _strategy; + public DeduplicationModel( + BacktraceData backtraceData, + DeduplicationStrategy strategy) + { + _backtraceData = backtraceData; + _strategy = strategy; + } + public string StackTrace + { + get + { + if (_strategy == DeduplicationStrategy.None) + { + return ""; + } + if (_backtraceData.Report == null || _backtraceData.Report.DiagnosticStack == null) + { + Debug.Log("Report or diagnostic stack is null"); + return ""; + } + var result = _backtraceData.Report.DiagnosticStack + .Select(n => n.FunctionName) + .OrderByDescending(n => n); + + var stackTrace = new HashSet(result).ToArray(); + return string.Join(",", stackTrace); + } + } + public string Classifier + { + get + { + if ((_strategy & DeduplicationStrategy.Classifier) == 0) + { + return ""; + } + return string.Join(",", _backtraceData.Classifier); + } + } + public string ExceptionMessage + { + get + { + if ((_strategy & DeduplicationStrategy.Message) == 0) + { + return string.Empty; + } + return _backtraceData.Report.Message; + } + } + + public string Factor + { + get + { + return _backtraceData.Report.Factor; + } + } + + public string GetSha() + { + if (!string.IsNullOrEmpty(_backtraceData.Report.Fingerprint)) + { + return _backtraceData.Report.Fingerprint; + } + + var stringBuilder = new StringBuilder(); + stringBuilder.Append(ExceptionMessage); + stringBuilder.Append(Classifier); + stringBuilder.Append(StackTrace); + + using (var sha256Hash = SHA256.Create()) + { + var bytes = sha256Hash.ComputeHash(Encoding.ASCII.GetBytes(stringBuilder.ToString())); + return Convert.ToBase64String(bytes); + } + } + } +} \ No newline at end of file diff --git a/src/Model/DeduplicationModel.cs.meta b/src/Model/DeduplicationModel.cs.meta new file mode 100644 index 00000000..8dae20bd --- /dev/null +++ b/src/Model/DeduplicationModel.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 15d765ebe6434764e816d497f03517c0 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/src/Model/JsonData/Annotations.cs b/src/Model/JsonData/Annotations.cs index c1fc8b5f..20e0d963 100644 --- a/src/Model/JsonData/Annotations.cs +++ b/src/Model/JsonData/Annotations.cs @@ -11,13 +11,33 @@ namespace Backtrace.Unity.Model.JsonData /// public class Annotations { + + private const string ENVIRONMENT_VARIABLE_KEY = "Environment Variables"; private JToken _serializedAnnotations; + private Dictionary _environmentVariables = new Dictionary(); /// /// Get system environment variables /// - [JsonProperty(PropertyName = "Environment Variables")] - public Dictionary EnvironmentVariables { get; set; } + [JsonProperty(PropertyName = ENVIRONMENT_VARIABLE_KEY)] + public Dictionary EnvironmentVariables + { + get + { + if (_serializedAnnotations != null && _environmentVariables.Count == 0) + { + foreach (BacktraceJProperty keys in _serializedAnnotations[ENVIRONMENT_VARIABLE_KEY]) + { + _environmentVariables.Add(keys.Name, keys.Value.Value()); + } + return _environmentVariables; + } + else + { + return _environmentVariables; + } + } + } /// /// Get built-in complex attributes @@ -37,7 +57,7 @@ public Annotations(Dictionary complexAttributes) { var environment = new EnvironmentVariables(); ComplexAttributes = complexAttributes; - EnvironmentVariables = environment.Variables; + _environmentVariables = environment.Variables; } public void FromJson(JToken jtoken) @@ -58,7 +78,8 @@ public BacktraceJObject ToJson() { envVariables[envVariable.Key] = envVariable.Value?.ToString() ?? string.Empty; } - annotations["Environment Variables"] = envVariables; + annotations[ENVIRONMENT_VARIABLE_KEY] = envVariables; + var activeScene = SceneManager.GetActiveScene(); if (activeScene != null) { @@ -66,27 +87,13 @@ public BacktraceJObject ToJson() var rootObjects = new List(); activeScene.GetRootGameObjects(rootObjects); - for (int i = 0; i < rootObjects.Count; ++i) + foreach (var objects in rootObjects) { - // https://docs.unity3d.com/ScriptReference/GameObject.html - // game object properties - var gameObject = new BacktraceJObject() + gameObjects.Add(ConvertGameObject(objects)); + if (gameObjects.Count > 30) { - ["name"] = rootObjects[i].name, - ["isStatic"] = rootObjects[i].isStatic, - ["layer"] = rootObjects[i].layer, - ["tag"] = rootObjects[i].tag, - ["transform.position"] = rootObjects[i].transform?.position.ToString() ?? "", - ["transform.rotation"] = rootObjects[i].transform?.rotation.ToString() ?? "", - ["tag"] = rootObjects[i].tag, - ["tag"] = rootObjects[i].tag, - ["activeInHierarchy"] = rootObjects[i].activeInHierarchy, - ["activeSelf"] = rootObjects[i].activeSelf, - ["hideFlags"] = (int)rootObjects[i].hideFlags, - ["instanceId"] = rootObjects[i].GetInstanceID(), - - }; - gameObjects.Add(gameObject); + break; + } } annotations["Game objects"] = gameObjects; } @@ -95,10 +102,77 @@ public BacktraceJObject ToJson() } public static Annotations Deserialize(JToken token) - { + { var annotations = new Annotations(); annotations.FromJson(token); return annotations; } + + private BacktraceJObject ConvertGameObject(GameObject gameObject) + { + if (gameObject == null) + { + return new BacktraceJObject(); + } + var jGameObject = GetJObject(gameObject); + var innerObjects = new JArray(); + + foreach (RectTransform childObject in gameObject.transform) + { + innerObjects.Add(ConvertGameObject(childObject, gameObject.name)); + } + jGameObject["childrens"] = innerObjects; + return jGameObject; + } + + private BacktraceJObject ConvertGameObject(RectTransform gameObject, string parentName) + { + var result = GetJObject(gameObject, parentName); + var innerObjects = new JArray(); + + foreach (RectTransform childObject in gameObject.transform) + { + innerObjects.Add(ConvertGameObject(childObject, gameObject.name)); + } + result["childrens"] = innerObjects; + return result; + } + + private BacktraceJObject GetJObject(GameObject gameObject, string parentName = "") + { + return new BacktraceJObject() + { + ["name"] = gameObject.name, + ["isStatic"] = gameObject.isStatic, + ["layer"] = gameObject.layer, + ["tag"] = gameObject.tag, + ["transform.position"] = gameObject.transform?.position.ToString() ?? "", + ["transform.rotation"] = gameObject.transform?.rotation.ToString() ?? "", + ["tag"] = gameObject.tag, + ["activeInHierarchy"] = gameObject.activeInHierarchy, + ["activeSelf"] = gameObject.activeSelf, + ["hideFlags"] = (int)gameObject.hideFlags, + ["instanceId"] = gameObject.GetInstanceID(), + ["parnetName"] = string.IsNullOrEmpty(parentName) ? "root object" : parentName + }; + } + + private BacktraceJObject GetJObject(RectTransform gameObject, string parentName = "") + { + return new BacktraceJObject() + { + ["name"] = gameObject.name, + ["tag"] = gameObject.tag, + ["transform.position"] = gameObject.transform?.position.ToString() ?? "", + ["transform.rotation"] = gameObject.transform?.rotation.ToString() ?? "", + ["tag"] = gameObject.tag, + ["hideFlags"] = (int)gameObject.hideFlags, + ["instanceId"] = gameObject.GetInstanceID(), + ["parnetName"] = string.IsNullOrEmpty(parentName) ? "root object" : parentName + }; + } + + + } } diff --git a/src/Model/JsonData/BacktraceAttributes.cs b/src/Model/JsonData/BacktraceAttributes.cs index 5527679f..d05451b5 100644 --- a/src/Model/JsonData/BacktraceAttributes.cs +++ b/src/Model/JsonData/BacktraceAttributes.cs @@ -20,6 +20,8 @@ public class BacktraceAttributes /// public Dictionary Attributes = new Dictionary(); + internal const string APPLICATION_ATTRIBUTE_NAME = "application"; + /// /// Get built-in complex attributes /// @@ -32,6 +34,10 @@ public class BacktraceAttributes /// Client's attributes (report and client) public BacktraceAttributes(BacktraceReport report, Dictionary clientAttributes) { + if(clientAttributes == null) + { + clientAttributes = new Dictionary(); + } if (report != null) { ConvertAttributes(report, clientAttributes); @@ -90,7 +96,7 @@ private void SetLibraryAttributes(BacktraceReport report) //A unique identifier of a machine Attributes["guid"] = GenerateMachineId(); //Base name of application generating the report - Attributes["application"] = Application.productName; + Attributes[APPLICATION_ATTRIBUTE_NAME] = Application.productName; Attributes["application.version"] = Application.version; Attributes["application.url"] = Application.absoluteURL; Attributes["application.company.name"] = Application.companyName; diff --git a/src/Services/BacktraceApi.cs b/src/Services/BacktraceApi.cs index 5a27aac3..aa236e71 100644 --- a/src/Services/BacktraceApi.cs +++ b/src/Services/BacktraceApi.cs @@ -29,12 +29,11 @@ internal class BacktraceApi : IBacktraceApi /// public Action OnServerResponse { get; set; } - internal readonly ReportLimitWatcher reportLimitWatcher; /// /// Url to server /// - private readonly string _serverurl; + private readonly Uri _serverurl; private readonly bool _ignoreSslValidation; @@ -43,16 +42,11 @@ internal class BacktraceApi : IBacktraceApi /// Create a new instance of Backtrace API /// /// API credentials - public BacktraceApi(BacktraceCredentials credentials, uint reportPerMin = 3, bool ignoreSslValidation = false) + public BacktraceApi(BacktraceCredentials credentials, bool ignoreSslValidation = false) { - if (credentials == null) - { - throw new ArgumentException($"{nameof(BacktraceCredentials)} cannot be null"); - } - _credentials = credentials; + _credentials = credentials ?? throw new ArgumentException($"{nameof(BacktraceCredentials)} cannot be null"); _ignoreSslValidation = ignoreSslValidation; - _serverurl = credentials.GetSubmissionUrl().ToString(); - reportLimitWatcher = new ReportLimitWatcher(reportPerMin); + _serverurl = credentials.GetSubmissionUrl(); } /// @@ -62,12 +56,6 @@ public BacktraceApi(BacktraceCredentials credentials, uint reportPerMin = 3, boo /// Server response public IEnumerator Send(BacktraceData data, Action callback = null) { - //check rate limiting - bool watcherValidation = reportLimitWatcher.WatchReport(data.Report); - if (!watcherValidation) - { - yield return BacktraceResult.OnLimitReached(data.Report); - } if (data == null) { yield return new BacktraceResult() @@ -75,17 +63,26 @@ public IEnumerator Send(BacktraceData data, Action callback = n Status = Types.BacktraceResultStatus.LimitReached }; } - if(RequestHandler != null) + if (RequestHandler != null) { - yield return RequestHandler.Invoke(_serverurl, data); + yield return RequestHandler.Invoke(_serverurl.ToString(), data); + } + else + { + string json = data.ToJson(); + yield return Send(json, data.Attachments, data.Report, data.Deduplication, callback); } - string json = data.ToJson(); - yield return Send(json, data.Attachments, data.Report, callback); } - private IEnumerator Send(string json, List attachments, BacktraceReport report, Action callback) + private IEnumerator Send(string json, List attachments, BacktraceReport report, int deduplication, Action callback) { - using (var request = new UnityWebRequest(_serverurl, "POST")) + var requestUrl = _serverurl.ToString(); + if (deduplication > 0) + { + var startingChar = string.IsNullOrEmpty(_serverurl.Query) ? "?" : "&"; + requestUrl += $"{startingChar}_mod_duplicate={deduplication}"; + } + using (var request = new UnityWebRequest(requestUrl, "POST")) { if (_ignoreSslValidation) { @@ -96,7 +93,7 @@ private IEnumerator Send(string json, List attachments, BacktraceReport request.downloadHandler = (DownloadHandler)new DownloadHandlerBuffer(); request.SetRequestHeader("Content-Type", "application/json"); yield return request.SendWebRequest(); - + BacktraceResult result; if (request.responseCode == 200) { @@ -194,15 +191,5 @@ private static string UrlEncode(string value) } return sb.ToString(); } - - public void SetClientRateLimitEvent(Action onClientReportLimitReached) - { - reportLimitWatcher.OnClientReportLimitReached = onClientReportLimitReached; - } - - public void SetClientRateLimit(uint rateLimit) - { - reportLimitWatcher.SetClientReportLimit(rateLimit); - } } } \ No newline at end of file diff --git a/src/Services/BacktraceDatabaseContext.cs b/src/Services/BacktraceDatabaseContext.cs index ad809cc7..70e0e04f 100644 --- a/src/Services/BacktraceDatabaseContext.cs +++ b/src/Services/BacktraceDatabaseContext.cs @@ -1,9 +1,11 @@ -using Backtrace.Unity.Interfaces; +using Backtrace.Unity.Common; +using Backtrace.Unity.Interfaces; using Backtrace.Unity.Model; using Backtrace.Unity.Model.Database; using Backtrace.Unity.Types; using System; using System.Collections.Generic; +using System.IO; using System.Linq; namespace Backtrace.Unity.Services @@ -11,12 +13,12 @@ namespace Backtrace.Unity.Services /// /// Backtrace Database Context /// - internal class BacktraceDatabaseContext : IBacktraceDatabaseContext + public class BacktraceDatabaseContext : IBacktraceDatabaseContext { /// /// Database cache /// - internal Dictionary> BatchRetry = new Dictionary>(); + public Dictionary> BatchRetry = new Dictionary>(); /// /// Total database size on hard drive @@ -43,17 +45,36 @@ internal class BacktraceDatabaseContext : IBacktraceDatabaseContext /// internal RetryOrder RetryOrder { get; set; } + /// + /// Deduplicaiton strategy + /// + public DeduplicationStrategy DeduplicationStrategy { get; set; } + + + /// + /// Initialize new instance of Backtrace Database Context + /// + /// Database settings + public BacktraceDatabaseContext(BacktraceDatabaseSettings settings) + : this(settings.DatabasePath, settings.RetryLimit, settings.RetryOrder, settings.DeduplicationStrategy) + { } + /// /// Initialize new instance of Backtrace Database Context /// /// Path to database directory /// Total number of retries /// Record order - public BacktraceDatabaseContext(string path, uint retryNumber, RetryOrder retryOrder) + public BacktraceDatabaseContext( + string path, + uint retryNumber, + RetryOrder retryOrder, + DeduplicationStrategy deduplicationStrategy = DeduplicationStrategy.None) { _path = path; _retryNumber = checked((int)retryNumber); RetryOrder = retryOrder; + DeduplicationStrategy = deduplicationStrategy; SetupBatch(); } @@ -72,24 +93,83 @@ private void SetupBatch() } } + /// + /// Generate hash for current diagnostic data + /// + /// Diagnostic data + /// hash for current backtrace data + private string GetHash(BacktraceData backtraceData) + { + var fingerprint = backtraceData?.Report.Fingerprint ?? string.Empty; + if (!string.IsNullOrEmpty(fingerprint)) + { + return fingerprint; + } + if (DeduplicationStrategy == DeduplicationStrategy.None) + { + return string.Empty; + } + + var deduplicationModel = new DeduplicationModel(backtraceData, DeduplicationStrategy); + return deduplicationModel.GetSha(); + } + /// /// Add new record to database /// /// Diagnostic data that should be stored in database /// New instance of DatabaseRecordy - public BacktraceDatabaseRecord Add(BacktraceData backtraceData) + public BacktraceDatabaseRecord Add(BacktraceData backtraceData, MiniDumpType miniDumpType = MiniDumpType.None) { if (backtraceData == null) { throw new NullReferenceException(nameof(backtraceData)); } - //create new record and save it on hard drive - var record = new BacktraceDatabaseRecord(backtraceData, _path); - record.Save(); + + string hash = GetHash(backtraceData); + if (!string.IsNullOrEmpty(hash)) + { + var existRecord = BatchRetry.SelectMany(n => n.Value) + .FirstOrDefault(n => n.Hash == hash); + + if (existRecord != null) + { + existRecord.Locked = true; + existRecord.Increment(); + TotalRecords++; + return existRecord; + } + } + + string minidumpPath = GenerateMiniDump(backtraceData.Report, miniDumpType); + backtraceData.Report.SetMinidumpPath(minidumpPath); + if (!string.IsNullOrEmpty(minidumpPath)) + { + backtraceData.Attachments.Add(minidumpPath); + } + + var record = ConvertToRecord(backtraceData, hash); //add record to database context return Add(record); } + /// + /// Convert Backtrace data to Backtrace record and save it. + /// + /// Backtrace data + /// deduplicaiton hash + /// + protected virtual BacktraceDatabaseRecord ConvertToRecord(BacktraceData backtraceData, string hash) + { + //create new record and save it on hard drive + var record = new BacktraceDatabaseRecord(backtraceData, _path) + { + Hash = hash + }; + record.Save(); + return record; + } + /// /// Add existing record to database /// @@ -152,7 +232,14 @@ public void Delete(BacktraceDatabaseRecord record) //delete value from current batch BatchRetry[key].Remove(value); //decrement all records - TotalRecords--; + if (value.Count > 0) + { + TotalRecords = TotalRecords - value.Count; + } + else + { + TotalRecords--; + } //decrement total size of database TotalSize -= value.Size; System.Diagnostics.Debug.WriteLine($"[Delete] :: Total Size = {TotalSize}"); @@ -182,7 +269,14 @@ public bool RemoveLastRecord() if (record != null) { record.Delete(); - TotalRecords--; + if (record.Count > 0) + { + TotalRecords = TotalRecords - record.Count; + } + else + { + TotalRecords--; + } TotalSize -= record.Size; System.Diagnostics.Debug.WriteLine($"[RemoveLastRecord] :: Total Size = {TotalSize}"); return true; @@ -216,7 +310,14 @@ private void RemoveMaxRetries() if (value.Valid()) { value.Delete(); - TotalRecords--; + if (value.Count > 0) + { + TotalRecords = TotalRecords - value.Count; + } + else + { + TotalRecords--; + } //decrement total size of database System.Diagnostics.Debug.WriteLine($"[RemoveMaxRetries]::BeforeDelete Total size: {TotalSize}. Record Size: {value.Size} "); TotalSize -= value.Size; @@ -240,7 +341,7 @@ public IEnumerable Get() /// public int Count() { - return TotalRecords; + return BatchRetry.SelectMany(n => n.Value).Sum(n => n.Count); } /// @@ -293,6 +394,18 @@ public BacktraceDatabaseRecord FirstOrDefault() : GetLastRecord(); } + /// + /// Get first Backtrace database record by predicate funciton + /// + /// Filter function + /// Backtrace Database record + public BacktraceDatabaseRecord FirstOrDefault(Func predicate) + { + return BatchRetry + .SelectMany(n => n.Value) + .FirstOrDefault(predicate); + } + /// /// Get first record in in-cache BacktraceDatabase /// @@ -308,7 +421,7 @@ private BacktraceDatabaseRecord GetFirstRecord() if (BatchRetry.ContainsKey(i) && BatchRetry[i].Any(n => !n.Locked)) { var record = BatchRetry[i].FirstOrDefault(n => !n.Locked); - if(record == null) + if (record == null) { return null; } @@ -352,7 +465,37 @@ public long GetSize() /// Total number of records public int GetTotalNumberOfRecords() { - return TotalRecords; + return Count(); + } + + + /// + /// Create new minidump file in database directory path. Minidump file name is a random Guid + /// + /// Current report + /// Generated minidump type + /// Path to minidump file + internal virtual string GenerateMiniDump(BacktraceReport backtraceReport, MiniDumpType miniDumpType) + { + if (miniDumpType == MiniDumpType.None) + { + return string.Empty; + } + //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, $"{backtraceReport.Uuid}-dump.dmp"); + MinidumpException minidumpExceptionType = backtraceReport.ExceptionTypeReport + ? MinidumpException.Present + : MinidumpException.None; + + bool minidumpSaved = MinidumpHelper.Write( + filePath: minidumpDestinationPath, + options: miniDumpType, + exceptionType: minidumpExceptionType); + + return minidumpSaved + ? minidumpDestinationPath + : string.Empty; } } } diff --git a/src/Services/BacktraceDatabaseFileContext.cs b/src/Services/BacktraceDatabaseFileContext.cs index 8b31b007..6ba7b758 100644 --- a/src/Services/BacktraceDatabaseFileContext.cs +++ b/src/Services/BacktraceDatabaseFileContext.cs @@ -74,7 +74,7 @@ public IEnumerable GetRecords() /// public void RemoveOrphaned(IEnumerable existingRecords) { - IEnumerable recordStringIds = existingRecords.Select(n => n.Id.ToString()); + var recordStringIds = existingRecords.Select(n => n.Id.ToString()); var files = GetAll(); for (int fileIndex = 0; fileIndex < files.Count(); fileIndex++) { diff --git a/src/Services/ReportLimitWatcher.cs b/src/Services/ReportLimitWatcher.cs index 1cc37a12..79fa2079 100644 --- a/src/Services/ReportLimitWatcher.cs +++ b/src/Services/ReportLimitWatcher.cs @@ -7,17 +7,26 @@ namespace Backtrace.Unity.Services /// /// Report watcher class. Watcher controls number of reports sending per one minute. If value reportPerMin is equal to zero, there is no request sending to API. Value has to be greater than or equal to 0 /// - internal class ReportLimitWatcher + public class ReportLimitWatcher { /// - /// Set event executed when client site report limit reached + /// Report timestamp queue. ReportLimitWatcher store events timestamp in _reportQueue + /// to validate number of reports that Backtarce integration will send per minute. /// - internal Action OnClientReportLimitReached = null; + internal readonly Queue _reportQueue; - internal readonly Queue _reportQue; + /// + /// Time period used to clear values from report queue. + /// + private readonly long _queueReportTime = 60; - private readonly long _queReportTime = 60; + /// + /// Determine if watcher is enabled. + /// private bool _watcherEnable; + /// + /// Determine how many reports class instance can store in report queue. + /// private int _reportPerSec; @@ -25,14 +34,14 @@ internal class ReportLimitWatcher /// Create new instance of background watcher /// /// How many times per minute watcher can send a report - public ReportLimitWatcher(uint reportPerMin) + internal ReportLimitWatcher(uint reportPerMin) { if (reportPerMin < 0) { throw new ArgumentException($"{nameof(reportPerMin)} have to be greater than or equal to zero"); } int reportNumber = checked((int)reportPerMin); - _reportQue = new Queue(reportNumber); + _reportQueue = new Queue(reportNumber); _reportPerSec = reportNumber; _watcherEnable = reportPerMin != 0; } @@ -44,12 +53,13 @@ internal void SetClientReportLimit(uint reportPerMin) _watcherEnable = reportPerMin != 0; } + /// /// Check if user can send new report to a Backtrace API /// /// Current report /// true if user can add a new report - public bool WatchReport(BacktraceReport report) + public bool WatchReport(long timestamp) { if (!_watcherEnable) { @@ -57,15 +67,26 @@ public bool WatchReport(BacktraceReport report) } //clear all reports older than _queReportTime Clear(); - if (_reportQue.Count + 1 > _reportPerSec) + if (_reportQueue.Count + 1 > _reportPerSec) { - OnClientReportLimitReached?.Invoke(report); return false; } - _reportQue.Enqueue(report.Timestamp); + _reportQueue.Enqueue(timestamp); return true; } + /// + /// Check if user can send new report to a Backtrace API + /// + /// Current report + /// true if user can add a new report + public bool WatchReport(BacktraceReport report) + { + return WatchReport(report.Timestamp); + } + + + /// /// Remove all records with timestamp older than one minute from now /// @@ -73,13 +94,13 @@ private void Clear() { long currentTime = (long)(DateTime.UtcNow.Subtract(new DateTime(1970, 1, 1))).TotalSeconds; bool clear = false; - while (!clear && _reportQue.Count != 0) + while (!clear && _reportQueue.Count != 0) { - var item = _reportQue.Peek(); - clear = !(currentTime - item >= _queReportTime); + var item = _reportQueue.Peek(); + clear = !(currentTime - item >= _queueReportTime); if (!clear) { - _reportQue.Dequeue(); + _reportQueue.Dequeue(); } } } @@ -89,7 +110,7 @@ private void Clear() /// internal void Reset() { - _reportQue.Clear(); + _reportQueue.Clear(); } } diff --git a/src/Types/DeduplicationStrategy.cs b/src/Types/DeduplicationStrategy.cs new file mode 100644 index 00000000..b83b1b58 --- /dev/null +++ b/src/Types/DeduplicationStrategy.cs @@ -0,0 +1,29 @@ +using System; + +namespace Backtrace.Unity.Types +{ + + /// + /// Determine deduplication strategy + /// + [Flags] + public enum DeduplicationStrategy + { + /// + /// Ignore deduplication strategy + /// + None = 0, + /// + /// Only stack trace + /// + Default = 1, + /// + /// Stack trace and exception type + /// + Classifier = 2, + /// + /// Stack trace and exception message + /// + Message = 4 + } +} \ No newline at end of file diff --git a/src/Types/DeduplicationStrategy.cs.meta b/src/Types/DeduplicationStrategy.cs.meta new file mode 100644 index 00000000..ccdba5ff --- /dev/null +++ b/src/Types/DeduplicationStrategy.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 5d5a13c2fef3c6049944ddf8ce67e461 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/src/artifacts/Debug.meta b/src/artifacts/Debug.meta deleted file mode 100644 index 49293a50..00000000 --- a/src/artifacts/Debug.meta +++ /dev/null @@ -1,8 +0,0 @@ -fileFormatVersion: 2 -guid: 9580199c9b725e042823c6ff86ef494b -folderAsset: yes -DefaultImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: