From dbea10ac7e4efae7c6a806f994a8bbb0df81516e Mon Sep 17 00:00:00 2001 From: kdysput Date: Tue, 4 May 2021 14:02:07 +0200 Subject: [PATCH 01/42] Breadcrumbs support: before database operations cleanup --- Editor/BacktraceConfigurationEditor.cs | 70 +- Editor/BacktraceConfigurationLabels.cs | 6 +- Runtime/BacktraceClient.cs | 55 +- Runtime/BacktraceDatabase.cs | 25 +- Runtime/Interfaces/IBacktraceClient.cs | 7 +- Runtime/Interfaces/IBacktraceDatabase.cs | 12 + Runtime/Model/BacktraceConfiguration.cs | 698 +++++++++--------- Runtime/Model/BacktraceData.cs | 10 - Runtime/Model/BacktraceLogManager.cs | 110 --- Runtime/Model/BacktraceReport.cs | 25 - Runtime/Model/BacktraceSourceCode.cs | 47 -- .../Model/Breadcrumbs.meta | 2 +- .../Model/Breadcrumbs/BacktraceBreadcrumbs.cs | 186 +++++ .../Breadcrumbs/BacktraceBreadcrumbs.cs.meta | 2 +- .../BacktraceBreadcrumbsEventHandler.cs | 117 +++ .../BacktraceBreadcrumbsEventHandler.cs.meta} | 2 +- .../Breadcrumbs/BacktraceBreadcrumbsLevel.cs | 20 + .../BacktraceBreadcrumbsLevel.cs.meta | 2 +- .../Breadcrumbs/BacktraceStorageLogManager.cs | 284 +++++++ .../BacktraceStorageLogManager.cs.meta} | 2 +- Runtime/Model/Breadcrumbs/BreadcrumbLevel.cs | 13 + .../Model/Breadcrumbs/BreadcrumbLevel.cs.meta | 11 + .../Breadcrumbs/IBacktraceBreadcrumbs.cs | 31 + .../Breadcrumbs/IBacktraceBreadcrumbs.cs.meta | 11 + .../Model/Breadcrumbs/IBacktraceLogManager.cs | 13 + .../Breadcrumbs/IBacktraceLogManager.cs.meta | 11 + .../Model/Breadcrumbs/UnityEngineLogLevel.cs | 17 + .../Breadcrumbs/UnityEngineLogLevel.cs.meta | 11 + .../Services/BacktraceDatabaseFileContext.cs | 7 + Tests/Runtime/ClientSendTests.cs | 26 - Tests/Runtime/SourceCode/LogManagerTests.cs | 165 ----- .../SourceCodeFlowWithLogManagerTests.cs | 233 ------ .../SourceCodeFlowWithoutLogManagerTests.cs | 200 ----- ...urceCodeFlowWithoutLogManagerTests.cs.meta | 11 - 34 files changed, 1218 insertions(+), 1224 deletions(-) delete mode 100644 Runtime/Model/BacktraceLogManager.cs delete mode 100644 Runtime/Model/BacktraceSourceCode.cs rename Tests/Runtime/SourceCode.meta => Runtime/Model/Breadcrumbs.meta (77%) create mode 100644 Runtime/Model/Breadcrumbs/BacktraceBreadcrumbs.cs rename Tests/Runtime/SourceCode/SourceCodeFlowWithLogManagerTests.cs.meta => Runtime/Model/Breadcrumbs/BacktraceBreadcrumbs.cs.meta (83%) create mode 100644 Runtime/Model/Breadcrumbs/BacktraceBreadcrumbsEventHandler.cs rename Runtime/Model/{BacktraceLogManager.cs.meta => Breadcrumbs/BacktraceBreadcrumbsEventHandler.cs.meta} (83%) create mode 100644 Runtime/Model/Breadcrumbs/BacktraceBreadcrumbsLevel.cs rename Tests/Runtime/SourceCode/LogManagerTests.cs.meta => Runtime/Model/Breadcrumbs/BacktraceBreadcrumbsLevel.cs.meta (83%) create mode 100644 Runtime/Model/Breadcrumbs/BacktraceStorageLogManager.cs rename Runtime/Model/{BacktraceSourceCode.cs.meta => Breadcrumbs/BacktraceStorageLogManager.cs.meta} (83%) create mode 100644 Runtime/Model/Breadcrumbs/BreadcrumbLevel.cs create mode 100644 Runtime/Model/Breadcrumbs/BreadcrumbLevel.cs.meta create mode 100644 Runtime/Model/Breadcrumbs/IBacktraceBreadcrumbs.cs create mode 100644 Runtime/Model/Breadcrumbs/IBacktraceBreadcrumbs.cs.meta create mode 100644 Runtime/Model/Breadcrumbs/IBacktraceLogManager.cs create mode 100644 Runtime/Model/Breadcrumbs/IBacktraceLogManager.cs.meta create mode 100644 Runtime/Model/Breadcrumbs/UnityEngineLogLevel.cs create mode 100644 Runtime/Model/Breadcrumbs/UnityEngineLogLevel.cs.meta delete mode 100644 Tests/Runtime/SourceCode/LogManagerTests.cs delete mode 100644 Tests/Runtime/SourceCode/SourceCodeFlowWithLogManagerTests.cs delete mode 100644 Tests/Runtime/SourceCode/SourceCodeFlowWithoutLogManagerTests.cs delete mode 100644 Tests/Runtime/SourceCode/SourceCodeFlowWithoutLogManagerTests.cs.meta diff --git a/Editor/BacktraceConfigurationEditor.cs b/Editor/BacktraceConfigurationEditor.cs index 365265be..8074373f 100644 --- a/Editor/BacktraceConfigurationEditor.cs +++ b/Editor/BacktraceConfigurationEditor.cs @@ -8,6 +8,7 @@ namespace Backtrace.Unity.Editor [CustomEditor(typeof(BacktraceConfiguration))] public class BacktraceConfigurationEditor : UnityEditor.Editor { + protected static bool showBreadcrumbsSettings = false; protected static bool showEventAggregationSettings = false; protected static bool showClientAdvancedSettings = false; protected static bool showDatabaseSettings = false; @@ -70,10 +71,6 @@ public override void OnInspectorGUI() serializedObject.FindProperty("ReportFilterType"), new GUIContent(BacktraceConfigurationLabels.LABEL_REPORT_FILTER)); - EditorGUILayout.PropertyField( - serializedObject.FindProperty("NumberOfLogs"), - new GUIContent(BacktraceConfigurationLabels.LABEL_NUMBER_OF_LOGS)); - EditorGUILayout.PropertyField( serializedObject.FindProperty("PerformanceStatistics"), new GUIContent(BacktraceConfigurationLabels.LABEL_PERFORMANCE_STATISTICS)); @@ -165,43 +162,64 @@ public override void OnInspectorGUI() new GUIContent(BacktraceConfigurationLabels.LABEL_ADD_UNITY_LOG)); #endif + } + + GUIStyle breadcrumbsSupportFoldout = new GUIStyle(EditorStyles.foldout); + showBreadcrumbsSettings = EditorGUILayout.Foldout(showBreadcrumbsSettings, BacktraceConfigurationLabels.LABEL_BREADCRUMBS_SECTION, breadcrumbsSupportFoldout); + if (showBreadcrumbsSettings) + { + var enableBreadcrumbsSupport = serializedObject.FindProperty("EnableBreadcrumbsSupport"); + EditorGUILayout.PropertyField( + enableBreadcrumbsSupport, + new GUIContent(BacktraceConfigurationLabels.LABEL_ENABLE_BREADCRUMBS)); + + if (enableBreadcrumbsSupport.boolValue) + { + EditorGUILayout.PropertyField( + serializedObject.FindProperty("BacktraceBreadcrumbsLevel"), + new GUIContent(BacktraceConfigurationLabels.LABEL_BREADCRUMBS_EVENTS)); + + EditorGUILayout.PropertyField( + serializedObject.FindProperty("LogLevel"), + new GUIContent(BacktraceConfigurationLabels.LABEL_BREADCRUMNS_LOG_LEVEL)); + } + } #if UNITY_ANDROID || UNITY_IOS EditorGUILayout.PropertyField( serializedObject.FindProperty("CaptureNativeCrashes"), new GUIContent(BacktraceConfigurationLabels.CAPTURE_NATIVE_CRASHES)); #endif - EditorGUILayout.PropertyField( - serializedObject.FindProperty("AutoSendMode"), - new GUIContent(BacktraceConfigurationLabels.LABEL_AUTO_SEND_MODE)); + EditorGUILayout.PropertyField( + serializedObject.FindProperty("AutoSendMode"), + new GUIContent(BacktraceConfigurationLabels.LABEL_AUTO_SEND_MODE)); - EditorGUILayout.PropertyField( - serializedObject.FindProperty("CreateDatabase"), - new GUIContent(BacktraceConfigurationLabels.LABEL_CREATE_DATABASE_DIRECTORY)); + EditorGUILayout.PropertyField( + serializedObject.FindProperty("CreateDatabase"), + new GUIContent(BacktraceConfigurationLabels.LABEL_CREATE_DATABASE_DIRECTORY)); - EditorGUILayout.PropertyField( - serializedObject.FindProperty("GenerateScreenshotOnException"), - new GUIContent(BacktraceConfigurationLabels.LABEL_GENERATE_SCREENSHOT_ON_EXCEPTION)); + EditorGUILayout.PropertyField( + serializedObject.FindProperty("GenerateScreenshotOnException"), + new GUIContent(BacktraceConfigurationLabels.LABEL_GENERATE_SCREENSHOT_ON_EXCEPTION)); - SerializedProperty maxRecordCount = serializedObject.FindProperty("MaxRecordCount"); - EditorGUILayout.PropertyField(maxRecordCount, new GUIContent(BacktraceConfigurationLabels.LABEL_MAX_REPORT_COUNT)); + SerializedProperty maxRecordCount = serializedObject.FindProperty("MaxRecordCount"); + EditorGUILayout.PropertyField(maxRecordCount, new GUIContent(BacktraceConfigurationLabels.LABEL_MAX_REPORT_COUNT)); - SerializedProperty maxDatabaseSize = serializedObject.FindProperty("MaxDatabaseSize"); - EditorGUILayout.PropertyField(maxDatabaseSize, new GUIContent(BacktraceConfigurationLabels.LABEL_MAX_DATABASE_SIZE)); + SerializedProperty maxDatabaseSize = serializedObject.FindProperty("MaxDatabaseSize"); + EditorGUILayout.PropertyField(maxDatabaseSize, new GUIContent(BacktraceConfigurationLabels.LABEL_MAX_DATABASE_SIZE)); - SerializedProperty retryInterval = serializedObject.FindProperty("RetryInterval"); - EditorGUILayout.PropertyField(retryInterval, new GUIContent(BacktraceConfigurationLabels.LABEL_RETRY_INTERVAL)); + SerializedProperty retryInterval = serializedObject.FindProperty("RetryInterval"); + EditorGUILayout.PropertyField(retryInterval, new GUIContent(BacktraceConfigurationLabels.LABEL_RETRY_INTERVAL)); - EditorGUILayout.LabelField("Backtrace database require at least one retry."); - SerializedProperty retryLimit = serializedObject.FindProperty("RetryLimit"); - EditorGUILayout.PropertyField(retryLimit, new GUIContent(BacktraceConfigurationLabels.LABEL_RETRY_LIMIT)); + EditorGUILayout.LabelField("Backtrace database require at least one retry."); + SerializedProperty retryLimit = serializedObject.FindProperty("RetryLimit"); + EditorGUILayout.PropertyField(retryLimit, new GUIContent(BacktraceConfigurationLabels.LABEL_RETRY_LIMIT)); - SerializedProperty retryOrder = serializedObject.FindProperty("RetryOrder"); - EditorGUILayout.PropertyField(retryOrder, new GUIContent(BacktraceConfigurationLabels.LABEL_RETRY_ORDER)); - } + SerializedProperty retryOrder = serializedObject.FindProperty("RetryOrder"); + EditorGUILayout.PropertyField(retryOrder, new GUIContent(BacktraceConfigurationLabels.LABEL_RETRY_ORDER)); } + serializedObject.ApplyModifiedProperties(); } } - } \ No newline at end of file diff --git a/Editor/BacktraceConfigurationLabels.cs b/Editor/BacktraceConfigurationLabels.cs index 95cdbfa6..3fb83767 100644 --- a/Editor/BacktraceConfigurationLabels.cs +++ b/Editor/BacktraceConfigurationLabels.cs @@ -23,11 +23,15 @@ internal static class BacktraceConfigurationLabels internal const string LABEL_EVENT_AGGREGATION_URL = "Event aggregation submission URL"; internal const string LABEL_EVENT_AGGREGATION_TIME_INTERVAL = "Event aggregation time interval in ms"; + internal const string LABEL_BREADCRUMBS_SECTION = "Breadcrumbs support"; + internal const string LABEL_ENABLE_BREADCRUMBS = "Enable breadcrumbs support"; + internal const string LABEL_BREADCRUMBS_EVENTS = "Breadcrumbs events type"; + internal const string LABEL_BREADCRUMNS_LOG_LEVEL = "Breadcrumbs log level"; + internal static string LABEL_REPORT_ATTACHMENTS = "Report attachment paths"; internal static string CAPTURE_NATIVE_CRASHES = "Capture native crashes"; internal static string LABEL_REPORT_FILTER = "Filter reports"; - internal static string LABEL_NUMBER_OF_LOGS = "Collect last n game logs"; internal static string LABEL_GAME_OBJECT_DEPTH = "Game object depth limit"; internal static string LABEL_IGNORE_SSL_VALIDATION = "Ignore SSL validation"; internal static string LABEL_SEND_UNHANDLED_GAME_CRASHES_ON_STARTUP = "Send unhandled native game crashes on startup"; diff --git a/Runtime/BacktraceClient.cs b/Runtime/BacktraceClient.cs index 83c53727..13804a6d 100644 --- a/Runtime/BacktraceClient.cs +++ b/Runtime/BacktraceClient.cs @@ -1,6 +1,7 @@ using Backtrace.Unity.Common; using Backtrace.Unity.Interfaces; using Backtrace.Unity.Model; +using Backtrace.Unity.Model.Breadcrumbs; using Backtrace.Unity.Model.Database; using Backtrace.Unity.Model.JsonData; using Backtrace.Unity.Runtime.Native; @@ -21,9 +22,21 @@ namespace Backtrace.Unity /// public class BacktraceClient : MonoBehaviour, IBacktraceClient { + public const string VERSION = "3.4.0"; + public BacktraceConfiguration Configuration; - public const string VERSION = "3.4.0"; + /// + /// Backtrace Breadcrumbs + /// + public IBacktraceBreadcrumbs Breadcrumbs + { + get + { + return Database?.Breadcrumbs; + } + } + public bool Enabled { get; private set; } /// @@ -295,9 +308,6 @@ internal ReportLimitWatcher ReportLimitWatcher } } - private BacktraceLogManager _backtraceLogManager; - - /// /// Initialize new Backtrace integration /// @@ -447,6 +457,7 @@ public void Refresh() Database.Reload(); Database.SetApi(BacktraceApi); Database.SetReportWatcher(_reportLimitWatcher); + EnableBreadcrumbsSupport(); } } @@ -465,6 +476,20 @@ public void Refresh() } } + public bool EnableBreadcrumbsSupport() + { + if (Database == null) + { + return false; + } + var initializationResult = Database.EnableBreadcrumbsSupport(); + if (initializationResult) + { + _clientReportAttachments.Add(Breadcrumbs.GetBreadcrumbLogPath()); + } + return initializationResult; + } + public void EnableSessionAgregationSupport(string submissionUrl, long timeIntervalInMs, uint maximumNumberOfEventsInStore) { if (Session != null) @@ -490,6 +515,7 @@ private void OnApplicationQuit() private void Awake() { + Breadcrumbs?.FromMonoBehavior("Application awake", LogType.Assert, null); Refresh(); } @@ -516,6 +542,8 @@ private void LateUpdate() private void OnDestroy() { Enabled = false; + Breadcrumbs?.FromMonoBehavior("Backtrace Client: OnDestroy", LogType.Warning, null); + Breadcrumbs?.UnregisterEvents(); Application.logMessageReceived -= HandleUnityMessage; Application.logMessageReceivedThreaded -= HandleUnityBackgroundException; #if UNITY_ANDROID || UNITY_IOS @@ -554,8 +582,8 @@ public void Send(string message, List attachmentPaths = null, Dictionary message: message, attachmentPaths: attachmentPaths, attributes: attributes); - _backtraceLogManager.Enqueue(report); + Breadcrumbs?.FromBacktrace(report); SendReport(report); } @@ -573,7 +601,7 @@ public void Send(Exception exception, List attachmentPaths = null, Dicti } var report = new BacktraceReport(exception, attributes, attachmentPaths); - _backtraceLogManager.Enqueue(report); + Breadcrumbs?.FromBacktrace(report); SendReport(report); } @@ -588,7 +616,7 @@ public void Send(BacktraceReport report, Action sendCallback = { return; } - _backtraceLogManager.Enqueue(report); + Breadcrumbs?.FromBacktrace(report); SendReport(report, sendCallback); } @@ -738,13 +766,6 @@ private BacktraceData SetupBacktraceData(BacktraceReport report) // normalized exception message instead environment stack trace // for exceptions without stack trace. report.SetReportFingerprint(Configuration.UseNormalizedExceptionMessage); - - // add environment information to backtrace report - var sourceCode = _backtraceLogManager.Disabled - ? new BacktraceUnityMessage(report).ToString() - : _backtraceLogManager.ToSourceCode(); - - report.AssignSourceCodeToReport(sourceCode); report.AttachmentPaths.AddRange(_clientReportAttachments); // pass copy of dictionary to prevent overriding client attributes @@ -766,6 +787,7 @@ internal void OnAnrDetected(string stackTrace) Debug.LogWarning("Please enable BacktraceClient first."); return; } + Breadcrumbs?.FromMonoBehavior("Application ANR Detected", LogType.Warning, null); const string anrMessage = "ANRException: Blocked thread detected"; _backtraceLogManager.Enqueue(new BacktraceUnityMessage(anrMessage, stackTrace, LogType.Error)); var hang = new BacktraceUnhandledException(anrMessage, stackTrace); @@ -780,8 +802,7 @@ internal void OnAnrDetected(string stackTrace) /// private void CaptureUnityMessages() { - _backtraceLogManager = new BacktraceLogManager(Configuration.NumberOfLogs); - if (Configuration.HandleUnhandledExceptions || Configuration.NumberOfLogs != 0) + if (Configuration.HandleUnhandledExceptions) { Application.logMessageReceived += HandleUnityMessage; Application.logMessageReceivedThreaded += HandleUnityBackgroundException; @@ -793,6 +814,7 @@ private void CaptureUnityMessages() internal void OnApplicationPause(bool pause) { + Breadcrumbs?.FromMonoBehavior("Application pause", LogType.Assert, new Dictionary { { "paused", pause.ToString(CultureInfo.InvariantCulture).ToLower() } }); _nativeClient?.PauseAnrThread(pause); } @@ -839,7 +861,6 @@ internal void HandleUnityMessage(string message, string stackTrace, LogType type return; } var unityMessage = new BacktraceUnityMessage(message, stackTrace, type); - _backtraceLogManager.Enqueue(unityMessage); if (Configuration.HandleUnhandledExceptions && unityMessage.IsUnhandledException()) { BacktraceUnhandledException exception = null; diff --git a/Runtime/BacktraceDatabase.cs b/Runtime/BacktraceDatabase.cs index 283e3801..bcdef5ee 100644 --- a/Runtime/BacktraceDatabase.cs +++ b/Runtime/BacktraceDatabase.cs @@ -1,6 +1,7 @@ using Backtrace.Unity.Common; using Backtrace.Unity.Interfaces; using Backtrace.Unity.Model; +using Backtrace.Unity.Model.Breadcrumbs; using Backtrace.Unity.Model.Database; using Backtrace.Unity.Services; using Backtrace.Unity.Types; @@ -20,7 +21,12 @@ public class BacktraceDatabase : MonoBehaviour, IBacktraceDatabase { private bool _timerBackgroundWork = false; - public BacktraceConfiguration Configuration; + public BacktraceConfiguration Configuration; + + /// + /// Backtrace Breadcrumbs + /// + public IBacktraceBreadcrumbs Breadcrumbs { get; private set; } internal static float LastFrameTime = 0; @@ -147,6 +153,7 @@ public void Reload() BacktraceDatabaseFileContext = new BacktraceDatabaseFileContext(DatabaseSettings.DatabasePath, DatabaseSettings.MaxDatabaseSize, DatabaseSettings.MaxRecordCount); BacktraceApi = new BacktraceApi(Configuration.ToCredentials()); _reportLimitWatcher = new ReportLimitWatcher(Convert.ToUInt32(Configuration.ReportPerMin)); + EnableBreadcrumbsSupport(); } @@ -592,6 +599,20 @@ public long GetDatabaseSize() public void SetReportWatcher(ReportLimitWatcher reportLimitWatcher) { _reportLimitWatcher = reportLimitWatcher; - } + } + + public bool EnableBreadcrumbsSupport() + { + if (!Enable || !Configuration.EnableBreadcrumbsSupport) + { + return false; + } + if (Breadcrumbs != null) + { + return true; + } + Breadcrumbs = new BacktraceBreadcrumbs(new BacktraceStorageLogManager(Configuration.GetFullDatabasePath())); + return Breadcrumbs.EnableBreadcrumbs(Configuration.BacktraceBreadcrumbsLevel, Configuration.LogLevel); + } } } diff --git a/Runtime/Interfaces/IBacktraceClient.cs b/Runtime/Interfaces/IBacktraceClient.cs index 6fa868f6..3f7fac6f 100644 --- a/Runtime/Interfaces/IBacktraceClient.cs +++ b/Runtime/Interfaces/IBacktraceClient.cs @@ -1,4 +1,5 @@ using Backtrace.Unity.Model; +using Backtrace.Unity.Model.Breadcrumbs; using System; using System.Collections.Generic; @@ -8,7 +9,11 @@ namespace Backtrace.Unity.Interfaces /// Backtrace client interface. Use this interface with dependency injection features /// public interface IBacktraceClient - { + { + /// + /// Backtrace Breadcrumbs + /// + IBacktraceBreadcrumbs Breadcrumbs { get; } /// /// Send a new report to a Backtrace API /// diff --git a/Runtime/Interfaces/IBacktraceDatabase.cs b/Runtime/Interfaces/IBacktraceDatabase.cs index 905fe48b..793ebccc 100644 --- a/Runtime/Interfaces/IBacktraceDatabase.cs +++ b/Runtime/Interfaces/IBacktraceDatabase.cs @@ -1,4 +1,5 @@ using Backtrace.Unity.Model; +using Backtrace.Unity.Model.Breadcrumbs; using Backtrace.Unity.Model.Database; using Backtrace.Unity.Services; using Backtrace.Unity.Types; @@ -13,6 +14,11 @@ namespace Backtrace.Unity.Interfaces /// public interface IBacktraceDatabase { + /// + /// Backtrace Breadcrumbs + /// + IBacktraceBreadcrumbs Breadcrumbs { get; } + /// /// Send all reports stored in BacktraceDatabase and clean database /// @@ -89,5 +95,11 @@ public interface IBacktraceDatabase /// /// true if BacktraceDatabase is enabled. Otherwise false. bool Enabled(); + + /// + /// Enables Breadcrumbs support + /// + /// True if the breadcrumbs file was initialized correctly. Otherwise false. + bool EnableBreadcrumbsSupport(); } } diff --git a/Runtime/Model/BacktraceConfiguration.cs b/Runtime/Model/BacktraceConfiguration.cs index 3de97d28..ae7f5bdd 100644 --- a/Runtime/Model/BacktraceConfiguration.cs +++ b/Runtime/Model/BacktraceConfiguration.cs @@ -1,346 +1,354 @@ -using Backtrace.Unity.Common; -using Backtrace.Unity.Types; -using System; -using System.Collections.Generic; -using System.IO; -using UnityEngine; - -namespace Backtrace.Unity.Model -{ - [Serializable] - [CreateAssetMenu(fileName = "Backtrace Configuration", menuName = "Backtrace/Configuration", order = 0)] - public class BacktraceConfiguration : ScriptableObject - { - /// - /// Backtrace server url - /// - [Header("Backtrace client configuration")] - [Tooltip("This field is required to submit exceptions from your Unity project to your Backtrace instance.\n \nMore 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?\n\nNOTE: the backtrace-unity plugin will expect full URL with token to your Backtrace instance.")] - public string ServerUrl; - - /// - /// Backtrace server API token - /// - public string Token; - - /// - /// Maximum number reports per minute - /// - [Tooltip("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. Default: 50")] - public int ReportPerMin = 50; - - /// - /// Determine if client should catch unhandled exceptions - /// - [Tooltip("Toggle this on or off to set the library to handle unhandled exceptions that are not captured by try-catch blocks.")] - public bool HandleUnhandledExceptions = true; - - /// - /// Determine if client should ignore ssl validation - /// - [Tooltip("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.")] - public bool IgnoreSslValidation = false; - - /// - /// Destroy Backtrace instances on new scene load. - /// - [Tooltip("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.")] - public bool DestroyOnLoad = false; - - /// - /// Sampling configuration - fractional sampling allows to drop some % of unhandled exception. - /// - [Tooltip("Log random sampling rate - Enables a random sampling mechanism for unhandled exceptions - by default sampling is equal to 0.01 - which means only 1% of randomply sampling reports will be send to Backtrace. \n" + - "* 1 - means 100% of unhandled exception reports will be reported by library,\n" + - "* 0.1 - means 10% of unhandled exception reports will be reported by library,\n" + - "* 0 - means library is going to drop all unhandled exception.")] - [Range(0, 1)] - public double Sampling = 0.01d; - - /// - /// Backtrace report filter type - /// - [Tooltip("Report filter allows to filter specific type of reports. Possible options:\n" + - "* Disable - Disable report filtering - send every type of report.\n" + - "* Message - Prevent message reports.\n" + - "* Exception - Prevent exception reports.\n" + - "* Unhandled exception- Prevent unhandled exception reports.\n" + - "* Hang - Prevent sending reports when game hang.")] - - public ReportFilterType ReportFilterType = ReportFilterType.None; - /// - /// Game object depth in Backtrace report - /// - [Tooltip("Allows developer to filter number of game object childrens in Backtrace report.")] - public int GameObjectDepth = -1; - - /// - /// Number of logs collected by Backtrace-Unity - /// - [Tooltip("Number of logs collected by Backtrace-Unity")] - public uint NumberOfLogs = 10; - - /// - /// Flag that allows to include performance statistics in Backtrace report - /// - [Tooltip("Enable performance statistics")] - public bool PerformanceStatistics = false; - - /// - /// Try to find game native crashes and send them on Game startup - /// - [Tooltip("Try to find game native crashes and send them on Game startup")] - public bool SendUnhandledGameCrashesOnGameStartup = true; - -#if UNITY_ANDROID || UNITY_IOS -#if UNITY_ANDROID - /// - /// Capture native NDK Crashes. - /// - [Tooltip("Capture native NDK Crashes (ANDROID API 21+)")] -#elif UNITY_IOS - /// - /// Capture native iOS Crashes. - /// - [Tooltip("Capture native Crashes")] -#endif - - public bool CaptureNativeCrashes = true; - /// - /// Handle ANR events - Application not responding - /// - [Tooltip("Handle ANR events - Application not responding")] - public bool HandleANR = true; - -#if UNITY_ANDROID - /// - /// Send Low memory warnings to Backtrace - /// - [Tooltip("(Early access) Send Low memory warnings to Backtrace")] -#elif UNITY_IOS - /// - /// Send Out of memory exceptions to Backtrace. - /// - [Tooltip("(Early access) Send Out of memory exceptions to Backtrace")] -#endif - public bool OomReports = false; - -#if UNITY_2019_2_OR_NEWER - /// - /// Symbols upload token - /// - [Tooltip("Symbols upload token required to upload symbols to Backtrace")] - public string SymbolsUploadToken = string.Empty; -#endif -#endif - - /// - /// Backtrace client deduplication strategy. - /// - [Tooltip("Client-side deduplication allows the backtrace-unity library to group multiple error reports into a single one based on various factors. Factors include:\n\n" + - "* Disable - Client-side deduplication rules are disabled.\n" + - "* Everything - Use all the options as a factor in client-side deduplication.\n" + - "* Faulting callstack - Use the faulting callstack as a factor in client-side deduplication.\n" + - "* Exception type - Use the exception type as a factor in client-side deduplication.\n" + - "* Exception message - Use the exception message as a factor in client-side deduplication.")] - - public DeduplicationStrategy DeduplicationStrategy = DeduplicationStrategy.None; - - - /// - /// Use normalized exception message instead environment stack trace, when exception doesn't have stack trace - /// - [Tooltip("If exception does not have a stack trace, use a normalized exception message to generate fingerprint.")] - public bool UseNormalizedExceptionMessage = false; - - /// - /// Determine minidump type support - minidump generation is supported on Windows. - /// - [Tooltip("Type of minidump that will be attached to Backtrace report in the report generated on Windows machine.")] - public MiniDumpType MinidumpType = MiniDumpType.None; - - /// - /// Generate game screen shot when exception happen - /// - [Tooltip("Generate and attach screenshot of frame as exception occurs")] - public bool GenerateScreenshotOnException = false; - - /// - /// List of path to attachments that Backtrace client will include in the native and managed reports. - /// - [Tooltip("List of path to attachments that Backtrace client will include in the native and managed reports.")] - public string[] AttachmentPaths; - - /// - /// Directory path where reports and minidumps are stored - /// - [Tooltip("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.")] - public string DatabasePath; - - /// - /// Enable event aggregation support - /// - [Tooltip("Enable default crash free events")] - public bool EnableEventAggregationSupport = false; - - /// - /// Event aggregation submission url - /// - [Tooltip("Event aggregation submission url")] - public string EventAggregationSubmissionUrl; - - /// - /// Time interval in ms - /// - [Range(0, 60)] - [Tooltip("Event aggregation time interval in min")] - public long TimeIntervalInMin = 30; - - /// - /// Maximum number of events in Event aggregation store - /// - [Tooltip("Maximum number of events stored by Backtrace")] - public uint MaximumNumberOfEvents = 10; - - /// - /// Determine if database is enable - /// - [Header("Backtrace database configuration")] - [Tooltip("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.")] - public bool Enabled; - - /// - /// Add Unity log file to Backtrace report - /// - [Tooltip("Add Unity player log file to Backtrace report")] - public bool AddUnityLogToReport = false; - - /// - /// Resend report when http client throw exception - /// - [Tooltip("When toggled on, the database will send automatically reports to Backtrace server based on the Retry Settings below. When toggled off, the developer will need to use the Flush method to attempt to send and clear. Recommend that this is toggled on.")] - public bool AutoSendMode = true; - - /// - /// Determine if BacktraceDatabase should try to create database directory on application start - /// - [Tooltip("If toggled, the library will create the offline database directory if the provided path doesn't exists.")] - public bool CreateDatabase = false; - - /// - /// Maximum number of stored reports in Database. If value is equal to zero, then limit not exists - /// - [Tooltip("This is one of two limits you can impose for controlling the growth of the offline store. This setting is the maximum number of stored reports in database. If value is equal to zero, then limit not exists, When the limit is reached, the database will remove the oldest entries.")] - public int MaxRecordCount = 8; - - /// - /// Database size in MB - /// - [Tooltip("This is the second limit you can impose for controlling the growth of the offline store. This setting is the maximum database size in MB. If value is equal to zero, then size is unlimited, When the limit is reached, the database will remove the oldest entries.")] - public long MaxDatabaseSize; - /// - /// How much seconds library should wait before next retry. - /// - [Tooltip("If the database is unable to send its record, this setting specifies how many seconds the library should wait between retries.")] - public int RetryInterval = 60; - - /// - /// Maximum number of retries - [Tooltip("If the database is unable to send its record, this setting specifies the maximum number of retries before the system gives up.")] - public int RetryLimit = 3; - - /// - /// Retry order - /// - [Tooltip("This specifies in which order records are sent to the Backtrace server.")] - public RetryOrder RetryOrder; - - /// - /// Get full paths to attachments added by client - /// - /// List of absolute path to attachments - public List GetAttachmentPaths() - { - var result = new List(); - if (AttachmentPaths == null || AttachmentPaths.Length == 0) - { - return result; - } - - foreach (var path in AttachmentPaths) - { - if (!string.IsNullOrEmpty(path)) - { - result.Add(ClientPathHelper.GetFullPath(path)); - } - } - return result; - } - - public string GetFullDatabasePath() - { - return ClientPathHelper.GetFullPath(DatabasePath); - } - public string CrashpadDatabasePath - { - get - { - if (!Enabled) - { - return string.Empty; - } - return Path.Combine(GetFullDatabasePath(), "crashpad"); - } - } - - public string GetValidServerUrl() - { - return UpdateServerUrl(ServerUrl); - } - - public static string UpdateServerUrl(string value) - { - //in case if user pass invalid string, copy value contain uri without method modifications - var copy = value; - - if (string.IsNullOrEmpty(value)) - { - return value; - } - - if (!value.StartsWith("http")) - { - value = string.Format("https://{0}", value); - } - string uriScheme = value.StartsWith("https://") - ? Uri.UriSchemeHttps - : Uri.UriSchemeHttp; - - if (!Uri.IsWellFormedUriString(value, UriKind.Absolute)) - { - return copy; - } - return new UriBuilder(value) { Scheme = uriScheme }.Uri.ToString(); - } - - public static bool ValidateServerUrl(string value) - { - return Uri.IsWellFormedUriString(UpdateServerUrl(value), UriKind.Absolute); - } - - public bool IsValid() - { - return ValidateServerUrl(ServerUrl); - } - - public long GetEventAggregationIntervalTimerInMs() - { - return TimeIntervalInMin * 60; - } - - public BacktraceCredentials ToCredentials() - { - return new BacktraceCredentials(ServerUrl); - } - } +using Backtrace.Unity.Common; +using Backtrace.Unity.Model.Breadcrumbs; +using Backtrace.Unity.Types; +using System; +using System.Collections.Generic; +using System.IO; +using UnityEngine; + +namespace Backtrace.Unity.Model +{ + [Serializable] + [CreateAssetMenu(fileName = "Backtrace Configuration", menuName = "Backtrace/Configuration", order = 0)] + public class BacktraceConfiguration : ScriptableObject + { + /// + /// Backtrace server url + /// + [Header("Backtrace client configuration")] + [Tooltip("This field is required to submit exceptions from your Unity project to your Backtrace instance.\n \nMore 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?\n\nNOTE: the backtrace-unity plugin will expect full URL with token to your Backtrace instance.")] + public string ServerUrl; + + /// + /// Backtrace server API token + /// + public string Token; + + /// + /// Maximum number reports per minute + /// + [Tooltip("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. Default: 50")] + public int ReportPerMin = 50; + + /// + /// Determine if client should catch unhandled exceptions + /// + [Tooltip("Toggle this on or off to set the library to handle unhandled exceptions that are not captured by try-catch blocks.")] + public bool HandleUnhandledExceptions = true; + + /// + /// Determine if client should ignore ssl validation + /// + [Tooltip("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.")] + public bool IgnoreSslValidation = false; + + /// + /// Destroy Backtrace instances on new scene load. + /// + [Tooltip("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.")] + public bool DestroyOnLoad = false; + + /// + /// Sampling configuration - fractional sampling allows to drop some % of unhandled exception. + /// + [Tooltip("Log random sampling rate - Enables a random sampling mechanism for unhandled exceptions - by default sampling is equal to 0.01 - which means only 1% of randomply sampling reports will be send to Backtrace. \n" + + "* 1 - means 100% of unhandled exception reports will be reported by library,\n" + + "* 0.1 - means 10% of unhandled exception reports will be reported by library,\n" + + "* 0 - means library is going to drop all unhandled exception.")] + [Range(0, 1)] + public double Sampling = 0.01d; + + /// + /// Backtrace report filter type + /// + [Tooltip("Report filter allows to filter specific type of reports. Possible options:\n" + + "* Disable - Disable report filtering - send every type of report.\n" + + "* Message - Prevent message reports.\n" + + "* Exception - Prevent exception reports.\n" + + "* Unhandled exception- Prevent unhandled exception reports.\n" + + "* Hang - Prevent sending reports when game hang.")] + + public ReportFilterType ReportFilterType = ReportFilterType.None; + /// + /// Game object depth in Backtrace report + /// + [Tooltip("Allows developer to filter number of game object childrens in Backtrace report.")] + public int GameObjectDepth = -1; + + /// + /// Number of logs collected by Backtrace-Unity + /// + [Obsolete("Please set breadcrumbs integration")] + public uint NumberOfLogs = 10; + + /// + /// Flag that allows to include performance statistics in Backtrace report + /// + [Tooltip("Enable performance statistics")] + public bool PerformanceStatistics = false; + + /// + /// Try to find game native crashes and send them on Game startup + /// + [Tooltip("Try to find game native crashes and send them on Game startup")] + public bool SendUnhandledGameCrashesOnGameStartup = true; + +#if UNITY_ANDROID || UNITY_IOS +#if UNITY_ANDROID + /// + /// Capture native NDK Crashes. + /// + [Tooltip("Capture native NDK Crashes (ANDROID API 21+)")] +#elif UNITY_IOS + /// + /// Capture native iOS Crashes. + /// + [Tooltip("Capture native Crashes")] +#endif + + public bool CaptureNativeCrashes = true; + /// + /// Handle ANR events - Application not responding + /// + [Tooltip("Handle ANR events - Application not responding")] + public bool HandleANR = true; + +#if UNITY_ANDROID + /// + /// Send Low memory warnings to Backtrace + /// + [Tooltip("(Early access) Send Low memory warnings to Backtrace")] +#elif UNITY_IOS + /// + /// Send Out of memory exceptions to Backtrace. + /// + [Tooltip("(Early access) Send Out of memory exceptions to Backtrace")] +#endif + public bool OomReports = false; + +#if UNITY_2019_2_OR_NEWER + /// + /// Symbols upload token + /// + [Tooltip("Symbols upload token required to upload symbols to Backtrace")] + public string SymbolsUploadToken = string.Empty; +#endif +#endif + + /// + /// Backtrace client deduplication strategy. + /// + [Tooltip("Client-side deduplication allows the backtrace-unity library to group multiple error reports into a single one based on various factors. Factors include:\n\n" + + "* Disable - Client-side deduplication rules are disabled.\n" + + "* Everything - Use all the options as a factor in client-side deduplication.\n" + + "* Faulting callstack - Use the faulting callstack as a factor in client-side deduplication.\n" + + "* Exception type - Use the exception type as a factor in client-side deduplication.\n" + + "* Exception message - Use the exception message as a factor in client-side deduplication.")] + + public DeduplicationStrategy DeduplicationStrategy = DeduplicationStrategy.None; + + /// + /// Enable breadcrumbs support + /// + [Tooltip("Enable breadcurmbs integration that will include game breadcrumbs in each report (native + managed).")] + public bool EnableBreadcrumbsSupport = false; + + /// + /// Backtrace breadcrumbs log level controls what type of information will be available in the breadcrumbs file + /// + [Tooltip("Breadcrumbs support breadcrumbs level- Backtrace breadcrumbs log level controls what type of information will be available in the breadcrumb file")] + public BacktraceBreadcrumbsLevel BacktraceBreadcrumbsLevel; + + /// + /// Backtrace Unity Engine log Level controls what log types will be included in the final breadcrumbs file + /// + [Tooltip("Braeadcrumbs log level")] + public UnityEngineLogLevel LogLevel; + + /// + /// Use normalized exception message instead environment stack trace, when exception doesn't have stack trace + /// + [Tooltip("If exception does not have a stack trace, use a normalized exception message to generate fingerprint.")] + public bool UseNormalizedExceptionMessage = false; + + /// + /// Determine minidump type support - minidump generation is supported on Windows. + /// + [Tooltip("Type of minidump that will be attached to Backtrace report in the report generated on Windows machine.")] + public MiniDumpType MinidumpType = MiniDumpType.None; + + /// + /// Generate game screen shot when exception happen + /// + [Tooltip("Generate and attach screenshot of frame as exception occurs")] + public bool GenerateScreenshotOnException = false; + + /// + /// List of path to attachments that Backtrace client will include in the native and managed reports. + /// + [Tooltip("List of path to attachments that Backtrace client will include in the native and managed reports.")] + public string[] AttachmentPaths; + + /// + /// Directory path where reports and minidumps are stored + /// + [Tooltip("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.")] + public string DatabasePath; + + /// + /// Enable event aggregation support + /// + [Header("Backtrace event aggregation")] + [Tooltip("Enable event aggregation support")] + public bool EnableEventAggregationSupport = false; + + /// + /// Event aggregation submission url + /// + [Tooltip("Event aggregation submission url")] + public string EventAggregationSubmissionUrl; + + /// + /// Time interval in ms + /// + [Tooltip("Event aggregation submission url")] + public long TimeIntervalInMs = 0; + + /// + /// Determine if database is enable + /// + [Header("Backtrace database configuration")] + [Tooltip("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.")] + public bool Enabled; + + /// + /// Add Unity log file to Backtrace report + /// + [Tooltip("Add Unity player log file to Backtrace report")] + public bool AddUnityLogToReport = false; + + /// + /// Resend report when http client throw exception + /// + [Tooltip("When toggled on, the database will send automatically reports to Backtrace server based on the Retry Settings below. When toggled off, the developer will need to use the Flush method to attempt to send and clear. Recommend that this is toggled on.")] + public bool AutoSendMode = true; + + /// + /// Determine if BacktraceDatabase should try to create database directory on application start + /// + [Tooltip("If toggled, the library will create the offline database directory if the provided path doesn't exists.")] + public bool CreateDatabase = false; + + /// + /// Maximum number of stored reports in Database. If value is equal to zero, then limit not exists + /// + [Tooltip("This is one of two limits you can impose for controlling the growth of the offline store. This setting is the maximum number of stored reports in database. If value is equal to zero, then limit not exists, When the limit is reached, the database will remove the oldest entries.")] + public int MaxRecordCount = 8; + + /// + /// Database size in MB + /// + [Tooltip("This is the second limit you can impose for controlling the growth of the offline store. This setting is the maximum database size in MB. If value is equal to zero, then size is unlimited, When the limit is reached, the database will remove the oldest entries.")] + public long MaxDatabaseSize; + /// + /// How much seconds library should wait before next retry. + /// + [Tooltip("If the database is unable to send its record, this setting specifies how many seconds the library should wait between retries.")] + public int RetryInterval = 60; + + /// + /// Maximum number of retries + [Tooltip("If the database is unable to send its record, this setting specifies the maximum number of retries before the system gives up.")] + public int RetryLimit = 3; + + /// + /// Retry order + /// + [Tooltip("This specifies in which order records are sent to the Backtrace server.")] + public RetryOrder RetryOrder; + + /// + /// Get full paths to attachments added by client + /// + /// List of absolute path to attachments + public List GetAttachmentPaths() + { + var result = new List(); + if (AttachmentPaths == null || AttachmentPaths.Length == 0) + { + return result; + } + + foreach (var path in AttachmentPaths) + { + if (!string.IsNullOrEmpty(path)) + { + result.Add(ClientPathHelper.GetFullPath(path)); + } + } + return result; + } + + public string GetFullDatabasePath() + { + return ClientPathHelper.GetFullPath(DatabasePath); + } + public string CrashpadDatabasePath + { + get + { + if (!Enabled) + { + return string.Empty; + } + return Path.Combine(GetFullDatabasePath(), "crashpad"); + } + } + + public string GetValidServerUrl() + { + return UpdateServerUrl(ServerUrl); + } + + public static string UpdateServerUrl(string value) + { + //in case if user pass invalid string, copy value contain uri without method modifications + var copy = value; + + if (string.IsNullOrEmpty(value)) + { + return value; + } + + if (!value.StartsWith("http")) + { + value = string.Format("https://{0}", value); + } + string uriScheme = value.StartsWith("https://") + ? Uri.UriSchemeHttps + : Uri.UriSchemeHttp; + + if (!Uri.IsWellFormedUriString(value, UriKind.Absolute)) + { + return copy; + } + return new UriBuilder(value) { Scheme = uriScheme }.Uri.ToString(); + } + + public static bool ValidateServerUrl(string value) + { + return Uri.IsWellFormedUriString(UpdateServerUrl(value), UriKind.Absolute); + } + + public bool IsValid() + { + return ValidateServerUrl(ServerUrl); + } + + + public BacktraceCredentials ToCredentials() + { + return new BacktraceCredentials(ServerUrl); + } + } } \ No newline at end of file diff --git a/Runtime/Model/BacktraceData.cs b/Runtime/Model/BacktraceData.cs index a003aa1e..12cff43b 100644 --- a/Runtime/Model/BacktraceData.cs +++ b/Runtime/Model/BacktraceData.cs @@ -82,11 +82,6 @@ internal string UuidString /// public string[] Classifier; - /// - /// Source code information. - /// - public BacktraceSourceCode SourceCode; - /// /// Get a path to report attachments /// @@ -152,10 +147,6 @@ public string ToJson() jObject.Add("attributes", Attributes.ToJson()); jObject.Add("annotations", Annotation.ToJson()); jObject.Add("threads", ThreadData.ToJson()); - if (SourceCode != null) - { - jObject.Add("sourceCode", SourceCode.ToJson()); - } return jObject.ToJson(); } @@ -170,7 +161,6 @@ private void SetThreadInformations() ThreadData = new ThreadData(Report.DiagnosticStack, faultingThread); ThreadInformations = ThreadData.ThreadInformations; MainThread = ThreadData.MainThread; - SourceCode = Report.SourceCode; } /// diff --git a/Runtime/Model/BacktraceLogManager.cs b/Runtime/Model/BacktraceLogManager.cs deleted file mode 100644 index 922dce91..00000000 --- a/Runtime/Model/BacktraceLogManager.cs +++ /dev/null @@ -1,110 +0,0 @@ -using System.Collections.Concurrent; -using System.Text; -using UnityEngine; - -namespace Backtrace.Unity.Model -{ - /// - /// Backtrace Unity engine log manager - /// - internal class BacktraceLogManager - { - /// - /// Unity message queue - /// - internal readonly ConcurrentQueue LogQueue; - - /// - /// Lock object - /// - private readonly object lockObject = new object(); - - /// - /// Maximum number of logs that log manager can store. - /// - private readonly uint _limit; - - public BacktraceLogManager(uint numberOfLogs) - { - _limit = numberOfLogs; - LogQueue = new ConcurrentQueue(); - } - - /// - /// Get log queue size - /// - public int Size - { - get - { - return LogQueue.Count; - } - } - - /// - /// Validate if log manager is enabled. Log manager might be disabled - /// if Log count is equal to 0. - /// - public bool Disabled - { - get - { - return _limit == 0; - } - } - - /// - /// Enqueue user message - /// - /// Backtrace reprot - /// Message stored in the log manager - public bool Enqueue(BacktraceReport report) - { - return Enqueue(new BacktraceUnityMessage(report)); - - } - - /// - /// Enqueue new unity message - /// - /// Unity message - /// Unity Stack trace - /// Log type - public bool Enqueue(string message, string stackTrace, LogType type) - { - return Enqueue(new BacktraceUnityMessage(message, stackTrace, type)); - } - - /// - /// Enqueue new unity message - /// - public bool Enqueue(BacktraceUnityMessage unityMessage) - { - if (Disabled) - { - return false; - } - - LogQueue.Enqueue(unityMessage.ToString()); - lock (lockObject) - { - while (LogQueue.Count > _limit && LogQueue.TryDequeue(out string _)) ; - } - return true; - } - - /// - /// Generate source code lines based on unity log messages stored in log manager. - /// - /// Source code - public string ToSourceCode() - { - var stringBuilder = new StringBuilder(); - foreach (var item in LogQueue) - { - stringBuilder.AppendLine(item); - } - return stringBuilder.ToString(); - } - } -} diff --git a/Runtime/Model/BacktraceReport.cs b/Runtime/Model/BacktraceReport.cs index fbb6bc5d..99c715c3 100644 --- a/Runtime/Model/BacktraceReport.cs +++ b/Runtime/Model/BacktraceReport.cs @@ -71,11 +71,6 @@ public class BacktraceReport /// public List DiagnosticStack { get; set; } - /// - /// Source code - /// - public BacktraceSourceCode SourceCode = null; - /// /// Create new instance of Backtrace report to sending a report with custom client message /// @@ -130,26 +125,6 @@ private void SetDefaultAttributes() } } - - /// - /// Assign source code to Backtrace report - text available in the right panel in web debugger. - /// - /// - internal void AssignSourceCodeToReport(string text) - { - if (DiagnosticStack == null || DiagnosticStack.Count == 0) - { - return; - } - - SourceCode = new BacktraceSourceCode() - { - Text = text - }; - // assign log information to first stack frame - DiagnosticStack[0].SourceCode = BacktraceSourceCode.SOURCE_CODE_PROPERTY; - } - /// /// Set report classifier /// diff --git a/Runtime/Model/BacktraceSourceCode.cs b/Runtime/Model/BacktraceSourceCode.cs deleted file mode 100644 index e9faa4be..00000000 --- a/Runtime/Model/BacktraceSourceCode.cs +++ /dev/null @@ -1,47 +0,0 @@ -using Backtrace.Unity.Json; -using System; - -namespace Backtrace.Unity.Model -{ - /// - /// Source code panel - in Unity integration, Backtrace-Unity stores unity Engine - /// logs in the Source Code integration. - /// - public class BacktraceSourceCode - { - internal static string SOURCE_CODE_PROPERTY = "main"; - /// - /// Default source code type - /// - public readonly string Type = "Text"; - /// - /// Default source code title - /// - public readonly string Title = "Log File"; - - - /// - /// Unity engine text - /// - public string Text { get; set; } - - /// - /// Convert Source code integration into JSON object - /// - /// Source code BacktraceJObject - internal BacktraceJObject ToJson() - { - var json = new BacktraceJObject(); - var sourceCode = new BacktraceJObject(new System.Collections.Generic.Dictionary() - { - { "id",SOURCE_CODE_PROPERTY }, - { "type", Type }, - { "title", Title }, - { "text", Text } - }); - sourceCode.Add("highlightLine", false); - json.Add(SOURCE_CODE_PROPERTY, sourceCode); - return json; - } - } -} diff --git a/Tests/Runtime/SourceCode.meta b/Runtime/Model/Breadcrumbs.meta similarity index 77% rename from Tests/Runtime/SourceCode.meta rename to Runtime/Model/Breadcrumbs.meta index 2e6051ac..44ab5746 100644 --- a/Tests/Runtime/SourceCode.meta +++ b/Runtime/Model/Breadcrumbs.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: b25749454c8d00548953374bdf6589ea +guid: 64b1da52fd6afa54484cde9e696a01f7 folderAsset: yes DefaultImporter: externalObjects: {} diff --git a/Runtime/Model/Breadcrumbs/BacktraceBreadcrumbs.cs b/Runtime/Model/Breadcrumbs/BacktraceBreadcrumbs.cs new file mode 100644 index 00000000..f21582a0 --- /dev/null +++ b/Runtime/Model/Breadcrumbs/BacktraceBreadcrumbs.cs @@ -0,0 +1,186 @@ +using System; +using System.Collections.Generic; +using UnityEngine; + +namespace Backtrace.Unity.Model.Breadcrumbs +{ + internal sealed class BacktraceBreadcrumbs : IBacktraceBreadcrumbs + { + /// + /// Breadcrumbs log level + /// + public BacktraceBreadcrumbsLevel BreadcrumbsLevel { get; internal set; } + + /// + /// Unity engine log level + /// + public UnityEngineLogLevel UnityLogLevel { get; set; } + + /// + /// Log manager + /// + internal readonly IBacktraceLogManager LogManager; + + internal readonly BacktraceBreadcrumbsEventHandler _eventHandler; + + /// + /// Determine if breadcrumbs are enabled + /// + private bool _enabled = false; + public BacktraceBreadcrumbs(IBacktraceLogManager logManager) + { + LogManager = logManager; + _eventHandler = new BacktraceBreadcrumbsEventHandler(this); + } + public void UnregisterEvents() + { + _eventHandler.Unregister(); + } + + public bool ClearBreadcrumbs() + { + return LogManager.Clear(); + } + + + public bool AddBreadcrumbs(string message, LogType type) + { + return AddBreadcrumbs(message, type, null); + } + + public bool AddBreadcrumbs(string message) + { + return AddBreadcrumbs(message, LogType.Log); + } + + public bool Debug(string message) + { + return AddBreadcrumbs(message, LogType.Assert); + } + + public bool EnableBreadcrumbs(BacktraceBreadcrumbsLevel level, UnityEngineLogLevel unityLogLevel) + { + if (_enabled) + { + return false; + } + BreadcrumbsLevel = level; + UnityLogLevel = unityLogLevel; + + var breadcrumbStorageEnabled = LogManager.Enable(); + if (!breadcrumbStorageEnabled) + { + return false; + } + _eventHandler.Register(level); + return true; + } + + public bool Exception(Exception exception) + { + return AddBreadcrumbs(exception.Message, LogType.Error, null); + } + + public bool Exception(string message) + { + return AddBreadcrumbs(message, LogType.Error, null); + } + + public bool FromBacktrace(BacktraceReport report) + { + var type = report.ExceptionTypeReport ? LogType.Exception : LogType.Log; + if (!ShouldLog(type)) + { + return false; + } + return AddBreadcrumbs( + report.Message, + BreadcrumbLevel.System, + type, + null); + } + + public bool FromMonoBehavior(string message, LogType type, IDictionary attributes) + { + return AddBreadcrumbs(message, BreadcrumbLevel.System, type, attributes); + } + + public string GetBreadcrumbLogPath() + { + return LogManager.BreadcrumbsFilePath; + } + + public bool Info(string message) + { + return AddBreadcrumbs(message, LogType.Log, null); + } + + public bool Warning(string message) + { + return AddBreadcrumbs(message, LogType.Warning, null); + } + + public bool Debug(string message, IDictionary attributes) + { + return AddBreadcrumbs(message, LogType.Assert, attributes); + } + + public bool Info(string message, IDictionary attributes) + { + return AddBreadcrumbs(message, LogType.Assert, attributes); + } + + public bool Warning(string message, IDictionary attributes) + { + return AddBreadcrumbs(message, LogType.Warning, attributes); + } + + public bool Exception(Exception exception, IDictionary attributes) + { + return AddBreadcrumbs(exception.Message, LogType.Exception, attributes); + } + + public bool Exception(string message, IDictionary attributes) + { + return AddBreadcrumbs(message, LogType.Exception, attributes); + } + public bool AddBreadcrumbs(string message, LogType type, IDictionary attributes) + { + if (!ShouldLog(type)) + { + return false; + } + return AddBreadcrumbs(message, BreadcrumbLevel.Manual, type, attributes); + } + internal bool AddBreadcrumbs(string message, BreadcrumbLevel level, LogType type, IDictionary attributes = null) + { + if (!BreadcrumbsLevel.HasFlag((BacktraceBreadcrumbsLevel)level)) + { + return false; + } + return LogManager.Add(message, level, type, attributes); + } + + internal bool ShouldLog(LogType type) + { + if (!BreadcrumbsLevel.HasFlag(BacktraceBreadcrumbsLevel.Manual)) + { + return false; + } + switch (type) + { + case LogType.Log: + return UnityLogLevel.HasFlag(UnityEngineLogLevel.Log); + case LogType.Warning: + return UnityLogLevel.HasFlag(UnityEngineLogLevel.Warning); + case LogType.Exception: + return UnityLogLevel.HasFlag(UnityEngineLogLevel.Exception); + case LogType.Error: + return UnityLogLevel.HasFlag(UnityEngineLogLevel.Error); + case LogType.Assert: + return UnityLogLevel.HasFlag(UnityEngineLogLevel.Assert); + } + return false; + } + } +} diff --git a/Tests/Runtime/SourceCode/SourceCodeFlowWithLogManagerTests.cs.meta b/Runtime/Model/Breadcrumbs/BacktraceBreadcrumbs.cs.meta similarity index 83% rename from Tests/Runtime/SourceCode/SourceCodeFlowWithLogManagerTests.cs.meta rename to Runtime/Model/Breadcrumbs/BacktraceBreadcrumbs.cs.meta index 53190057..1d9ae03b 100644 --- a/Tests/Runtime/SourceCode/SourceCodeFlowWithLogManagerTests.cs.meta +++ b/Runtime/Model/Breadcrumbs/BacktraceBreadcrumbs.cs.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: 2ada0b6de56adce498865f989562c093 +guid: 9836cde8f6a207742bac629d769af019 MonoImporter: externalObjects: {} serializedVersion: 2 diff --git a/Runtime/Model/Breadcrumbs/BacktraceBreadcrumbsEventHandler.cs b/Runtime/Model/Breadcrumbs/BacktraceBreadcrumbsEventHandler.cs new file mode 100644 index 00000000..2987ad75 --- /dev/null +++ b/Runtime/Model/Breadcrumbs/BacktraceBreadcrumbsEventHandler.cs @@ -0,0 +1,117 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using UnityEngine; +using UnityEngine.SceneManagement; + +namespace Backtrace.Unity.Model.Breadcrumbs +{ + internal sealed class BacktraceBreadcrumbsEventHandler + { + private readonly BacktraceBreadcrumbs _breadcrumbs; + private BacktraceBreadcrumbsLevel _registeredLevel; + private Thread _thread; + public BacktraceBreadcrumbsEventHandler(BacktraceBreadcrumbs breadcrumbs) + { + _thread = Thread.CurrentThread; + _breadcrumbs = breadcrumbs; + } + /// + /// Register unity events that will generate logs in the breadcrumbs file + /// + /// Breadcrumbs level + public void Register(BacktraceBreadcrumbsLevel level) + { + _registeredLevel = level; + if (!level.HasFlag(BacktraceBreadcrumbsLevel.System)) + { + return; + } + SceneManager.activeSceneChanged += HandleSceneChanged; + SceneManager.sceneLoaded += SceneManager_sceneLoaded; + SceneManager.sceneUnloaded += SceneManager_sceneUnloaded; + Application.lowMemory += HandleLowMemory; + Application.quitting += HandleApplicationQuitting; + Application.focusChanged += Application_focusChanged; + Application.logMessageReceived += HandleMessage; + Application.logMessageReceivedThreaded += HandleBackgroundMessage; + } + + /// + /// Unregister Unity breadcrumbs events + /// + public void Unregister() + { + if (!_registeredLevel.HasFlag(BacktraceBreadcrumbsLevel.System)) + { + return; + } + SceneManager.activeSceneChanged -= HandleSceneChanged; + SceneManager.sceneLoaded -= SceneManager_sceneLoaded; + SceneManager.sceneUnloaded -= SceneManager_sceneUnloaded; + Application.lowMemory -= HandleLowMemory; + Application.quitting -= HandleApplicationQuitting; + Application.logMessageReceived -= HandleMessage; + Application.logMessageReceivedThreaded -= HandleBackgroundMessage; + Application.focusChanged -= Application_focusChanged; + } + + private void SceneManager_sceneUnloaded(Scene scene) + { + var message = string.Format("SceneManager:scene {0} unloaded", scene.name); + Log(message, LogType.Assert); + } + + private void SceneManager_sceneLoaded(Scene scene, LoadSceneMode loadSceneMode) + { + var message = string.Format("SceneManager:scene {0} loaded", scene.name); + Log(message, LogType.Assert, new Dictionary() { { "LoadSceneMode", loadSceneMode.ToString() } }); + } + + private void HandleSceneChanged(Scene sceneFrom, Scene sceneTo) + { + var message = string.Format("SceneManager:scene changed from {0} to {1}", sceneFrom.name, sceneTo.name); + Log(message, LogType.Assert); + } + + private void HandleLowMemory() + { + Log("Application:low memory", LogType.Warning); + } + + private void HandleApplicationQuitting() + { + Log("Application:quitting", LogType.Log); + } + + private void HandleBackgroundMessage(string condition, string stackTrace, LogType type) + { + // validate if a message is from main thread + // and skip messages from main thread + if (Thread.CurrentThread == _thread) + { + return; + } + HandleMessage(condition, stackTrace, type); + } + + private void HandleMessage(string condition, string stackTrace, LogType type) + { + Log(condition, type, new Dictionary { { "stackTrace", stackTrace } }); + } + + private void Application_focusChanged(bool hasFocus) + { + Log("Application:focus changed.", LogType.Assert, new Dictionary { { "hasFocus", hasFocus.ToString() } }); + } + + private void Log(string message, LogType level, IDictionary attributes = null) + { + if (!_breadcrumbs.ShouldLog(level)) + { + return; + } + _breadcrumbs.AddBreadcrumbs(message, BreadcrumbLevel.System, level, attributes); + } + } +} diff --git a/Runtime/Model/BacktraceLogManager.cs.meta b/Runtime/Model/Breadcrumbs/BacktraceBreadcrumbsEventHandler.cs.meta similarity index 83% rename from Runtime/Model/BacktraceLogManager.cs.meta rename to Runtime/Model/Breadcrumbs/BacktraceBreadcrumbsEventHandler.cs.meta index f312328e..184a192b 100644 --- a/Runtime/Model/BacktraceLogManager.cs.meta +++ b/Runtime/Model/Breadcrumbs/BacktraceBreadcrumbsEventHandler.cs.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: c313244615b5d3a41836e82e38234b2d +guid: 0d06be320eb80114d99b02e1ba03d5f7 MonoImporter: externalObjects: {} serializedVersion: 2 diff --git a/Runtime/Model/Breadcrumbs/BacktraceBreadcrumbsLevel.cs b/Runtime/Model/Breadcrumbs/BacktraceBreadcrumbsLevel.cs new file mode 100644 index 00000000..5d8d87c2 --- /dev/null +++ b/Runtime/Model/Breadcrumbs/BacktraceBreadcrumbsLevel.cs @@ -0,0 +1,20 @@ +using System; + +namespace Backtrace.Unity.Model.Breadcrumbs +{ + /// + /// Breadcrumbs level + /// + [Flags] + public enum BacktraceBreadcrumbsLevel + { + Manual = BreadcrumbLevel.Manual, + Log = BreadcrumbLevel.Log, + Navigation = BreadcrumbLevel.Navigation, + Http = BreadcrumbLevel.Http, + System = BreadcrumbLevel.System, + User = BreadcrumbLevel.User, + Configuration = BreadcrumbLevel.Configuration, + } + +} diff --git a/Tests/Runtime/SourceCode/LogManagerTests.cs.meta b/Runtime/Model/Breadcrumbs/BacktraceBreadcrumbsLevel.cs.meta similarity index 83% rename from Tests/Runtime/SourceCode/LogManagerTests.cs.meta rename to Runtime/Model/Breadcrumbs/BacktraceBreadcrumbsLevel.cs.meta index 84223314..21c653f6 100644 --- a/Tests/Runtime/SourceCode/LogManagerTests.cs.meta +++ b/Runtime/Model/Breadcrumbs/BacktraceBreadcrumbsLevel.cs.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: faed03f8b5417ab4eaa241a2387608de +guid: 508c4d81da57a174380f6de284a8f4c0 MonoImporter: externalObjects: {} serializedVersion: 2 diff --git a/Runtime/Model/Breadcrumbs/BacktraceStorageLogManager.cs b/Runtime/Model/Breadcrumbs/BacktraceStorageLogManager.cs new file mode 100644 index 00000000..5100176b --- /dev/null +++ b/Runtime/Model/Breadcrumbs/BacktraceStorageLogManager.cs @@ -0,0 +1,284 @@ +using Backtrace.Unity.Common; +using Backtrace.Unity.Json; +using System; +using System.Collections.Generic; +using System.IO; +using UnityEngine; + +namespace Backtrace.Unity.Model.Breadcrumbs +{ + internal sealed class BacktraceStorageLogManager : IBacktraceLogManager + { + /// + /// Path to the breadcrumbs file + /// + public string BreadcrumbsFilePath { get; private set; } + + /// + /// Minimum size of the breadcrumbs file (10kB) + /// + public const int MinimumBreadcrumbsFileSize = 10 * 1000; + + /// + /// Breadcrumbs file size. + /// + public long BreadcrumbsSize + { + get + { + return _breadcrumbsSize; + } + set + { + if (value < MinimumBreadcrumbsFileSize) + { + throw new ArgumentException("Breadcrumbs size must be greater or equal to 10kB"); + } + _breadcrumbsSize = value; + } + } + + /// + /// Default breacrumbs size. By default breadcrumbs file size is limitted to 64kB. + /// + private long _breadcrumbsSize = 64000; + + /// + /// Default log file name + /// + internal const string BreadcrumbLogFileName = "bt-breadcrumbs-0"; + + /// + /// default breadcrumb row ending + /// + /// Default breadcrumb document ending + /// + private byte[] _endOfDocument = System.Text.Encoding.UTF8.GetBytes("\n]"); + + /// + /// Default breadcrumb end of the document + /// + private byte[] _startOfDocument = System.Text.Encoding.UTF8.GetBytes("[\n"); + + /// + /// Breadcrumb id + /// + private long _breadcrumbId = 0; + + /// + /// Lock object + /// + private object _lockObject = new object(); + + /// + /// Current breadcurmbs fle size + /// + private long currentSize = 0; + + /// + /// Queue that represents number of bytes in each log stored in the breadcrumb file + /// + private readonly Queue _logSize = new Queue(); + + public BacktraceStorageLogManager(string storagePath) + { + if (string.IsNullOrEmpty(storagePath)) + { + throw new ArgumentException("Breadcrumbs storage path is null or empty"); + } + BreadcrumbsFilePath = Path.Combine(storagePath, BreadcrumbLogFileName); + } + + /// + /// Enables breadcrumbs integration + /// + /// true if breadcrumbs file was created. Otherwise false. + public bool Enable() + { + try + { + if (File.Exists(BreadcrumbsFilePath)) + { + File.Delete(BreadcrumbsFilePath); + } + + using (var _breadcrumbStream = new FileStream(BreadcrumbsFilePath, FileMode.CreateNew, FileAccess.Write)) + { + _breadcrumbStream.Write(_startOfDocument, 0, _startOfDocument.Length); + _breadcrumbStream.Write(_endOfDocument, 0, _endOfDocument.Length); + } + currentSize = _startOfDocument.Length + _endOfDocument.Length; + } + catch (Exception e) + { + System.Diagnostics.Debug.WriteLine(string.Format("Cannot initialize breadcrumbs file. Reason: {0}", e.Message)); + return false; + } + return true; + } + + /// + /// Adds breadcrumb entry to the breadcrumbs file. + /// + /// Breadcrumb message + /// Breadcrumb level + /// Breadcrumb type + /// Breadcrumb attributs + /// True if breadcrumb was stored in the breadcrumbs file. Otherwise false. + public bool Add(string message, BreadcrumbLevel level, LogType type, IDictionary attributes) + { + byte[] bytes; + lock (_lockObject) + { + long id = _breadcrumbId++; + var jsonObject = CreateBreadcrumbJson(id, message, level, type, attributes); + bytes = System.Text.Encoding.UTF8.GetBytes(jsonObject.ToJson()); + + if (currentSize + bytes.Length > BreadcrumbsSize) + { + try + { + ClearOldLogs(); + } + catch (Exception) + { + return false; + } + } + } + + try + { + return AppendBreadcrumb(bytes); + } + catch (Exception) + { + return false; + } + } + + /// + /// Convert diagnostic data to JSON format + /// + /// Breadcrumbs id + /// breadcrumbs message + /// Breadcrumb level + /// Breadcrumb type + /// Breadcrumb attributes + /// JSON object + private BacktraceJObject CreateBreadcrumbJson( + long id, + string message, + BreadcrumbLevel level, + LogType type, + IDictionary attributes) + { + var jsonObject = new BacktraceJObject(); + jsonObject.Add("timestamp", DateTimeHelper.Timestamp()); + jsonObject.Add("id", id); + jsonObject.Add("level", Enum.GetName(typeof(BreadcrumbLevel), level)); + jsonObject.Add("type", Enum.GetName(typeof(LogType), type)); + jsonObject.Add("message", message); + jsonObject.Add("attributes", new BacktraceJObject(attributes)); + return jsonObject; + } + /// + /// Append breadcrumb JSON to the end of the breadcrumbs file. + /// + /// Bytes that represents single JSON object with breadcrumb informatiom + private bool AppendBreadcrumb(byte[] bytes) + { + // size of the breadcrumb - it's negative at the beginning because we're removing 2 bytes on start + long appendingSize = _endOfDocument.Length * -1; + using (var breadcrumbStream = new FileStream(BreadcrumbsFilePath, FileMode.Open, FileAccess.Write)) + { + //back to position before end of the document \n} + breadcrumbStream.Position = breadcrumbStream.Length - _endOfDocument.Length; + + // append ,\n when we're appending new row to existing list of rows. If this is first row + // ignore it + if (_breadcrumbId != 1) + { + breadcrumbStream.Write(_newRow, 0, _newRow.Length); + appendingSize += _newRow.Length; + } + // append breadcrumbs json + breadcrumbStream.Write(bytes, 0, bytes.Length); + // and close JSON document + breadcrumbStream.Write(_endOfDocument, 0, _endOfDocument.Length); + appendingSize += (bytes.Length + _endOfDocument.Length); + } + currentSize += appendingSize; + _logSize.Enqueue(bytes.Length); + return true; + } + + /// + /// Remove last n logs from the breadcrumbs file. When breacrumbs file hit + /// the file size limit, this method will clear up the oldest logs to decrease + /// file size. + /// + private void ClearOldLogs() + { + var startPosition = GetNextStartPosition(); + using (FileStream breadcrumbsStream = new FileStream(BreadcrumbsFilePath, FileMode.Open, FileAccess.ReadWrite)) + { + using (MemoryStream ms = new MemoryStream()) + { + var size = breadcrumbsStream.Length - startPosition; + breadcrumbsStream.Seek(size * -1, SeekOrigin.End); + + breadcrumbsStream.CopyTo(ms); + breadcrumbsStream.SetLength(size + _startOfDocument.Length); + + ms.Position = 0; + breadcrumbsStream.Position = 0; + + breadcrumbsStream.Write(_startOfDocument, 0, _startOfDocument.Length); + ms.CopyTo(breadcrumbsStream); + } + } + // decrease a size of the breadcrumb file after removing n breadcrumbs + currentSize -= startPosition; + currentSize += _startOfDocument.Length; + } + + /// + /// Calculate start position of the file that will be used + /// to recreate breadcrumbs file. Position represents place + /// where starts breadcrumbs that we should keep in the recreated file. + /// + /// Breadcrumb start index + private long GetNextStartPosition() + { + double expectedFreedBytes = BreadcrumbsSize - (BreadcrumbsSize * 0.7); + long numberOfFreeBytes = _startOfDocument.Length; + int nextLineBytes = _newRow.Length; + while (numberOfFreeBytes < expectedFreedBytes) + { + numberOfFreeBytes += (_logSize.Dequeue() + nextLineBytes); + } + + return numberOfFreeBytes; + } + + /// + /// Remove breadcrumbs file + /// + public bool Clear() + { + try + { + File.Delete(BreadcrumbsFilePath); + return true; + } + catch (Exception) + { + return false; + } + } + } +} diff --git a/Runtime/Model/BacktraceSourceCode.cs.meta b/Runtime/Model/Breadcrumbs/BacktraceStorageLogManager.cs.meta similarity index 83% rename from Runtime/Model/BacktraceSourceCode.cs.meta rename to Runtime/Model/Breadcrumbs/BacktraceStorageLogManager.cs.meta index 8ec991c6..3bb5864c 100644 --- a/Runtime/Model/BacktraceSourceCode.cs.meta +++ b/Runtime/Model/Breadcrumbs/BacktraceStorageLogManager.cs.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: e69d783703bd3794980ada5953db82c5 +guid: e1dc90f936be9934fa3947671f48d17b MonoImporter: externalObjects: {} serializedVersion: 2 diff --git a/Runtime/Model/Breadcrumbs/BreadcrumbLevel.cs b/Runtime/Model/Breadcrumbs/BreadcrumbLevel.cs new file mode 100644 index 00000000..62ae29d9 --- /dev/null +++ b/Runtime/Model/Breadcrumbs/BreadcrumbLevel.cs @@ -0,0 +1,13 @@ +namespace Backtrace.Unity.Model.Breadcrumbs +{ + internal enum BreadcrumbLevel + { + Manual = 1, + Log = 2, + Navigation = 4, + Http = 8, + System = 16, + User = 32, + Configuration = 64, + } +} diff --git a/Runtime/Model/Breadcrumbs/BreadcrumbLevel.cs.meta b/Runtime/Model/Breadcrumbs/BreadcrumbLevel.cs.meta new file mode 100644 index 00000000..f34ccaa5 --- /dev/null +++ b/Runtime/Model/Breadcrumbs/BreadcrumbLevel.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 892fb2ec577600d43b2a44ef1a8d47a9 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Model/Breadcrumbs/IBacktraceBreadcrumbs.cs b/Runtime/Model/Breadcrumbs/IBacktraceBreadcrumbs.cs new file mode 100644 index 00000000..a8f9acc0 --- /dev/null +++ b/Runtime/Model/Breadcrumbs/IBacktraceBreadcrumbs.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using UnityEngine; + +namespace Backtrace.Unity.Model.Breadcrumbs +{ + public interface IBacktraceBreadcrumbs + { + BacktraceBreadcrumbsLevel BreadcrumbsLevel { get; } + bool EnableBreadcrumbs(BacktraceBreadcrumbsLevel level, UnityEngineLogLevel unityLogLevel); + bool ClearBreadcrumbs(); + bool AddBreadcrumbs(string message, LogType type, IDictionary attributes); + bool AddBreadcrumbs(string message, LogType type); + bool AddBreadcrumbs(string message); + bool Debug(string message); + bool Debug(string message, IDictionary attributes); + bool Info(string message); + bool Info(string message, IDictionary attributes); + bool Warning(string message); + bool Warning(string message, IDictionary attributes); + bool Exception(Exception exception); + bool Exception(Exception exception, IDictionary attributes); + bool Exception(string message); + bool Exception(string message, IDictionary attributes); + bool FromBacktrace(BacktraceReport report); + bool FromMonoBehavior(string message, LogType type, IDictionary attributes); + string GetBreadcrumbLogPath(); + void UnregisterEvents(); + + } +} diff --git a/Runtime/Model/Breadcrumbs/IBacktraceBreadcrumbs.cs.meta b/Runtime/Model/Breadcrumbs/IBacktraceBreadcrumbs.cs.meta new file mode 100644 index 00000000..2f6d5377 --- /dev/null +++ b/Runtime/Model/Breadcrumbs/IBacktraceBreadcrumbs.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: f9d956a61e603a444939e169e8607847 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Model/Breadcrumbs/IBacktraceLogManager.cs b/Runtime/Model/Breadcrumbs/IBacktraceLogManager.cs new file mode 100644 index 00000000..da810d87 --- /dev/null +++ b/Runtime/Model/Breadcrumbs/IBacktraceLogManager.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; +using UnityEngine; + +namespace Backtrace.Unity.Model.Breadcrumbs +{ + internal interface IBacktraceLogManager + { + string BreadcrumbsFilePath { get; } + bool Add(string message, BreadcrumbLevel level, LogType type, IDictionary attributes); + bool Clear(); + bool Enable(); + } +} diff --git a/Runtime/Model/Breadcrumbs/IBacktraceLogManager.cs.meta b/Runtime/Model/Breadcrumbs/IBacktraceLogManager.cs.meta new file mode 100644 index 00000000..6ce7b056 --- /dev/null +++ b/Runtime/Model/Breadcrumbs/IBacktraceLogManager.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 195257e099f15ed4bbf1a4aa9a14141c +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Model/Breadcrumbs/UnityEngineLogLevel.cs b/Runtime/Model/Breadcrumbs/UnityEngineLogLevel.cs new file mode 100644 index 00000000..df00cd4e --- /dev/null +++ b/Runtime/Model/Breadcrumbs/UnityEngineLogLevel.cs @@ -0,0 +1,17 @@ +using System; + +namespace Backtrace.Unity.Model.Breadcrumbs +{ + /// + /// Backtrace Breadcrumbs unity engine log received + /// + [Flags] + public enum UnityEngineLogLevel + { + Assert = 1, + Warning = 2, + Log = 4, + Exception = 8, + Error = 16 + } +} diff --git a/Runtime/Model/Breadcrumbs/UnityEngineLogLevel.cs.meta b/Runtime/Model/Breadcrumbs/UnityEngineLogLevel.cs.meta new file mode 100644 index 00000000..28657d46 --- /dev/null +++ b/Runtime/Model/Breadcrumbs/UnityEngineLogLevel.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 7659125c5bc43d144ab6c10edee7f167 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Services/BacktraceDatabaseFileContext.cs b/Runtime/Services/BacktraceDatabaseFileContext.cs index c7a51ff0..1958e22c 100644 --- a/Runtime/Services/BacktraceDatabaseFileContext.cs +++ b/Runtime/Services/BacktraceDatabaseFileContext.cs @@ -1,4 +1,5 @@ using Backtrace.Unity.Interfaces; +using Backtrace.Unity.Model.Breadcrumbs; using Backtrace.Unity.Model.Database; using System; using System.Collections.Generic; @@ -79,6 +80,11 @@ public void RemoveOrphaned(IEnumerable existingRecords) //database only store data in json and files in dmp extension try { + // prevent from removing breadcrumbs file + if (file.Name.StartsWith(BacktraceStorageLogManager.BreadcrumbLogFileName)) + { + continue; + } if (!_possibleDatabaseExtension.Any(n => n == file.Extension)) { file.Delete(); @@ -156,5 +162,6 @@ public void Clear() file.Delete(); } } + } } diff --git a/Tests/Runtime/ClientSendTests.cs b/Tests/Runtime/ClientSendTests.cs index 31910167..3fda45dc 100644 --- a/Tests/Runtime/ClientSendTests.cs +++ b/Tests/Runtime/ClientSendTests.cs @@ -123,32 +123,6 @@ public IEnumerator PiiTests_ShouldChangeApplicationDataPath_ApplicationDataPathD yield return null; } - [UnityTest] - public IEnumerator PiiTests_ShouldChangeSourceCodeIntegration_SourceCodeTextDoesntHaveUserNameAnymore() - { - var trigger = false; - var exception = new Exception("custom exception message"); - var sourceCodeTestString = "source-code-test-string"; - client.BeforeSend = (BacktraceData data) => - { - Assert.IsNotNull(data.SourceCode); - Assert.IsNotEmpty(data.SourceCode.Text); - data.SourceCode.Text = sourceCodeTestString; - return data; - }; - client.RequestHandler = (string url, BacktraceData data) => - { - trigger = true; - Assert.AreEqual(sourceCodeTestString, data.SourceCode.Text); - return new BacktraceResult(); - }; - client.Send(exception); - - yield return new WaitForEndOfFrame(); - Assert.IsTrue(trigger); - yield return null; - } - [UnityTest] public IEnumerator PiiTests_ShouldModifyEnvironmentVariable_IntegrationShouldUseModifiedEnvironmentVariables() { diff --git a/Tests/Runtime/SourceCode/LogManagerTests.cs b/Tests/Runtime/SourceCode/LogManagerTests.cs deleted file mode 100644 index 35b38404..00000000 --- a/Tests/Runtime/SourceCode/LogManagerTests.cs +++ /dev/null @@ -1,165 +0,0 @@ -using Backtrace.Unity.Model; -using NUnit.Framework; -using System; -using System.Globalization; -using System.Linq; -using System.Threading; -using UnityEngine; - -namespace Backtrace.Unity.Tests.Runtime -{ - public class LogManagerTests - { - - [Test] - public void TestLogManagerInitialization_LimitEqualToZero_ShouldBeDisabled() - { - var logManager = new BacktraceLogManager(0); - Assert.IsTrue(logManager.Disabled); - } - - [Test] - public void TestLogManagerInitialization_LimitNotEqualToZero_ShouldBeEnabled() - { - var logManager = new BacktraceLogManager(1); - Assert.IsFalse(logManager.Disabled); - } - - [Test] - public void TestDisabledManager_AddLogToDisabledManager_ShouldntEnqueueMessage() - { - var logManager = new BacktraceLogManager(0); - logManager.Enqueue("fake message", string.Empty, LogType.Log); - Assert.AreEqual(0, logManager.Size); - } - - [Test] - public void TestMessageQueue_AddLogToEnabledManager_ShouldEnqueueMessage() - { - uint expectedNumberOfMessages = 1; - var logManager = new BacktraceLogManager(expectedNumberOfMessages); - logManager.Enqueue("fake message", string.Empty, LogType.Log); - Assert.AreEqual(expectedNumberOfMessages, logManager.Size); - } - - [Test] - public void TestMessageQueue_AddMultipleLosgToEnabledManager_ShouldEnqueueLimitMessage() - { - uint expectedNumberOfMessages = 1; - var logManager = new BacktraceLogManager(expectedNumberOfMessages); - - logManager.Enqueue("deleted message", string.Empty, LogType.Log); - var enqueuedMessage = "enqueued message"; - logManager.Enqueue(enqueuedMessage, string.Empty, LogType.Log); - - Assert.AreEqual(expectedNumberOfMessages, logManager.Size); - // validate if log ends with enqueueMessage to validate if message is there, - // without checking other message content (date, log type etc..) - Assert.IsTrue(logManager.LogQueue.First().EndsWith(enqueuedMessage)); - } - - - [TestCase(5, "ar-DZ")] - [TestCase(10, "ar-SA")] - [TestCase(25, "ko-KR")] - public void TestLogManagerLimit_AddMessagesThatMatchLimitCriteria_AllMessagesShouldBeInLogManager(int numberOfLogs, string cultureName) - { - var culture = CultureInfo.GetCultureInfo(cultureName); - Thread.CurrentThread.CurrentCulture = culture; - Thread.CurrentThread.CurrentUICulture = culture; - - var message = "fake message"; - var stackTrace = string.Empty; - var type = LogType.Log; - var backtraceUnityLogManager = new BacktraceLogManager((uint)numberOfLogs); - - for (int i = 0; i < numberOfLogs; i++) - { - backtraceUnityLogManager.Enqueue(message, stackTrace, type); - } - Assert.AreEqual(backtraceUnityLogManager.Size, numberOfLogs); - } - - [TestCase(5)] - [TestCase(10)] - [TestCase(25)] - public void TestLogManagerLimit_AddMessageReportsThatMatchLimitCriteria_AllMessagesShouldBeInLogManager(int numberOfLogs) - { - var report = new BacktraceReport("message"); - var backtraceUnityLogManager = new BacktraceLogManager((uint)numberOfLogs); - - for (int i = 0; i < numberOfLogs; i++) - { - backtraceUnityLogManager.Enqueue(report); - } - Assert.AreEqual(backtraceUnityLogManager.Size, numberOfLogs); - } - - [TestCase(5)] - [TestCase(10)] - [TestCase(25)] - public void TestLogManagerLimit_AddExceptionReportsThatMatchLimitCriteria_AllMessagesShouldBeInLogManager(int numberOfLogs) - { - var report = new BacktraceReport(new Exception(string.Empty)); - var backtraceUnityLogManager = new BacktraceLogManager((uint)numberOfLogs); - - for (int i = 0; i < numberOfLogs; i++) - { - backtraceUnityLogManager.Enqueue(report); - } - Assert.AreEqual(backtraceUnityLogManager.Size, numberOfLogs); - } - [Test] - public void TestLogManagerSourceCodeGeneration_ShouldReturnStringSourceCode_CorrectSourceCodeGenerationFromLog() - { - var message = "Message"; - var stackTrace = "stack trace"; - var type = LogType.Log; - var backtraceUnityLogManager = new BacktraceLogManager(1); - backtraceUnityLogManager.Enqueue(message, stackTrace, type); - var sourceCodeText = backtraceUnityLogManager.ToSourceCode(); - Assert.IsNotEmpty(sourceCodeText); - Assert.IsTrue(sourceCodeText.Contains(message)); - Assert.IsFalse(sourceCodeText.Contains(stackTrace)); - } - [Test] - public void TestLogManagerSourceCodeGeneration_ShouldReturnStringSourceCode_CorrectSourceCodeGenerationFromEmptyLog() - { - var type = LogType.Log; - var backtraceUnityLogManager = new BacktraceLogManager(1); - backtraceUnityLogManager.Enqueue(string.Empty, string.Empty, type); - var sourceCodeText = backtraceUnityLogManager.ToSourceCode(); - Assert.IsNotEmpty(sourceCodeText); - } - [Test] - public void TestLogManagerSourceCodeGeneration_ShouldReturnStringSourceCode_CorrectSourceCodeGenerationWithStackTraceForException() - { - var message = "Message"; - var stackTrace = "stack trace"; - var type = LogType.Exception; - var backtraceUnityLogManager = new BacktraceLogManager(1); - backtraceUnityLogManager.Enqueue(message, stackTrace, type); - var sourceCodeText = backtraceUnityLogManager.ToSourceCode(); - Assert.IsNotEmpty(sourceCodeText); - Assert.IsTrue(sourceCodeText.Contains(message)); - Assert.IsTrue(sourceCodeText.Contains(stackTrace)); - } - - [Test] - public void TestLogManagerSourceCodeGeneration_ShouldReturnStringSourceCodeForEmptyValues_CorrectSourceCodeGenerationWithStackTraceForException() - { - var message = string.Empty; - var stackTrace = string.Empty; - var type = LogType.Log; - var backtraceUnityLogManager = new BacktraceLogManager(1); - backtraceUnityLogManager.Enqueue(message, stackTrace, type); - string sourceCodeText = string.Empty; - Assert.DoesNotThrow(() => - { - sourceCodeText = backtraceUnityLogManager.ToSourceCode(); - }); - - Assert.IsNotEmpty(sourceCodeText); - } - } -} diff --git a/Tests/Runtime/SourceCode/SourceCodeFlowWithLogManagerTests.cs b/Tests/Runtime/SourceCode/SourceCodeFlowWithLogManagerTests.cs deleted file mode 100644 index 00971788..00000000 --- a/Tests/Runtime/SourceCode/SourceCodeFlowWithLogManagerTests.cs +++ /dev/null @@ -1,233 +0,0 @@ -using Backtrace.Unity.Model; -using NUnit.Framework; -using System; -using System.Collections; -using System.Linq; -using UnityEngine; -using UnityEngine.TestTools; - -namespace Backtrace.Unity.Tests.Runtime -{ - public class SourceCodeFlowWithLogManagerTests : BacktraceBaseTest - { - private readonly BacktraceApiMock api = new BacktraceApiMock(); - private readonly int _numberOfLogs = 10; - - [SetUp] - public void Setup() - { - BeforeSetup(); - - var configuration = GetValidClientConfiguration(); - configuration.NumberOfLogs = (uint)_numberOfLogs; - BacktraceClient.Configuration = configuration; - AfterSetup(true); - BacktraceClient.BacktraceApi = api; - } - - [UnityTest] - public IEnumerator TestSourceCodeAssignment_EnabledLogManagerAndSendExceptionReport_SourceCodeAvailable() - { - BacktraceData lastData = null; - BacktraceClient.BeforeSend = (BacktraceData data) => - { - lastData = data; - return data; - }; - - BacktraceClient.Send(new Exception("foo")); - yield return new WaitForEndOfFrame(); - - - Assert.IsNotNull(lastData.SourceCode); - - var threadName = lastData.ThreadData.MainThread; - Assert.AreEqual(BacktraceSourceCode.SOURCE_CODE_PROPERTY, lastData.ThreadData.ThreadInformations[threadName].Stack.First().SourceCode); - } - - - [UnityTest] - public IEnumerator TestSourceCodeAssignment_EnabledLogManagerAndSendMessageReport_SourceCodeAvailable() - { - BacktraceData lastData = null; - BacktraceClient.BeforeSend = (BacktraceData data) => - { - lastData = data; - return data; - }; - - BacktraceClient.Send("foo"); - yield return new WaitForEndOfFrame(); - - Assert.IsNotNull(lastData.SourceCode); - - var threadName = lastData.ThreadData.MainThread; - Assert.AreEqual(BacktraceSourceCode.SOURCE_CODE_PROPERTY, lastData.ThreadData.ThreadInformations[threadName].Stack.First().SourceCode); - } - - - [UnityTest] - public IEnumerator TestSourceCodeAssignment_EnabledLogManagerAndSendUnhandledException_SourceCodeAvailable() - { - BacktraceData lastData = null; - BacktraceClient.BeforeSend = (BacktraceData data) => - { - lastData = data; - return data; - }; - - BacktraceClient.HandleUnityMessage("foo", string.Empty, LogType.Exception); - yield return new WaitForEndOfFrame(); - - Assert.IsNotNull(lastData.SourceCode); - - var threadName = lastData.ThreadData.MainThread; - Assert.AreEqual(BacktraceSourceCode.SOURCE_CODE_PROPERTY, lastData.ThreadData.ThreadInformations[threadName].Stack.First().SourceCode); - } - - [UnityTest] - public IEnumerator TestSourceCodeAssignment_EnabledLogManagerAndSendUnhandledError_SourceCodeAvailable() - { - BacktraceData lastData = null; - BacktraceClient.BeforeSend = (BacktraceData data) => - { - lastData = data; - return data; - }; - - BacktraceClient.HandleUnityMessage("foo", string.Empty, LogType.Error); - yield return new WaitForEndOfFrame(); - Assert.IsNotNull(lastData.SourceCode); - - var threadName = lastData.ThreadData.MainThread; - Assert.AreEqual(BacktraceSourceCode.SOURCE_CODE_PROPERTY, lastData.ThreadData.ThreadInformations[threadName].Stack.First().SourceCode); - } - - [UnityTest] - public IEnumerator TestSourceCodeAssignment_EnabledLogManagerWithMultipleLogMessage_SourceCodeAvailable() - { - BacktraceData lastData = null; - BacktraceClient.BeforeSend = (BacktraceData data) => - { - lastData = data; - return data; - }; - //fake messages - var fakeLogMessage = "log"; - BacktraceClient.HandleUnityMessage(fakeLogMessage, string.Empty, LogType.Log); - var fakeWarningMessage = "warning message"; - BacktraceClient.HandleUnityMessage(fakeWarningMessage, string.Empty, LogType.Warning); - - // real exception - var expectedExceptionMessage = "Exception message"; - BacktraceClient.HandleUnityMessage(expectedExceptionMessage, string.Empty, LogType.Exception); - yield return new WaitForEndOfFrame(); - Assert.IsNotNull(lastData.SourceCode); - - var generatedText = lastData.SourceCode.Text; - Assert.IsTrue(generatedText.Contains(expectedExceptionMessage)); - Assert.IsTrue(generatedText.Contains(fakeLogMessage)); - Assert.IsTrue(generatedText.Contains(fakeWarningMessage)); - } - - [UnityTest] - public IEnumerator TestSourceCodeAssignment_EnabledLogManagerWithMultipleLogMessageAndExceptionReport_SourceCodeAvailable() - { - BacktraceData lastData = null; - BacktraceClient.BeforeSend = (BacktraceData data) => - { - lastData = data; - return data; - }; - //fake messages - var fakeLogMessage = "log"; - BacktraceClient.HandleUnityMessage(fakeLogMessage, string.Empty, LogType.Log); - yield return new WaitForEndOfFrame(); - var fakeWarningMessage = "warning message"; - BacktraceClient.HandleUnityMessage(fakeWarningMessage, string.Empty, LogType.Warning); - yield return new WaitForEndOfFrame(); - - // real exception - var expectedExceptionMessage = "Exception message"; - BacktraceClient.Send(new Exception(expectedExceptionMessage)); - yield return new WaitForEndOfFrame(); - - Assert.IsNotNull(lastData.SourceCode); - - var generatedText = lastData.SourceCode.Text; - Assert.IsTrue(generatedText.Contains(expectedExceptionMessage)); - Assert.IsTrue(generatedText.Contains(fakeLogMessage)); - Assert.IsTrue(generatedText.Contains(fakeWarningMessage)); - } - - - [UnityTest] - public IEnumerator TestSourceCodeAssignment_EnabledLogManagerWithMultipleLogMessageAndMessageReport_SourceCodeAvailable() - { - BacktraceData lastData = null; - BacktraceClient.BeforeSend = (BacktraceData data) => - { - lastData = data; - return data; - }; - //fake messages - var fakeLogMessage = "log"; - BacktraceClient.HandleUnityMessage(fakeLogMessage, string.Empty, LogType.Log); - yield return new WaitForEndOfFrame(); - var fakeWarningMessage = "warning message"; - BacktraceClient.HandleUnityMessage(fakeWarningMessage, string.Empty, LogType.Warning); - yield return new WaitForEndOfFrame(); - - // real exception - var expectedExceptionMessage = "Exception message"; - BacktraceClient.Send(expectedExceptionMessage); - yield return new WaitForEndOfFrame(); - Assert.IsNotNull(lastData.SourceCode); - - var generatedText = lastData.SourceCode.Text; - Assert.IsTrue(generatedText.Contains(expectedExceptionMessage)); - Assert.IsTrue(generatedText.Contains(fakeLogMessage)); - Assert.IsTrue(generatedText.Contains(fakeWarningMessage)); - } - - [UnityTest] - public IEnumerator TestSourceCodeAssignment_DisabledUnhandledException_ShouldStoreUnhandledExceptionInfo() - { - BacktraceClient.Configuration.HandleUnhandledExceptions = false; - - BacktraceData lastData = null; - BacktraceClient.BeforeSend = (BacktraceData data) => - { - lastData = data; - return data; - }; - - //fake messages - var fakeLogMessage = "log"; - BacktraceClient.HandleUnityMessage(fakeLogMessage, string.Empty, LogType.Log); - yield return new WaitForEndOfFrame(); - - var fakeWarningMessage = "warning message"; - BacktraceClient.HandleUnityMessage(fakeWarningMessage, string.Empty, LogType.Warning); - yield return new WaitForEndOfFrame(); - - // real exception - var expectedExceptionMessage = "Exception message"; - BacktraceClient.HandleUnityMessage(expectedExceptionMessage, string.Empty, LogType.Exception); - yield return new WaitForEndOfFrame(); - Assert.IsNull(api.LastData); - - var expectedReportMessage = "Report message"; - var report = new BacktraceReport(new Exception(expectedReportMessage)); - BacktraceClient.Send(report); - yield return new WaitForEndOfFrame(); - Assert.IsNotNull(lastData.SourceCode); - - var generatedText = lastData.SourceCode.Text; - Assert.IsTrue(generatedText.Contains(expectedExceptionMessage)); - Assert.IsTrue(generatedText.Contains(fakeLogMessage)); - Assert.IsTrue(generatedText.Contains(fakeWarningMessage)); - Assert.IsTrue(generatedText.Contains(expectedReportMessage)); - } - } -} diff --git a/Tests/Runtime/SourceCode/SourceCodeFlowWithoutLogManagerTests.cs b/Tests/Runtime/SourceCode/SourceCodeFlowWithoutLogManagerTests.cs deleted file mode 100644 index c0b7c4be..00000000 --- a/Tests/Runtime/SourceCode/SourceCodeFlowWithoutLogManagerTests.cs +++ /dev/null @@ -1,200 +0,0 @@ -using Backtrace.Unity.Model; -using NUnit.Framework; -using System; -using System.Collections; -using System.Linq; -using UnityEngine; -using UnityEngine.TestTools; - -namespace Backtrace.Unity.Tests.Runtime -{ - public class SourceCodeFlowWithoutLogManagerTests : BacktraceBaseTest - { - private readonly BacktraceApiMock api = new BacktraceApiMock(); - - [OneTimeSetUp] - public void Setup() - { - BeforeSetup(); - - var configuration = GetValidClientConfiguration(); - configuration.NumberOfLogs = 0; - BacktraceClient.Configuration = configuration; - AfterSetup(true); - BacktraceClient.BacktraceApi = api; - } - - [UnityTest] - public IEnumerator TestSourceCodeAssignment_DisabledLogManagerAndSendExceptionReport_SourceCodeAvailable() - { - - var invoked = false; - BacktraceClient.BeforeSend = (BacktraceData lastData) => - { - invoked = true; - Assert.IsNotNull(lastData.SourceCode); - - var threadName = lastData.ThreadData.MainThread; - Assert.AreEqual(BacktraceSourceCode.SOURCE_CODE_PROPERTY, lastData.ThreadData.ThreadInformations[threadName].Stack.First().SourceCode); - return lastData; - }; - BacktraceClient.Send(new Exception("foo")); - yield return new WaitForEndOfFrame(); - Assert.IsTrue(invoked); - - } - - - [UnityTest] - public IEnumerator TestSourceCodeAssignment_DisabledLogManagerAndSendMessageReport_SourceCodeAvailable() - { - var invoked = false; - BacktraceClient.BeforeSend = (BacktraceData lastData) => - { - invoked = true; - Assert.IsNotNull(lastData.SourceCode); - - var threadName = lastData.ThreadData.MainThread; - Assert.AreEqual(BacktraceSourceCode.SOURCE_CODE_PROPERTY, lastData.ThreadData.ThreadInformations[threadName].Stack.First().SourceCode); - return lastData; - }; - BacktraceClient.Send("foo"); - yield return new WaitForEndOfFrame(); - Assert.IsTrue(invoked); - } - - - [UnityTest] - public IEnumerator TestSourceCodeAssignment_DisabledLogManagerAndSendUnhandledException_SourceCodeAvailable() - { - var invoked = false; - BacktraceClient.BeforeSend = (BacktraceData lastData) => - { - invoked = true; - Assert.IsNotNull(lastData.SourceCode); - - var threadName = lastData.ThreadData.MainThread; - Assert.AreEqual(BacktraceSourceCode.SOURCE_CODE_PROPERTY, lastData.ThreadData.ThreadInformations[threadName].Stack.First().SourceCode); - return lastData; - }; - - BacktraceClient.HandleUnityMessage("foo", string.Empty, LogType.Exception); - yield return new WaitForEndOfFrame(); - Assert.IsTrue(invoked); - - } - - [UnityTest] - public IEnumerator TestSourceCodeAssignment_DisabledLogManagerAndSendUnhandledError_SourceCodeAvailable() - { - var invoked = false; - BacktraceClient.BeforeSend = (BacktraceData lastData) => - { - invoked = true; - - Assert.IsNotNull(lastData.SourceCode); - - var threadName = lastData.ThreadData.MainThread; - Assert.AreEqual(BacktraceSourceCode.SOURCE_CODE_PROPERTY, lastData.ThreadData.ThreadInformations[threadName].Stack.First().SourceCode); - return lastData; - }; - - BacktraceClient.HandleUnityMessage("foo", string.Empty, LogType.Error); - yield return new WaitForEndOfFrame(); - Assert.IsTrue(invoked); - } - - [UnityTest] - public IEnumerator TestSourceCodeAssignment_DisabledLogManagerWithMultipleLogMessage_SourceCodeAvailable() - { - BacktraceData lastData = null; - BacktraceClient.BeforeSend = (BacktraceData data) => - { - lastData = data; - return data; - }; - - - //fake messages - var fakeLogMessage = "log"; - BacktraceClient.HandleUnityMessage(fakeLogMessage, string.Empty, LogType.Log); - yield return new WaitForEndOfFrame(); - var fakeWarningMessage = "warning message"; - BacktraceClient.HandleUnityMessage(fakeWarningMessage, string.Empty, LogType.Warning); - yield return new WaitForEndOfFrame(); - - // real exception - var expectedExceptionMessage = "Exception message"; - BacktraceClient.HandleUnityMessage(expectedExceptionMessage, string.Empty, LogType.Exception); - yield return new WaitForEndOfFrame(); - - Assert.IsNotNull(lastData.SourceCode); - - var generatedText = lastData.SourceCode.Text; - Assert.IsTrue(generatedText.Contains(expectedExceptionMessage)); - Assert.IsFalse(generatedText.Contains(fakeLogMessage)); - Assert.IsFalse(generatedText.Contains(fakeWarningMessage)); - } - - [UnityTest] - public IEnumerator TestSourceCodeAssignment_DisabledLogManagerWithMultipleLogMessageAndExceptionReport_SourceCodeAvailable() - { - BacktraceData lastData = null; - BacktraceClient.BeforeSend = (BacktraceData data) => - { - lastData = data; - return data; - }; - - - //fake messages - var fakeLogMessage = "log"; - BacktraceClient.HandleUnityMessage(fakeLogMessage, string.Empty, UnityEngine.LogType.Log); - yield return new WaitForEndOfFrame(); - var fakeWarningMessage = "warning message"; - BacktraceClient.HandleUnityMessage(fakeWarningMessage, string.Empty, UnityEngine.LogType.Warning); - yield return new WaitForEndOfFrame(); - - // real exception - var expectedExceptionMessage = "Exception message"; - BacktraceClient.Send(new Exception(expectedExceptionMessage)); - yield return new WaitForEndOfFrame(); - - Assert.IsNotNull(lastData.SourceCode); - - var generatedText = lastData.SourceCode.Text; - Assert.IsTrue(generatedText.Contains(expectedExceptionMessage)); - Assert.IsFalse(generatedText.Contains(fakeLogMessage)); - Assert.IsFalse(generatedText.Contains(fakeWarningMessage)); - } - - - [UnityTest] - public IEnumerator TestSourceCodeAssignment_DisabledLogManagerWithMultipleLogMessageAndMessageReport_SourceCodeAvailable() - { - BacktraceData lastData = null; - BacktraceClient.BeforeSend = (BacktraceData data) => - { - lastData = data; - return data; - }; - //fake messages - var fakeLogMessage = "log"; - BacktraceClient.HandleUnityMessage(fakeLogMessage, string.Empty, UnityEngine.LogType.Log); - var fakeWarningMessage = "warning message"; - BacktraceClient.HandleUnityMessage(fakeWarningMessage, string.Empty, UnityEngine.LogType.Warning); - - // real exception - var expectedExceptionMessage = "Exception message"; - BacktraceClient.Send(expectedExceptionMessage); - yield return new WaitForEndOfFrame(); - - Assert.IsNotNull(lastData.SourceCode); - - var generatedText = lastData.SourceCode.Text; - Assert.IsTrue(generatedText.Contains(expectedExceptionMessage)); - Assert.IsFalse(generatedText.Contains(fakeLogMessage)); - Assert.IsFalse(generatedText.Contains(fakeWarningMessage)); - } - } -} diff --git a/Tests/Runtime/SourceCode/SourceCodeFlowWithoutLogManagerTests.cs.meta b/Tests/Runtime/SourceCode/SourceCodeFlowWithoutLogManagerTests.cs.meta deleted file mode 100644 index 20afdf19..00000000 --- a/Tests/Runtime/SourceCode/SourceCodeFlowWithoutLogManagerTests.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: d8b98b46e709f79498f1a896b966aa41 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: From b5873c6cea0618e99687c7abd0ea38712fb7490d Mon Sep 17 00:00:00 2001 From: kdysput Date: Wed, 5 May 2021 16:27:23 +0200 Subject: [PATCH 02/42] Breadcrumbs improvements --- Runtime/BacktraceClient.cs | 6 +-- Runtime/Common/DateTimeHelper.cs | 28 ++++++++++--- Runtime/Json/BacktraceJObject.cs | 11 +++--- Runtime/Model/BacktraceConfiguration.cs | 6 +-- Runtime/Model/BacktraceHttpClient.cs | 3 +- ...mbsLevel.cs => BacktraceBreadcrumbType.cs} | 2 +- ...s.meta => BacktraceBreadcrumbType.cs.meta} | 2 +- .../Model/Breadcrumbs/BacktraceBreadcrumbs.cs | 39 +++++++++++-------- .../BacktraceBreadcrumbsEventHandler.cs | 16 ++++---- .../Breadcrumbs/BacktraceStorageLogManager.cs | 13 ++++--- .../Breadcrumbs/IBacktraceBreadcrumbs.cs | 4 +- .../Model/Breadcrumbs/IBacktraceLogManager.cs | 2 +- .../Model/Breadcrumbs/UnityEngineLogLevel.cs | 6 +-- Runtime/Services/BacktraceApi.cs | 2 +- 14 files changed, 82 insertions(+), 58 deletions(-) rename Runtime/Model/Breadcrumbs/{BacktraceBreadcrumbsLevel.cs => BacktraceBreadcrumbType.cs} (87%) rename Runtime/Model/Breadcrumbs/{BacktraceBreadcrumbsLevel.cs.meta => BacktraceBreadcrumbType.cs.meta} (83%) diff --git a/Runtime/BacktraceClient.cs b/Runtime/BacktraceClient.cs index 13804a6d..957a7178 100644 --- a/Runtime/BacktraceClient.cs +++ b/Runtime/BacktraceClient.cs @@ -54,7 +54,7 @@ public IBacktraceBreadcrumbs Breadcrumbs /// /// Client report attachments /// - private List _clientReportAttachments; + private HashSet _clientReportAttachments; /// /// Attribute object accessor @@ -90,7 +90,7 @@ public void AddAttachment(string pathToAttachment) /// Returns list of defined path to attachments stored by Backtrace client. /// /// List of client attachments - public List GetAttachments() + public IEnumerable GetAttachments() { return _clientReportAttachments; } @@ -472,7 +472,7 @@ public void Refresh() } if (Configuration.EnableEventAggregationSupport && !string.IsNullOrEmpty(Configuration.EventAggregationSubmissionUrl)) { - EnableSessionAgregationSupport(Configuration.EventAggregationSubmissionUrl, Configuration.GetEventAggregationIntervalTimerInMs(), Configuration.MaximumNumberOfEvents); + //EnableSessionAgregationSupport(Configuration.EventAggregationSubmissionUrl, Configuration.GetEventAggregationIntervalTimerInMs(), Configuration.MaximumNumberOfEvents); } } diff --git a/Runtime/Common/DateTimeHelper.cs b/Runtime/Common/DateTimeHelper.cs index 97684a43..2b078f6f 100644 --- a/Runtime/Common/DateTimeHelper.cs +++ b/Runtime/Common/DateTimeHelper.cs @@ -1,16 +1,34 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace Backtrace.Unity.Common { internal static class DateTimeHelper { + /// + /// Current time in Timespan. + /// Warning: We keep this code, because modern api that calculates + /// timestamp is not available in the .NET 3.5/.NET 2.0. + /// + /// Timespan that represents DateTime.Now + private static TimeSpan Now() + { + return (DateTime.UtcNow.Subtract(new DateTime(1970, 1, 1))); + } + /// + /// Generates timestamp in sec + /// + /// Timestamp in sec public static int Timestamp() { - return (int)(DateTime.UtcNow.Subtract(new DateTime(1970, 1, 1))).TotalSeconds; + return (int)(Now()).TotalSeconds; + } + /// + /// Generates timestamp in ms + /// + /// Timestamp in ms + public static double TimestampMs() + { + return Now().TotalMilliseconds; } } } diff --git a/Runtime/Json/BacktraceJObject.cs b/Runtime/Json/BacktraceJObject.cs index 9296015e..200497f3 100644 --- a/Runtime/Json/BacktraceJObject.cs +++ b/Runtime/Json/BacktraceJObject.cs @@ -1,5 +1,4 @@ -using System; -using System.Collections; +using System.Collections; using System.Collections.Generic; using System.Globalization; using System.Text; @@ -57,9 +56,9 @@ public void Add(string key, bool value) /// /// JSON key /// value - public void Add(string key, float value) + public void Add(string key, float value, string format = "G") { - PrimitiveValues.Add(key, value.ToString("G", CultureInfo.InvariantCulture)); + PrimitiveValues.Add(key, value.ToString(format, CultureInfo.InvariantCulture)); } /// @@ -67,9 +66,9 @@ public void Add(string key, float value) /// /// JSON key /// value - public void Add(string key, double value) + public void Add(string key, double value, string format = "G") { - PrimitiveValues.Add(key, value.ToString("G", CultureInfo.InvariantCulture)); + PrimitiveValues.Add(key, value.ToString(format, CultureInfo.InvariantCulture)); } /// diff --git a/Runtime/Model/BacktraceConfiguration.cs b/Runtime/Model/BacktraceConfiguration.cs index ae7f5bdd..c4e08798 100644 --- a/Runtime/Model/BacktraceConfiguration.cs +++ b/Runtime/Model/BacktraceConfiguration.cs @@ -157,7 +157,7 @@ public class BacktraceConfiguration : ScriptableObject /// Backtrace breadcrumbs log level controls what type of information will be available in the breadcrumbs file /// [Tooltip("Breadcrumbs support breadcrumbs level- Backtrace breadcrumbs log level controls what type of information will be available in the breadcrumb file")] - public BacktraceBreadcrumbsLevel BacktraceBreadcrumbsLevel; + public BacktraceBreadcrumbType BacktraceBreadcrumbsLevel; /// /// Backtrace Unity Engine log Level controls what log types will be included in the final breadcrumbs file @@ -271,9 +271,9 @@ public class BacktraceConfiguration : ScriptableObject /// Get full paths to attachments added by client /// /// List of absolute path to attachments - public List GetAttachmentPaths() + public HashSet GetAttachmentPaths() { - var result = new List(); + var result = new HashSet(); if (AttachmentPaths == null || AttachmentPaths.Length == 0) { return result; diff --git a/Runtime/Model/BacktraceHttpClient.cs b/Runtime/Model/BacktraceHttpClient.cs index 76306e9d..ca1da529 100644 --- a/Runtime/Model/BacktraceHttpClient.cs +++ b/Runtime/Model/BacktraceHttpClient.cs @@ -127,7 +127,8 @@ private void AddAttachmentToFormData(List formData, IEnum // make sure attachments are not bigger than 10 Mb. const int maximumAttachmentSize = 10000000; const string attachmentPrefix = "attachment_"; - foreach (var file in attachments) + var uniqueAttachments = new HashSet(attachments); + foreach (var file in uniqueAttachments) { if (File.Exists(file) && new FileInfo(file).Length < maximumAttachmentSize) { diff --git a/Runtime/Model/Breadcrumbs/BacktraceBreadcrumbsLevel.cs b/Runtime/Model/Breadcrumbs/BacktraceBreadcrumbType.cs similarity index 87% rename from Runtime/Model/Breadcrumbs/BacktraceBreadcrumbsLevel.cs rename to Runtime/Model/Breadcrumbs/BacktraceBreadcrumbType.cs index 5d8d87c2..6efc1f78 100644 --- a/Runtime/Model/Breadcrumbs/BacktraceBreadcrumbsLevel.cs +++ b/Runtime/Model/Breadcrumbs/BacktraceBreadcrumbType.cs @@ -6,7 +6,7 @@ namespace Backtrace.Unity.Model.Breadcrumbs /// Breadcrumbs level /// [Flags] - public enum BacktraceBreadcrumbsLevel + public enum BacktraceBreadcrumbType { Manual = BreadcrumbLevel.Manual, Log = BreadcrumbLevel.Log, diff --git a/Runtime/Model/Breadcrumbs/BacktraceBreadcrumbsLevel.cs.meta b/Runtime/Model/Breadcrumbs/BacktraceBreadcrumbType.cs.meta similarity index 83% rename from Runtime/Model/Breadcrumbs/BacktraceBreadcrumbsLevel.cs.meta rename to Runtime/Model/Breadcrumbs/BacktraceBreadcrumbType.cs.meta index 21c653f6..40fa546d 100644 --- a/Runtime/Model/Breadcrumbs/BacktraceBreadcrumbsLevel.cs.meta +++ b/Runtime/Model/Breadcrumbs/BacktraceBreadcrumbType.cs.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: 508c4d81da57a174380f6de284a8f4c0 +guid: 59fdb5a3f444d5946bfaeec18dcee685 MonoImporter: externalObjects: {} serializedVersion: 2 diff --git a/Runtime/Model/Breadcrumbs/BacktraceBreadcrumbs.cs b/Runtime/Model/Breadcrumbs/BacktraceBreadcrumbs.cs index f21582a0..3d377b60 100644 --- a/Runtime/Model/Breadcrumbs/BacktraceBreadcrumbs.cs +++ b/Runtime/Model/Breadcrumbs/BacktraceBreadcrumbs.cs @@ -9,7 +9,7 @@ internal sealed class BacktraceBreadcrumbs : IBacktraceBreadcrumbs /// /// Breadcrumbs log level /// - public BacktraceBreadcrumbsLevel BreadcrumbsLevel { get; internal set; } + public BacktraceBreadcrumbType BreadcrumbsLevel { get; internal set; } /// /// Unity engine log level @@ -58,7 +58,7 @@ public bool Debug(string message) return AddBreadcrumbs(message, LogType.Assert); } - public bool EnableBreadcrumbs(BacktraceBreadcrumbsLevel level, UnityEngineLogLevel unityLogLevel) + public bool EnableBreadcrumbs(BacktraceBreadcrumbType level, UnityEngineLogLevel unityLogLevel) { if (_enabled) { @@ -88,7 +88,7 @@ public bool Exception(string message) public bool FromBacktrace(BacktraceReport report) { - var type = report.ExceptionTypeReport ? LogType.Exception : LogType.Log; + var type = report.ExceptionTypeReport ? UnityEngineLogLevel.Error : UnityEngineLogLevel.Info; if (!ShouldLog(type)) { return false; @@ -102,7 +102,7 @@ public bool FromBacktrace(BacktraceReport report) public bool FromMonoBehavior(string message, LogType type, IDictionary attributes) { - return AddBreadcrumbs(message, BreadcrumbLevel.System, type, attributes); + return AddBreadcrumbs(message, BreadcrumbLevel.System, ConvertLogTypeToLogLevel(type), attributes); } public string GetBreadcrumbLogPath() @@ -144,43 +144,48 @@ public bool Exception(string message, IDictionary attributes) { return AddBreadcrumbs(message, LogType.Exception, attributes); } - public bool AddBreadcrumbs(string message, LogType type, IDictionary attributes) + public bool AddBreadcrumbs(string message, LogType logType, IDictionary attributes) { + var type = ConvertLogTypeToLogLevel(logType); if (!ShouldLog(type)) { return false; } return AddBreadcrumbs(message, BreadcrumbLevel.Manual, type, attributes); } - internal bool AddBreadcrumbs(string message, BreadcrumbLevel level, LogType type, IDictionary attributes = null) + internal bool AddBreadcrumbs(string message, BreadcrumbLevel level, UnityEngineLogLevel type, IDictionary attributes = null) { - if (!BreadcrumbsLevel.HasFlag((BacktraceBreadcrumbsLevel)level)) + if (!BreadcrumbsLevel.HasFlag((BacktraceBreadcrumbType)level)) { return false; } return LogManager.Add(message, level, type, attributes); } - - internal bool ShouldLog(LogType type) + internal bool ShouldLog(UnityEngineLogLevel type) { - if (!BreadcrumbsLevel.HasFlag(BacktraceBreadcrumbsLevel.Manual)) + if (!BreadcrumbsLevel.HasFlag(BacktraceBreadcrumbType.Manual)) { return false; } + return UnityLogLevel.HasFlag(type); + } + + internal UnityEngineLogLevel ConvertLogTypeToLogLevel(LogType type) + { switch (type) { - case LogType.Log: - return UnityLogLevel.HasFlag(UnityEngineLogLevel.Log); case LogType.Warning: - return UnityLogLevel.HasFlag(UnityEngineLogLevel.Warning); + return UnityEngineLogLevel.Warning; case LogType.Exception: - return UnityLogLevel.HasFlag(UnityEngineLogLevel.Exception); + return UnityEngineLogLevel.Fatal; case LogType.Error: - return UnityLogLevel.HasFlag(UnityEngineLogLevel.Error); + return UnityEngineLogLevel.Error; case LogType.Assert: - return UnityLogLevel.HasFlag(UnityEngineLogLevel.Assert); + return UnityEngineLogLevel.Debug; + case LogType.Log: + default: + return UnityEngineLogLevel.Info; } - return false; } } } diff --git a/Runtime/Model/Breadcrumbs/BacktraceBreadcrumbsEventHandler.cs b/Runtime/Model/Breadcrumbs/BacktraceBreadcrumbsEventHandler.cs index 2987ad75..87c1bb3f 100644 --- a/Runtime/Model/Breadcrumbs/BacktraceBreadcrumbsEventHandler.cs +++ b/Runtime/Model/Breadcrumbs/BacktraceBreadcrumbsEventHandler.cs @@ -1,5 +1,4 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Threading; using UnityEngine; using UnityEngine.SceneManagement; @@ -9,7 +8,7 @@ namespace Backtrace.Unity.Model.Breadcrumbs internal sealed class BacktraceBreadcrumbsEventHandler { private readonly BacktraceBreadcrumbs _breadcrumbs; - private BacktraceBreadcrumbsLevel _registeredLevel; + private BacktraceBreadcrumbType _registeredLevel; private Thread _thread; public BacktraceBreadcrumbsEventHandler(BacktraceBreadcrumbs breadcrumbs) { @@ -20,10 +19,10 @@ public BacktraceBreadcrumbsEventHandler(BacktraceBreadcrumbs breadcrumbs) /// Register unity events that will generate logs in the breadcrumbs file /// /// Breadcrumbs level - public void Register(BacktraceBreadcrumbsLevel level) + public void Register(BacktraceBreadcrumbType level) { _registeredLevel = level; - if (!level.HasFlag(BacktraceBreadcrumbsLevel.System)) + if (!level.HasFlag(BacktraceBreadcrumbType.System)) { return; } @@ -42,7 +41,7 @@ public void Register(BacktraceBreadcrumbsLevel level) /// public void Unregister() { - if (!_registeredLevel.HasFlag(BacktraceBreadcrumbsLevel.System)) + if (!_registeredLevel.HasFlag(BacktraceBreadcrumbType.System)) { return; } @@ -107,11 +106,12 @@ private void Application_focusChanged(bool hasFocus) private void Log(string message, LogType level, IDictionary attributes = null) { - if (!_breadcrumbs.ShouldLog(level)) + var type = _breadcrumbs.ConvertLogTypeToLogLevel(level); + if (!_breadcrumbs.ShouldLog(type)) { return; } - _breadcrumbs.AddBreadcrumbs(message, BreadcrumbLevel.System, level, attributes); + _breadcrumbs.AddBreadcrumbs(message, BreadcrumbLevel.System, type, attributes); } } } diff --git a/Runtime/Model/Breadcrumbs/BacktraceStorageLogManager.cs b/Runtime/Model/Breadcrumbs/BacktraceStorageLogManager.cs index 5100176b..b45a8dea 100644 --- a/Runtime/Model/Breadcrumbs/BacktraceStorageLogManager.cs +++ b/Runtime/Model/Breadcrumbs/BacktraceStorageLogManager.cs @@ -2,8 +2,8 @@ using Backtrace.Unity.Json; using System; using System.Collections.Generic; +using System.Globalization; using System.IO; -using UnityEngine; namespace Backtrace.Unity.Model.Breadcrumbs { @@ -128,7 +128,7 @@ public bool Enable() /// Breadcrumb type /// Breadcrumb attributs /// True if breadcrumb was stored in the breadcrumbs file. Otherwise false. - public bool Add(string message, BreadcrumbLevel level, LogType type, IDictionary attributes) + public bool Add(string message, BreadcrumbLevel level, UnityEngineLogLevel type, IDictionary attributes) { byte[] bytes; lock (_lockObject) @@ -173,14 +173,15 @@ private BacktraceJObject CreateBreadcrumbJson( long id, string message, BreadcrumbLevel level, - LogType type, + UnityEngineLogLevel type, IDictionary attributes) { var jsonObject = new BacktraceJObject(); - jsonObject.Add("timestamp", DateTimeHelper.Timestamp()); + // breadcrumbs integration accepts timestamp in ms not in sec. + jsonObject.Add("timestamp", DateTimeHelper.TimestampMs(), "F0"); jsonObject.Add("id", id); - jsonObject.Add("level", Enum.GetName(typeof(BreadcrumbLevel), level)); - jsonObject.Add("type", Enum.GetName(typeof(LogType), type)); + jsonObject.Add("type", Enum.GetName(typeof(BreadcrumbLevel), level).ToLower()); + jsonObject.Add("level", Enum.GetName(typeof(UnityEngineLogLevel), type).ToLower()); jsonObject.Add("message", message); jsonObject.Add("attributes", new BacktraceJObject(attributes)); return jsonObject; diff --git a/Runtime/Model/Breadcrumbs/IBacktraceBreadcrumbs.cs b/Runtime/Model/Breadcrumbs/IBacktraceBreadcrumbs.cs index a8f9acc0..fa8f8b1c 100644 --- a/Runtime/Model/Breadcrumbs/IBacktraceBreadcrumbs.cs +++ b/Runtime/Model/Breadcrumbs/IBacktraceBreadcrumbs.cs @@ -6,8 +6,8 @@ namespace Backtrace.Unity.Model.Breadcrumbs { public interface IBacktraceBreadcrumbs { - BacktraceBreadcrumbsLevel BreadcrumbsLevel { get; } - bool EnableBreadcrumbs(BacktraceBreadcrumbsLevel level, UnityEngineLogLevel unityLogLevel); + BacktraceBreadcrumbType BreadcrumbsLevel { get; } + bool EnableBreadcrumbs(BacktraceBreadcrumbType level, UnityEngineLogLevel unityLogLevel); bool ClearBreadcrumbs(); bool AddBreadcrumbs(string message, LogType type, IDictionary attributes); bool AddBreadcrumbs(string message, LogType type); diff --git a/Runtime/Model/Breadcrumbs/IBacktraceLogManager.cs b/Runtime/Model/Breadcrumbs/IBacktraceLogManager.cs index da810d87..184f81b9 100644 --- a/Runtime/Model/Breadcrumbs/IBacktraceLogManager.cs +++ b/Runtime/Model/Breadcrumbs/IBacktraceLogManager.cs @@ -6,7 +6,7 @@ namespace Backtrace.Unity.Model.Breadcrumbs internal interface IBacktraceLogManager { string BreadcrumbsFilePath { get; } - bool Add(string message, BreadcrumbLevel level, LogType type, IDictionary attributes); + bool Add(string message, BreadcrumbLevel level, UnityEngineLogLevel type, IDictionary attributes); bool Clear(); bool Enable(); } diff --git a/Runtime/Model/Breadcrumbs/UnityEngineLogLevel.cs b/Runtime/Model/Breadcrumbs/UnityEngineLogLevel.cs index df00cd4e..fe4d73ab 100644 --- a/Runtime/Model/Breadcrumbs/UnityEngineLogLevel.cs +++ b/Runtime/Model/Breadcrumbs/UnityEngineLogLevel.cs @@ -8,10 +8,10 @@ namespace Backtrace.Unity.Model.Breadcrumbs [Flags] public enum UnityEngineLogLevel { - Assert = 1, + Debug = 1, Warning = 2, - Log = 4, - Exception = 8, + Info = 4, + Fatal = 8, Error = 16 } } diff --git a/Runtime/Services/BacktraceApi.cs b/Runtime/Services/BacktraceApi.cs index 2d1dffe7..a0cb683c 100644 --- a/Runtime/Services/BacktraceApi.cs +++ b/Runtime/Services/BacktraceApi.cs @@ -104,7 +104,7 @@ public IEnumerator SendMinidump(string minidumpPath, IEnumerable attachm { if (attachments == null) { - attachments = new List(); + attachments = new HashSet(); } var stopWatch = EnablePerformanceStatistics From b30b700696ab712c54600c3d28e10aa36c32749e Mon Sep 17 00:00:00 2001 From: kdysput Date: Wed, 5 May 2021 22:53:17 +0200 Subject: [PATCH 03/42] Database adjustements --- Runtime/BacktraceDatabase.cs | 133 ++++++++++---- .../Interfaces/IBacktraceDatabaseContext.cs | 37 ++-- .../IBacktraceDatabaseFileContext.cs | 29 ++- .../Model/Database/BacktraceDatabaseRecord.cs | 147 +--------------- Runtime/Services/BacktraceDatabaseContext.cs | 155 +++------------- .../Services/BacktraceDatabaseFileContext.cs | 165 +++++++++++++++++- .../Mocks/BacktraceDatabaseContextMock.cs | 9 - 7 files changed, 337 insertions(+), 338 deletions(-) diff --git a/Runtime/BacktraceDatabase.cs b/Runtime/BacktraceDatabase.cs index bcdef5ee..f76285ba 100644 --- a/Runtime/BacktraceDatabase.cs +++ b/Runtime/BacktraceDatabase.cs @@ -1,7 +1,7 @@ using Backtrace.Unity.Common; using Backtrace.Unity.Interfaces; using Backtrace.Unity.Model; -using Backtrace.Unity.Model.Breadcrumbs; +using Backtrace.Unity.Model.Breadcrumbs; using Backtrace.Unity.Model.Database; using Backtrace.Unity.Services; using Backtrace.Unity.Types; @@ -9,6 +9,7 @@ using System.Collections.Generic; using System.Globalization; using System.IO; +using System.Linq; using UnityEngine; namespace Backtrace.Unity @@ -21,10 +22,10 @@ public class BacktraceDatabase : MonoBehaviour, IBacktraceDatabase { private bool _timerBackgroundWork = false; - public BacktraceConfiguration Configuration; - - /// - /// Backtrace Breadcrumbs + public BacktraceConfiguration Configuration; + + /// + /// Backtrace Breadcrumbs /// public IBacktraceBreadcrumbs Breadcrumbs { get; private set; } @@ -150,7 +151,7 @@ public void Reload() LastFrameTime = Time.unscaledTime; //Setup database context BacktraceDatabaseContext = new BacktraceDatabaseContext(DatabaseSettings); - BacktraceDatabaseFileContext = new BacktraceDatabaseFileContext(DatabaseSettings.DatabasePath, DatabaseSettings.MaxDatabaseSize, DatabaseSettings.MaxRecordCount); + BacktraceDatabaseFileContext = new BacktraceDatabaseFileContext(DatabaseSettings); BacktraceApi = new BacktraceApi(Configuration.ToCredentials()); _reportLimitWatcher = new ReportLimitWatcher(Convert.ToUInt32(Configuration.ReportPerMin)); EnableBreadcrumbsSupport(); @@ -215,7 +216,10 @@ private void Start() if (DatabaseSettings.AutoSendMode) { _lastConnection = Time.unscaledTime; - SendData(BacktraceDatabaseContext.FirstOrDefault()); + if (BacktraceDatabaseContext.Any()) + { + SendData(BacktraceDatabaseContext.FirstOrDefault()); + } } } @@ -279,7 +283,47 @@ public BacktraceDatabaseRecord Add(BacktraceData data, bool @lock = true) { return null; } - var record = BacktraceDatabaseContext.Add(data); + + // validate if record already exists in the database object + var hash = BacktraceDatabaseContext.GetHash(data); + if (!string.IsNullOrEmpty(hash)) + { + var existingRecord = BacktraceDatabaseContext.GetRecordByHash(hash); + if (existingRecord != null) + { + BacktraceDatabaseContext.AddDuplicate(existingRecord); + return existingRecord; + } + } + + // now we now we're adding new unique report to database + var record = new BacktraceDatabaseRecord(data) + { + Hash = hash + }; + + //add built-in attachments + var attachments = BacktraceDatabaseFileContext.GenerateRecordAttachments(data); + for (int attachmentIndex = 0; attachmentIndex < attachments.Count(); attachmentIndex++) + { + if (!string.IsNullOrEmpty(attachments.ElementAt(attachmentIndex))) + { + data.Attachments.Add(attachments.ElementAt(attachmentIndex)); + record.Attachments.Add(attachments.ElementAt(attachmentIndex)); + } + } + + // save record on the hard drive and add it to database context + var saveResult = BacktraceDatabaseFileContext.Save(record); + if (!saveResult) + { + // file context won't remove json object that wasn't stored in the previous method + // but will clean up attachments associated with this record. + BacktraceDatabaseFileContext.Delete(record); + return null; + } + + BacktraceDatabaseContext.Add(record); if (!@lock) { record.Unlock(); @@ -297,15 +341,8 @@ public BacktraceDatabaseRecord Add(BacktraceReport backtraceReport, Dictionary Get() public void Delete(BacktraceDatabaseRecord record) { if (BacktraceDatabaseContext != null) + { BacktraceDatabaseContext.Delete(record); + } + if (BacktraceDatabaseFileContext != null) + { + BacktraceDatabaseFileContext.Delete(record); + } } /// @@ -423,7 +466,7 @@ private void SendData(BacktraceDatabaseRecord record) } else { - BacktraceDatabaseContext.IncrementBatchRetry(); + IncrementBatchRetry(); return; } bool shouldProcess = _reportLimitWatcher.WatchReport(DateTimeHelper.Timestamp()); @@ -521,13 +564,12 @@ protected virtual void LoadReports() { continue; } - record.DatabasePath(DatabaseSettings.DatabasePath); - if (!record.Valid()) + if (!BacktraceDatabaseFileContext.IsValidRecord(record)) { try { Debug.Log("Removing record from Backtrace Database path - invalid record."); - record.Delete(); + BacktraceDatabaseFileContext.Delete(record); } catch (Exception) { @@ -566,7 +608,12 @@ private bool ValidateDatabaseSize() int deletePolicyRetry = 5; while (BacktraceDatabaseContext.GetSize() > DatabaseSettings.MaxDatabaseSize) { - BacktraceDatabaseContext.RemoveLastRecord(); + var lastRecord = BacktraceDatabaseContext.LastOrDefault(); + if (lastRecord != null) + { + BacktraceDatabaseContext.Delete(lastRecord); + BacktraceDatabaseFileContext.Delete(lastRecord); + } deletePolicyRetry--; if (deletePolicyRetry != 0) { @@ -599,20 +646,34 @@ public long GetDatabaseSize() public void SetReportWatcher(ReportLimitWatcher reportLimitWatcher) { _reportLimitWatcher = reportLimitWatcher; - } - - public bool EnableBreadcrumbsSupport() - { - if (!Enable || !Configuration.EnableBreadcrumbsSupport) - { - return false; - } - if (Breadcrumbs != null) - { - return true; - } - Breadcrumbs = new BacktraceBreadcrumbs(new BacktraceStorageLogManager(Configuration.GetFullDatabasePath())); - return Breadcrumbs.EnableBreadcrumbs(Configuration.BacktraceBreadcrumbsLevel, Configuration.LogLevel); - } + } + + private void IncrementBatchRetry() + { + var data = BacktraceDatabaseContext.GetRecordsToDelete(); + BacktraceDatabaseContext.IncrementBatchRetry(); + if (data != null && data.Count() != 0) + { + foreach (var item in data) + { + BacktraceDatabaseFileContext.Delete(item); + } + } + } + + + public bool EnableBreadcrumbsSupport() + { + if (!Enable || !Configuration.EnableBreadcrumbsSupport) + { + return false; + } + if (Breadcrumbs != null) + { + return true; + } + Breadcrumbs = new BacktraceBreadcrumbs(new BacktraceStorageLogManager(Configuration.GetFullDatabasePath())); + return Breadcrumbs.EnableBreadcrumbs(Configuration.BacktraceBreadcrumbsLevel, Configuration.LogLevel); + } } } diff --git a/Runtime/Interfaces/IBacktraceDatabaseContext.cs b/Runtime/Interfaces/IBacktraceDatabaseContext.cs index 21c5058a..8187b555 100644 --- a/Runtime/Interfaces/IBacktraceDatabaseContext.cs +++ b/Runtime/Interfaces/IBacktraceDatabaseContext.cs @@ -8,11 +8,6 @@ namespace Backtrace.Unity.Interfaces { public interface IBacktraceDatabaseContext : IDisposable { - /// - /// Add new data to database - /// - /// Database record - BacktraceDatabaseRecord Add(BacktraceData backtraceData); /// /// Add new data to database @@ -90,14 +85,34 @@ public interface IBacktraceDatabaseContext : IDisposable int GetTotalNumberOfRecords(); /// - /// Remove last record in database. + /// Context deduplication strategy /// - /// If algorithm can remove last record, method return true. Otherwise false - bool RemoveLastRecord(); + DeduplicationStrategy DeduplicationStrategy { get; set; } /// - /// Context deduplication strategy + /// Returns path to files from last batch /// - DeduplicationStrategy DeduplicationStrategy { get; set; } + /// Path to files available in the last batch + IEnumerable GetRecordsToDelete(); + + /// + /// Get hash from backtrace data based on the database deduplication rules + /// + /// Backtrace diagnostic object + /// hash + string GetHash(BacktraceData backtraceData); + + /// + /// Returns database record based on the backtrace data hash + /// + /// Diagnostic object hash + /// Record if record associated to the hash exists + BacktraceDatabaseRecord GetRecordByHash(string hash); + + /// + /// Add duplicate to database context + /// + /// Duplicated record + void AddDuplicate(BacktraceDatabaseRecord record); } -} +} \ No newline at end of file diff --git a/Runtime/Interfaces/IBacktraceDatabaseFileContext.cs b/Runtime/Interfaces/IBacktraceDatabaseFileContext.cs index bbe74fc1..24f045b1 100644 --- a/Runtime/Interfaces/IBacktraceDatabaseFileContext.cs +++ b/Runtime/Interfaces/IBacktraceDatabaseFileContext.cs @@ -1,4 +1,5 @@ -using Backtrace.Unity.Model.Database; +using Backtrace.Unity.Model; +using Backtrace.Unity.Model.Database; using System.Collections.Generic; using System.IO; @@ -33,5 +34,31 @@ internal interface IBacktraceDatabaseFileContext /// Remove all files from database directory /// void Clear(); + + /// + /// Deletes backtrace database record from persistent data storage + /// + /// Database record + void Delete(BacktraceDatabaseRecord record); + + /// + /// Generates list of attachments for current diagnostic data record + /// + /// Backtrace data + IEnumerable GenerateRecordAttachments(BacktraceData data); + + /// + /// Saves BacktraceDatabaseRerord on the hard drive + /// + /// BacktraceDatabaseRecord + /// true if file context was able to save data on the hard drive. Otherwise false + bool Save(BacktraceDatabaseRecord record); + + /// + /// Determine if BacktraceDatabaseRecord is valid. + /// + /// Database record + /// True, if the record exists. Otherwise false. + bool IsValidRecord(BacktraceDatabaseRecord record); } } diff --git a/Runtime/Model/Database/BacktraceDatabaseRecord.cs b/Runtime/Model/Database/BacktraceDatabaseRecord.cs index 4006d272..565befea 100644 --- a/Runtime/Model/Database/BacktraceDatabaseRecord.cs +++ b/Runtime/Model/Database/BacktraceDatabaseRecord.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.IO; -using System.Text; using UnityEngine; namespace Backtrace.Unity.Model.Database @@ -46,11 +45,6 @@ public class BacktraceDatabaseRecord /// internal BacktraceData Record { get; set; } - /// - /// Path to database directory - /// - private string _path = string.Empty; - /// /// Attachments path /// @@ -165,86 +159,13 @@ private BacktraceDatabaseRecord(BacktraceDatabaseRawRecord rawRecord) /// Create new instance of database record /// /// Diagnostic data - /// database path - public BacktraceDatabaseRecord(BacktraceData data, string path) + public BacktraceDatabaseRecord(BacktraceData data) { Id = data.Uuid; Record = data; - _path = path; Attachments = data.Attachments; } - /// - /// Save data to hard drive - /// - /// True if record was successfully saved on hard drive - public bool Save() - { - try - { - var jsonPrefix = Record.UuidString; - _diagnosticDataJson = Record.ToJson(); - DiagnosticDataPath = Path.Combine(_path, string.Format("{0}-attachment.json", jsonPrefix)); - Save(_diagnosticDataJson, DiagnosticDataPath); - - if (Attachments != null && Attachments.Count != 0) - { - foreach (var attachment in Attachments) - { - if (IsInsideDatabaseDirectory(attachment)) - { - Size += new FileInfo(attachment).Length; - } - } - } - //save record - RecordPath = Path.Combine(_path, string.Format("{0}-record.json", jsonPrefix)); - Save(ToJson(), RecordPath); - return true; - } - catch (IOException io) - { - Debug.Log("Received IOException while saving data to database."); - Debug.Log(io.Message); - return false; - } - catch (Exception ex) - { - Debug.Log(string.Format("Received {0} while saving data to database.", ex.GetType().Name)); - Debug.Log(string.Format("Message {0}", ex.Message)); - return false; - } - } - - /// - /// Setup RecordWriter and database path after deserialization event - /// - /// Path to database - internal void DatabasePath(string path) - { - _path = path; - } - - /// - /// Save single file from database record - /// - /// single file (json/dmp) - /// file path - private void Save(string json, string destPath) - { - if (string.IsNullOrEmpty(json)) - { - return; - } - byte[] file = Encoding.UTF8.GetBytes(json); - Size += file.Length; - - using (var fs = new FileStream(destPath, FileMode.Create, FileAccess.Write)) - { - fs.Write(file, 0, file.Length); - } - } - /// /// Increment number of the same records in database /// @@ -253,59 +174,6 @@ public virtual void Increment() _count++; } - /// - /// Check if all necessary files declared on record exists - /// - /// True if record is valid - internal bool Valid() - { - return File.Exists(DiagnosticDataPath); - } - - /// - /// Delete all records from hard drive. - /// - internal void Delete() - { - Delete(DiagnosticDataPath); - Delete(RecordPath); - - //remove database attachments - if (Attachments != null && Attachments.Count != 0) - { - foreach (var attachment in Attachments) - { - if (IsInsideDatabaseDirectory(attachment)) - { - Delete(attachment); - } - } - } - } - - /// - /// Delete single file on database record - /// - /// path to file - private void Delete(string path) - { - try - { - if (File.Exists(path)) - { - File.Delete(path); - } - } - catch (IOException e) - { - Debug.Log(string.Format("File {0} is in use. Message: {1}", path, e.Message)); - } - catch (Exception e) - { - Debug.Log(string.Format("Cannot delete file: {0}. Message: {1}", path, e.Message)); - } - } - /// /// Read single record from file /// @@ -328,19 +196,6 @@ internal static BacktraceDatabaseRecord ReadFromFile(FileInfo file) } } - /// - /// Validate if attachment is placed in Backtrace database. - /// - /// Path to attachment - /// True if attachment is in backtrace-database directory. Otherwise false. - private bool IsInsideDatabaseDirectory(string path) - { - if (string.IsNullOrEmpty(path) || !File.Exists(path)) - { - return false; - } - return Path.GetDirectoryName(path) == _path; - } public virtual void Unlock() { Locked = false; diff --git a/Runtime/Services/BacktraceDatabaseContext.cs b/Runtime/Services/BacktraceDatabaseContext.cs index 87f7272b..8ee59b15 100644 --- a/Runtime/Services/BacktraceDatabaseContext.cs +++ b/Runtime/Services/BacktraceDatabaseContext.cs @@ -1,11 +1,9 @@ -using Backtrace.Unity.Common; -using Backtrace.Unity.Interfaces; +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; [assembly: System.Runtime.CompilerServices.InternalsVisibleTo("Backtrace.Unity.Tests.Runtime")] @@ -19,7 +17,7 @@ internal class BacktraceDatabaseContext : IBacktraceDatabaseContext /// /// Database cache /// - public Dictionary> BatchRetry = new Dictionary>(); + internal IDictionary> BatchRetry { get; private set; } = new Dictionary>(); /// /// Total database size on hard drive @@ -31,11 +29,6 @@ internal class BacktraceDatabaseContext : IBacktraceDatabaseContext /// internal int TotalRecords = 0; - /// - /// Path to database directory - /// - private readonly string _path; - /// /// Maximum number of retries /// @@ -51,24 +44,18 @@ internal class BacktraceDatabaseContext : IBacktraceDatabaseContext /// public DeduplicationStrategy DeduplicationStrategy { get; set; } - private readonly BacktraceDatabaseAttachmentManager _attachmentManager; - - /// /// Initialize new instance of Backtrace Database Context /// /// Database settings public BacktraceDatabaseContext(BacktraceDatabaseSettings settings) { - _path = settings.DatabasePath; _retryNumber = checked((int)settings.RetryLimit); - _attachmentManager = new BacktraceDatabaseAttachmentManager(settings); RetryOrder = settings.RetryOrder; DeduplicationStrategy = settings.DeduplicationStrategy; SetupBatch(); } - /// /// Setup cache /// @@ -89,7 +76,7 @@ private void SetupBatch() /// /// Diagnostic data /// hash for current backtrace data - private string GetHash(BacktraceData backtraceData) + public string GetHash(BacktraceData backtraceData) { var fingerprint = backtraceData == null ? string.Empty : backtraceData.Report.Fingerprint ?? string.Empty; if (!string.IsNullOrEmpty(fingerprint)) @@ -106,46 +93,11 @@ private string GetHash(BacktraceData backtraceData) } /// - /// Add new record to database + /// Returns record by record's hash /// - /// Diagnostic data that should be stored in database - /// New instance of DatabaseRecordy - public BacktraceDatabaseRecord Add(BacktraceData backtraceData) - { - if (backtraceData == null) - { - throw new NullReferenceException("backtraceData"); - } - - string hash = GetHash(backtraceData); - if (!string.IsNullOrEmpty(hash)) - { - var existingRecord = GetRecordByHash(hash); - if (existingRecord != null) - { - existingRecord.Locked = true; - existingRecord.Increment(); - TotalRecords++; - return existingRecord; - } - } - //add built-in attachments - var attachments = _attachmentManager.GetReportAttachments(backtraceData); - for (int attachmentIndex = 0; attachmentIndex < attachments.Count(); attachmentIndex++) - { - if (!string.IsNullOrEmpty(attachments.ElementAt(attachmentIndex))) - { - backtraceData.Report.AttachmentPaths.Add(attachments.ElementAt(attachmentIndex)); - backtraceData.Attachments.Add(attachments.ElementAt(attachmentIndex)); - } - } - - var record = ConvertToRecord(backtraceData, hash); - //add record to database context - return Add(record); - } - - private BacktraceDatabaseRecord GetRecordByHash(string hash) + /// Hash associated to the record + /// Database record, if record with associated hash exists. + public BacktraceDatabaseRecord GetRecordByHash(string hash) { for (int batchIndex = 0; batchIndex < BatchRetry.Count; batchIndex++) { @@ -153,30 +105,15 @@ private BacktraceDatabaseRecord GetRecordByHash(string hash) { if (BatchRetry[batchIndex][recordIndex].Hash == hash) { - return BatchRetry[batchIndex][recordIndex]; + var result = BatchRetry[batchIndex][recordIndex]; + result.Locked = true; + return result; } } } return null; } - /// - /// 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 /// @@ -234,8 +171,6 @@ public void Delete(BacktraceDatabaseRecord record) var value = BatchRetry[key].ElementAt(batchIndex); if (value.Id == record.Id) { - //delete value from hard drive - value.Delete(); //delete value from current batch BatchRetry[key].Remove(value); //decrement all records @@ -264,21 +199,6 @@ public void IncrementBatchRetry() IncrementBatches(); } - /// - /// Remove last record in database. - /// - /// If algorithm can remove last record, method return true. Otherwise false - public bool RemoveLastRecord() - { - var record = LastOrDefault(); - if (record != null) - { - Delete(record); - return true; - } - return false; - } - /// /// Increment each batch /// @@ -302,19 +222,15 @@ private void RemoveMaxRetries() for (int i = 0; i < total; i++) { var value = currentBatch[i]; - if (value.Valid()) + if (value.Count > 0) { - value.Delete(); - if (value.Count > 0) - { - TotalRecords = TotalRecords - value.Count; - } - else - { - TotalRecords--; - } - TotalSize -= value.Size; + TotalRecords = TotalRecords - value.Count; + } + else + { + TotalRecords--; } + TotalSize -= value.Size; } } @@ -359,10 +275,6 @@ public void Dispose() public void Clear() { var records = BatchRetry.SelectMany(n => n.Value); - foreach (var record in records) - { - record.Delete(); - } TotalRecords = 0; TotalSize = 0; //clear all existing batches @@ -468,34 +380,15 @@ public int GetTotalNumberOfRecords() 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) + public IEnumerable GetRecordsToDelete() { - 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, string.Format("{0}-dump.dmp", backtraceReport.Uuid.ToString())); - MinidumpException minidumpExceptionType = backtraceReport.ExceptionTypeReport - ? MinidumpException.Present - : MinidumpException.None; - - bool minidumpSaved = MinidumpHelper.Write( - filePath: minidumpDestinationPath, - options: miniDumpType, - exceptionType: minidumpExceptionType); + return BatchRetry[_retryNumber - 1]; + } - return minidumpSaved - ? minidumpDestinationPath - : string.Empty; + public void AddDuplicate(BacktraceDatabaseRecord record) + { + record.Increment(); + TotalRecords++; } } } diff --git a/Runtime/Services/BacktraceDatabaseFileContext.cs b/Runtime/Services/BacktraceDatabaseFileContext.cs index 1958e22c..832d769c 100644 --- a/Runtime/Services/BacktraceDatabaseFileContext.cs +++ b/Runtime/Services/BacktraceDatabaseFileContext.cs @@ -1,10 +1,12 @@ using Backtrace.Unity.Interfaces; +using Backtrace.Unity.Model; using Backtrace.Unity.Model.Breadcrumbs; using Backtrace.Unity.Model.Database; using System; using System.Collections.Generic; using System.IO; using System.Linq; +using System.Text; using System.Text.RegularExpressions; using UnityEngine; @@ -36,14 +38,27 @@ internal class BacktraceDatabaseFileContext : IBacktraceDatabaseFileContext /// Regex for filter physical database records /// private const string RecordFilterRegex = "*-record.json"; + + /// + /// Attachment manager + /// + private readonly BacktraceDatabaseAttachmentManager _attachmentManager; + + /// + /// Path to database directory + /// + private readonly string _path; + /// /// Initialize new BacktraceDatabaseFileContext instance /// - public BacktraceDatabaseFileContext(string databasePath, long maxDatabaseSize, uint maxRecordNumber) + public BacktraceDatabaseFileContext(BacktraceDatabaseSettings settings) { - _maxDatabaseSize = maxDatabaseSize; - _maxRecordNumber = maxRecordNumber; - _databaseDirectoryInfo = new DirectoryInfo(databasePath); + _attachmentManager = new BacktraceDatabaseAttachmentManager(settings); + _maxDatabaseSize = settings.MaxDatabaseSize; + _maxRecordNumber = settings.MaxRecordCount; + _path = settings.DatabasePath; + _databaseDirectoryInfo = new DirectoryInfo(_path); } /// @@ -163,5 +178,147 @@ public void Clear() } } + /// + /// Deletes backtrace database record from persistent data storage + /// + /// Database record + public void Delete(BacktraceDatabaseRecord record) + { + // rmeove json objects + Delete(record.DiagnosticDataPath); + Delete(record.RecordPath); + //remove database attachments + if (record.Attachments == null || record.Attachments.Count == 0) + { + return; + } + // remove associated attachments + foreach (var attachment in record.Attachments) + { + Delete(attachment); + } + } + + /// + /// Validate if attachment is placed in Backtrace database. + /// + /// Path to attachment + /// True if attachment is in backtrace-database directory. Otherwise false. + private bool IsDatabaseDependency(string path) + { + if (string.IsNullOrEmpty(path) || !File.Exists(path)) + { + return false; + } + return Path.GetDirectoryName(path) == _path && !path.EndsWith(BacktraceStorageLogManager.BreadcrumbLogFileName); + } + + /// + /// Deletes files that are generated by the BacktraceDatabase object + /// + /// Path to file + private void Delete(string path) + { + try + { + if (IsDatabaseDependency(path)) + { + File.Delete(path); + } + } + catch (IOException e) + { + Debug.Log(string.Format("File {0} is in use. Message: {1}", path, e.Message)); + } + catch (Exception e) + { + Debug.Log(string.Format("Cannot delete file: {0}. Message: {1}", path, e.Message)); + } + } + + /// + /// Returns list of attachments generated for current diagnostic data + /// + /// Diagnostic data + /// + public IEnumerable GenerateRecordAttachments(BacktraceData data) + { + return _attachmentManager.GetReportAttachments(data); + } + + /// + /// Saves BacktraceDatabaseRerord on the hard drive + /// + /// BacktraceDatabaseRecord + /// true if file context was able to save data on the hard drive. Otherwise false + public bool Save(BacktraceDatabaseRecord record) + { + try + { + var jsonPrefix = record.BacktraceData.UuidString; + var diagnosticJson = record.BacktraceData.ToJson(); + record.DiagnosticDataPath = Path.Combine(_path, string.Format("{0}-attachment.json", jsonPrefix)); + record.Size += Save(diagnosticJson, record.DiagnosticDataPath); + + // update record size based on the attachment information + if (record.Attachments != null && record.Attachments.Count != 0) + { + foreach (var attachment in record.Attachments) + { + if (IsDatabaseDependency(attachment)) + { + record.Size += new FileInfo(attachment).Length; + } + } + } + record.RecordPath = Path.Combine(_path, string.Format("{0}-record.json", jsonPrefix)); + var recordJson = record.ToJson(); + record.Size += UTF8Encoding.Unicode.GetByteCount(recordJson); + Save(recordJson, record.RecordPath); + return true; + } + catch (Exception e) + { + Debug.LogWarning($"Backtrace: Cannot save record on the hard drive. Reason: {e.Message}"); + Delete(record); + + return false; + } + } + + /// + /// Save single file from database record + /// + /// single file (json/dmp) + /// file path + /// Saved file size + private int Save(string json, string destPath) + { + if (string.IsNullOrEmpty(json)) + { + return 0; + } + byte[] file = Encoding.UTF8.GetBytes(json); + + using (var fs = new FileStream(destPath, FileMode.Create, FileAccess.Write)) + { + fs.Write(file, 0, file.Length); + } + return file.Length; + } + + /// + /// Determine if BacktraceDatabaseRecord is valid. + /// + /// Database record + /// True, if the record exists. Otherwise false. + public bool IsValidRecord(BacktraceDatabaseRecord record) + { + if (record == null) + { + return false; + } + return File.Exists(record.DiagnosticDataPath); + } } } diff --git a/Tests/Runtime/Mocks/BacktraceDatabaseContextMock.cs b/Tests/Runtime/Mocks/BacktraceDatabaseContextMock.cs index 34f70a93..5d0a356e 100644 --- a/Tests/Runtime/Mocks/BacktraceDatabaseContextMock.cs +++ b/Tests/Runtime/Mocks/BacktraceDatabaseContextMock.cs @@ -11,14 +11,5 @@ public BacktraceDatabaseContextMock(BacktraceDatabaseSettings settings) : base(s { _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 - }; - } } } \ No newline at end of file From 3aad6c6c967289498ca488af0054a9100d00185a Mon Sep 17 00:00:00 2001 From: kdysput Date: Wed, 5 May 2021 23:38:41 +0200 Subject: [PATCH 04/42] Adjusted database code --- Runtime/BacktraceDatabase.cs | 33 ++++++---- .../Mocks/BacktraceDatabaseFileContextMock.cs | 64 +++++++++++++++++++ .../BacktraceDatabaseFileContextMock.cs.meta | 11 ++++ 3 files changed, 95 insertions(+), 13 deletions(-) create mode 100644 Tests/Runtime/Database/Mocks/BacktraceDatabaseFileContextMock.cs create mode 100644 Tests/Runtime/Database/Mocks/BacktraceDatabaseFileContextMock.cs.meta diff --git a/Runtime/BacktraceDatabase.cs b/Runtime/BacktraceDatabase.cs index f76285ba..7d4c7c1a 100644 --- a/Runtime/BacktraceDatabase.cs +++ b/Runtime/BacktraceDatabase.cs @@ -177,7 +177,7 @@ private void Awake() /// /// Backtrace database update event /// - private void Update() + internal void Update() { if (!Enable) { @@ -593,29 +593,26 @@ private bool ValidateDatabaseSize() //check how many records are stored in database //remove in case when we want to store one more than expected number //If record count == 0 then we ignore this condition - var noMoreSpaceForReport = BacktraceDatabaseContext.Count() + 1 > DatabaseSettings.MaxRecordCount && DatabaseSettings.MaxRecordCount != 0; - if (noMoreSpaceForReport) - { - return false; - } + var noMoreSpaceForReport = ReachedMaximumNumberOfRecords(); //check database size. If database size == 0 then we ignore this condition //remove all records till database use enough space - if (DatabaseSettings.MaxDatabaseSize != 0 && BacktraceDatabaseContext.GetSize() > DatabaseSettings.MaxDatabaseSize) + var noMoreSpace = ReachedDiskSpaceLimit(); + if (noMoreSpaceForReport || noMoreSpace) { //if your database is entry or every record is locked //deletePolicyRetry avoid infinity loop int deletePolicyRetry = 5; - while (BacktraceDatabaseContext.GetSize() > DatabaseSettings.MaxDatabaseSize) + while (ReachedDiskSpaceLimit() || ReachedMaximumNumberOfRecords()) { var lastRecord = BacktraceDatabaseContext.LastOrDefault(); - if (lastRecord != null) - { - BacktraceDatabaseContext.Delete(lastRecord); - BacktraceDatabaseFileContext.Delete(lastRecord); + if (lastRecord != null) + { + BacktraceDatabaseContext.Delete(lastRecord); + BacktraceDatabaseFileContext.Delete(lastRecord); } deletePolicyRetry--; - if (deletePolicyRetry != 0) + if (deletePolicyRetry == 0) { break; } @@ -625,6 +622,16 @@ private bool ValidateDatabaseSize() return true; } + private bool ReachedDiskSpaceLimit() + { + return DatabaseSettings.MaxDatabaseSize != 0 && BacktraceDatabaseContext.GetSize() > DatabaseSettings.MaxDatabaseSize; + } + + private bool ReachedMaximumNumberOfRecords() + { + return BacktraceDatabaseContext.Count() + 1 > DatabaseSettings.MaxRecordCount && DatabaseSettings.MaxRecordCount != 0; + } + /// /// Valid database consistency requirements /// diff --git a/Tests/Runtime/Database/Mocks/BacktraceDatabaseFileContextMock.cs b/Tests/Runtime/Database/Mocks/BacktraceDatabaseFileContextMock.cs new file mode 100644 index 00000000..8736f742 --- /dev/null +++ b/Tests/Runtime/Database/Mocks/BacktraceDatabaseFileContextMock.cs @@ -0,0 +1,64 @@ +using Backtrace.Unity.Interfaces; +using Backtrace.Unity.Model; +using Backtrace.Unity.Model.Database; +using System; +using System.Collections.Generic; +using System.IO; + +namespace Backtrace.Unity.Tests.Runtime +{ + internal class BacktraceDatabaseFileContextMock : IBacktraceDatabaseFileContext + { + public Func> OnFileAttachments { get; set; } + public Action OnDelete { get; set; } + public Func OnValidRecord { get; set; } + public Func OnSave { get; set; } + public void Clear() + { + throw new System.NotImplementedException(); + } + + public void Delete(BacktraceDatabaseRecord record) + { + OnDelete?.Invoke(record); + return; + } + + public IEnumerable GenerateRecordAttachments(BacktraceData data) + { + return OnFileAttachments == null + ? new List() + : OnFileAttachments.Invoke(data); + } + + public IEnumerable GetAll() + { + return new List(); + } + + public IEnumerable GetRecords() + { + return new List(); + } + + public bool IsValidRecord(BacktraceDatabaseRecord record) + { + return OnValidRecord?.Invoke(record) ?? true; + } + + public void RemoveOrphaned(IEnumerable existingRecords) + { + return; + } + + public bool Save(BacktraceDatabaseRecord record) + { + return OnSave?.Invoke(record) ?? true; + } + + public bool ValidFileConsistency() + { + return true; + } + } +} diff --git a/Tests/Runtime/Database/Mocks/BacktraceDatabaseFileContextMock.cs.meta b/Tests/Runtime/Database/Mocks/BacktraceDatabaseFileContextMock.cs.meta new file mode 100644 index 00000000..7c1bafca --- /dev/null +++ b/Tests/Runtime/Database/Mocks/BacktraceDatabaseFileContextMock.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 2c290b8b2b24533409e891f657cbf3d2 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: From 487472ccd1d2c55ca172cf07282eac45572dd2af Mon Sep 17 00:00:00 2001 From: kdysput Date: Wed, 5 May 2021 23:49:34 +0200 Subject: [PATCH 05/42] Removed try/catch block from function that handles exception and prevent from sending nullable records --- Runtime/BacktraceDatabase.cs | 17 +++++++---------- Tests/Runtime/Database/Mocks.meta | 8 ++++++++ 2 files changed, 15 insertions(+), 10 deletions(-) create mode 100644 Tests/Runtime/Database/Mocks.meta diff --git a/Runtime/BacktraceDatabase.cs b/Runtime/BacktraceDatabase.cs index 7d4c7c1a..7d1ba13d 100644 --- a/Runtime/BacktraceDatabase.cs +++ b/Runtime/BacktraceDatabase.cs @@ -435,6 +435,10 @@ record = BacktraceDatabaseContext.FirstOrDefault(); private void SendData(BacktraceDatabaseRecord record) { + if (record == null) + { + return; + } var stopWatch = Configuration.PerformanceStatistics ? System.Diagnostics.Stopwatch.StartNew() : new System.Diagnostics.Stopwatch(); @@ -565,16 +569,9 @@ protected virtual void LoadReports() continue; } if (!BacktraceDatabaseFileContext.IsValidRecord(record)) - { - try - { - Debug.Log("Removing record from Backtrace Database path - invalid record."); - BacktraceDatabaseFileContext.Delete(record); - } - catch (Exception) - { - Debug.LogWarning(string.Format("Cannot remove file from database. File name: {0}", file.FullName)); - } + { + Debug.Log("Removing record from Backtrace Database path - invalid record."); + BacktraceDatabaseFileContext.Delete(record); continue; } BacktraceDatabaseContext.Add(record); diff --git a/Tests/Runtime/Database/Mocks.meta b/Tests/Runtime/Database/Mocks.meta new file mode 100644 index 00000000..44af9a62 --- /dev/null +++ b/Tests/Runtime/Database/Mocks.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: f0166034f6c705945ad1b6ef44ec3303 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: From c1b629a21c68d994d4ef3e9555c85b8e763ee33f Mon Sep 17 00:00:00 2001 From: kdysput Date: Wed, 5 May 2021 23:53:01 +0200 Subject: [PATCH 06/42] Allow nullable record in SendData method --- Runtime/BacktraceDatabase.cs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/Runtime/BacktraceDatabase.cs b/Runtime/BacktraceDatabase.cs index 7d1ba13d..1174cf7d 100644 --- a/Runtime/BacktraceDatabase.cs +++ b/Runtime/BacktraceDatabase.cs @@ -215,11 +215,8 @@ private void Start() RemoveOrphaned(); if (DatabaseSettings.AutoSendMode) { - _lastConnection = Time.unscaledTime; - if (BacktraceDatabaseContext.Any()) - { - SendData(BacktraceDatabaseContext.FirstOrDefault()); - } + _lastConnection = Time.unscaledTime; + SendData(BacktraceDatabaseContext.FirstOrDefault()); } } From 108c76dcea15c0f65b3473c6b49b0558070c7cdb Mon Sep 17 00:00:00 2001 From: kdysput Date: Thu, 6 May 2021 00:14:12 +0200 Subject: [PATCH 07/42] Squashed commit of the following: commit 8c2cec90811c597b207b64e285afad305e986392 Author: kdysput Date: Thu May 6 00:00:10 2021 +0200 Adjusted nullable JSON values commit 2ed2d7fe043ebe805481853c13d4eac7d117a6ca Author: kdysput Date: Wed May 5 23:55:47 2021 +0200 Fixed invalid property name after merge --- Editor/BacktraceConfigurationEditor.cs | 6 +++++- Editor/BacktraceConfigurationLabels.cs | 6 ++++-- Tests/Runtime/BacktraceJObjectTests.cs | 10 +++++----- Tests/Runtime/Serialization/SerializationTests.cs | 2 +- 4 files changed, 15 insertions(+), 9 deletions(-) diff --git a/Editor/BacktraceConfigurationEditor.cs b/Editor/BacktraceConfigurationEditor.cs index 8074373f..1ad0468b 100644 --- a/Editor/BacktraceConfigurationEditor.cs +++ b/Editor/BacktraceConfigurationEditor.cs @@ -114,8 +114,12 @@ public override void OnInspectorGUI() } EditorGUILayout.PropertyField( - serializedObject.FindProperty("TimeIntervalInMs"), + serializedObject.FindProperty("TimeIntervalInMin"), new GUIContent(BacktraceConfigurationLabels.LABEL_EVENT_AGGREGATION_TIME_INTERVAL)); + + EditorGUILayout.PropertyField( + serializedObject.FindProperty("MaximumNumberOfEvents"), + new GUIContent(BacktraceConfigurationLabels.LABEL_EVENT_AGGREGATION_MAXIMUM_NUMBER_OF_EVENTS)); } } diff --git a/Editor/BacktraceConfigurationLabels.cs b/Editor/BacktraceConfigurationLabels.cs index 3fb83767..32575891 100644 --- a/Editor/BacktraceConfigurationLabels.cs +++ b/Editor/BacktraceConfigurationLabels.cs @@ -22,6 +22,7 @@ internal static class BacktraceConfigurationLabels internal const string LABEL_ENABLE_EVENT_AGGREGATION = "Enable default crash free events"; internal const string LABEL_EVENT_AGGREGATION_URL = "Event aggregation submission URL"; internal const string LABEL_EVENT_AGGREGATION_TIME_INTERVAL = "Event aggregation time interval in ms"; + internal const string LABEL_EVENT_AGGREGATION_MAXIMUM_NUMBER_OF_EVENTS = "Maximum number of events in the event aggregation storage"; internal const string LABEL_BREADCRUMBS_SECTION = "Breadcrumbs support"; internal const string LABEL_ENABLE_BREADCRUMBS = "Enable breadcrumbs support"; @@ -52,7 +53,8 @@ internal static class BacktraceConfigurationLabels internal static string LABEL_MAX_DATABASE_SIZE = "Maximum database size (mb)"; internal static string LABEL_RETRY_INTERVAL = "Retry interval"; internal static string LABEL_RETRY_LIMIT = "Maximum retries"; - internal static string LABEL_RETRY_ORDER = "Retry order (FIFO/LIFO)"; - + internal static string LABEL_RETRY_ORDER = "Retry order (FIFO/LIFO)"; + + } } diff --git a/Tests/Runtime/BacktraceJObjectTests.cs b/Tests/Runtime/BacktraceJObjectTests.cs index 1e6b6205..06ebc79e 100644 --- a/Tests/Runtime/BacktraceJObjectTests.cs +++ b/Tests/Runtime/BacktraceJObjectTests.cs @@ -289,8 +289,8 @@ public IEnumerator TestDataSerialization_ShouldSerializeEmptyOrNullableValues_Da var json = jObject.ToJson(); var expectedResult = "{" + - "\"bar\":null," + - "\"foo\":null" + + "\"bar\":\"\"," + + "\"foo\":\"\"" + "}"; Assert.AreEqual(expectedResult, json); yield return null; @@ -307,9 +307,9 @@ public IEnumerator TestDataSerialization_ShouldEscapeCorrectlyAllKeys_DataSerial var json = jObject.ToJson(); var expectedResult = "{" + - "\"foo\\\"\":null," + - "\"\\\\bar\":null," + - "\"b\\naz\":null" + + "\"foo\\\"\":\"\"," + + "\"\\\\bar\":\"\"," + + "\"b\\naz\":\"\"" + "}"; Assert.AreEqual(expectedResult, json); yield return null; diff --git a/Tests/Runtime/Serialization/SerializationTests.cs b/Tests/Runtime/Serialization/SerializationTests.cs index 48a5bb8b..a5e28f14 100644 --- a/Tests/Runtime/Serialization/SerializationTests.cs +++ b/Tests/Runtime/Serialization/SerializationTests.cs @@ -44,7 +44,7 @@ public IEnumerator TestDataSerialization_ReportWithCustomAttribtues_ShouldGenera var json = data.ToJson(); foreach (var keyValuePair in attributes) { - var value = string.Format("\"{0}\":{1}", keyValuePair.Key, string.IsNullOrEmpty(keyValuePair.Value) ? "null" : string.Format("\"{0}\"", keyValuePair.Value)); + var value = string.Format("\"{0}\":{1}", keyValuePair.Key, string.IsNullOrEmpty(keyValuePair.Value) ? "\"\"" : string.Format("\"{0}\"", keyValuePair.Value)); Assert.IsTrue(json.Contains(value)); } From a550120dc010030a586f8f2de939d85b36d26cd2 Mon Sep 17 00:00:00 2001 From: kdysput Date: Thu, 6 May 2021 00:19:21 +0200 Subject: [PATCH 08/42] Undo session aggregation changes --- Runtime/BacktraceClient.cs | 2 +- Runtime/Model/BacktraceConfiguration.cs | 51 +++++++++++++++---------- 2 files changed, 32 insertions(+), 21 deletions(-) diff --git a/Runtime/BacktraceClient.cs b/Runtime/BacktraceClient.cs index 957a7178..be8cf299 100644 --- a/Runtime/BacktraceClient.cs +++ b/Runtime/BacktraceClient.cs @@ -472,7 +472,7 @@ public void Refresh() } if (Configuration.EnableEventAggregationSupport && !string.IsNullOrEmpty(Configuration.EventAggregationSubmissionUrl)) { - //EnableSessionAgregationSupport(Configuration.EventAggregationSubmissionUrl, Configuration.GetEventAggregationIntervalTimerInMs(), Configuration.MaximumNumberOfEvents); + EnableSessionAgregationSupport(Configuration.EventAggregationSubmissionUrl, Configuration.GetEventAggregationIntervalTimerInMs(), Configuration.MaximumNumberOfEvents); } } diff --git a/Runtime/Model/BacktraceConfiguration.cs b/Runtime/Model/BacktraceConfiguration.cs index c4e08798..215d464b 100644 --- a/Runtime/Model/BacktraceConfiguration.cs +++ b/Runtime/Model/BacktraceConfiguration.cs @@ -193,26 +193,32 @@ public class BacktraceConfiguration : ScriptableObject /// Directory path where reports and minidumps are stored /// [Tooltip("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.")] - public string DatabasePath; - - /// - /// Enable event aggregation support - /// - [Header("Backtrace event aggregation")] - [Tooltip("Enable event aggregation support")] - public bool EnableEventAggregationSupport = false; - - /// - /// Event aggregation submission url - /// - [Tooltip("Event aggregation submission url")] - public string EventAggregationSubmissionUrl; - - /// - /// Time interval in ms - /// - [Tooltip("Event aggregation submission url")] - public long TimeIntervalInMs = 0; + public string DatabasePath; + + /// + /// Enable event aggregation support + /// + [Tooltip("Enable default crash free events")] + public bool EnableEventAggregationSupport = false; + + /// + /// Event aggregation submission url + /// + [Tooltip("Event aggregation submission url")] + public string EventAggregationSubmissionUrl; + + /// + /// Time interval in ms + /// + [Range(0, 60)] + [Tooltip("Event aggregation time interval in min")] + public long TimeIntervalInMin = 30; + + /// + /// Maximum number of events in Event aggregation store + /// + [Tooltip("Maximum number of events stored by Backtrace")] + public uint MaximumNumberOfEvents = 10; /// /// Determine if database is enable @@ -343,6 +349,11 @@ public static bool ValidateServerUrl(string value) public bool IsValid() { return ValidateServerUrl(ServerUrl); + } + + public long GetEventAggregationIntervalTimerInMs() + { + return TimeIntervalInMin * 60; } From fb401428e1b5658548a8b3d16456d08a49666ebc Mon Sep 17 00:00:00 2001 From: kdysput Date: Thu, 6 May 2021 17:17:46 +0200 Subject: [PATCH 09/42] Create attribute provider dynamically --- Runtime/BacktraceClient.cs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/Runtime/BacktraceClient.cs b/Runtime/BacktraceClient.cs index be8cf299..d9a29c41 100644 --- a/Runtime/BacktraceClient.cs +++ b/Runtime/BacktraceClient.cs @@ -22,7 +22,7 @@ namespace Backtrace.Unity /// public class BacktraceClient : MonoBehaviour, IBacktraceClient { - public const string VERSION = "3.4.0"; + public const string VERSION = "3.5.0"; public BacktraceConfiguration Configuration; @@ -63,10 +63,18 @@ public string this[string index] { get { + if (_attributeProvider == null) + { + _attributeProvider = new AttributeProvider(); + } return _attributeProvider[index]; } set { + if (_attributeProvider == null) + { + _attributeProvider = new AttributeProvider(); + } _attributeProvider[index] = value; if (_nativeClient != null) { @@ -429,7 +437,6 @@ public void Refresh() Enabled = true; _current = Thread.CurrentThread; CaptureUnityMessages(); - _attributeProvider = new AttributeProvider(); _reportLimitWatcher = new ReportLimitWatcher(Convert.ToUInt32(Configuration.ReportPerMin)); _clientReportAttachments = Configuration.GetAttachmentPaths(); From e9affc08ab515196f98c5a93cd28481d84d22b1c Mon Sep 17 00:00:00 2001 From: kdysput Date: Sat, 8 May 2021 18:34:17 +0200 Subject: [PATCH 10/42] Undo maximum number of events --- Editor/BacktraceConfigurationEditor.cs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/Editor/BacktraceConfigurationEditor.cs b/Editor/BacktraceConfigurationEditor.cs index f2eb722a..ef20383a 100644 --- a/Editor/BacktraceConfigurationEditor.cs +++ b/Editor/BacktraceConfigurationEditor.cs @@ -106,10 +106,6 @@ public override void OnInspectorGUI() EditorGUILayout.PropertyField( serializedObject.FindProperty("TimeIntervalInMin"), new GUIContent(BacktraceConfigurationLabels.LABEL_EVENT_AGGREGATION_TIME_INTERVAL)); - - EditorGUILayout.PropertyField( - serializedObject.FindProperty("MaximumNumberOfEvents"), - new GUIContent(BacktraceConfigurationLabels.LABEL_EVENT_AGGREGATION_MAXIMUM_NUMBER_OF_EVENTS)); } } From 16acd9c8c816ce9e360d2ba8b75fc52ab9a9c1b3 Mon Sep 17 00:00:00 2001 From: kdysput Date: Sat, 8 May 2021 18:36:29 +0200 Subject: [PATCH 11/42] Undo attribute provider management --- Runtime/BacktraceClient.cs | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/Runtime/BacktraceClient.cs b/Runtime/BacktraceClient.cs index 683cb13d..9eca3f60 100644 --- a/Runtime/BacktraceClient.cs +++ b/Runtime/BacktraceClient.cs @@ -99,19 +99,11 @@ public string this[string index] { get { - if (_attributeProvider == null) - { - _attributeProvider = new AttributeProvider(); - } - return _attributeProvider[index]; + return AttributeProvider[index]; } set { - if (_attributeProvider == null) - { - _attributeProvider = new AttributeProvider(); - } - _attributeProvider[index] = value; + AttributeProvider[index] = value; if (_nativeClient != null) { _nativeClient.SetAttribute(index, value); From fcbbd95f1d5a7662cb41e09a4513fd123582b8f9 Mon Sep 17 00:00:00 2001 From: kdysput Date: Sat, 8 May 2021 21:14:31 +0200 Subject: [PATCH 12/42] Fixed labels --- Editor/BacktraceConfigurationLabels.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Editor/BacktraceConfigurationLabels.cs b/Editor/BacktraceConfigurationLabels.cs index 158b63e6..400f4f9f 100644 --- a/Editor/BacktraceConfigurationLabels.cs +++ b/Editor/BacktraceConfigurationLabels.cs @@ -23,6 +23,11 @@ internal static class BacktraceConfigurationLabels internal const string LABEL_EVENT_AGGREGATION_TIME_INTERVAL = "Auto send interval in sec"; internal const string LABEL_CRASH_FREE_SECTION = "Crash Free Metrics Reporting"; + internal const string LABEL_BREADCRUMBS_SECTION = "Breadcrumbs support"; + internal const string LABEL_ENABLE_BREADCRUMBS = "Enable breadcrumbs support"; + internal const string LABEL_BREADCRUMBS_EVENTS = "Breadcrumbs events type"; + internal const string LABEL_BREADCRUMNS_LOG_LEVEL = "Breadcrumbs log level"; + internal static string LABEL_REPORT_ATTACHMENTS = "Report attachment paths"; internal static string CAPTURE_NATIVE_CRASHES = "Capture native crashes"; internal static string LABEL_REPORT_FILTER = "Filter reports"; From fe410d9113803840ab2d54d1068ba109f5274c69 Mon Sep 17 00:00:00 2001 From: kdysput Date: Sun, 9 May 2021 13:20:55 +0200 Subject: [PATCH 13/42] Expose enableBreadcrumbs API --- Runtime/Interfaces/IBacktraceClient.cs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/Runtime/Interfaces/IBacktraceClient.cs b/Runtime/Interfaces/IBacktraceClient.cs index 3f7fac6f..3bc21e78 100644 --- a/Runtime/Interfaces/IBacktraceClient.cs +++ b/Runtime/Interfaces/IBacktraceClient.cs @@ -1,5 +1,5 @@ using Backtrace.Unity.Model; -using Backtrace.Unity.Model.Breadcrumbs; +using Backtrace.Unity.Model.Breadcrumbs; using System; using System.Collections.Generic; @@ -9,7 +9,7 @@ namespace Backtrace.Unity.Interfaces /// Backtrace client interface. Use this interface with dependency injection features /// public interface IBacktraceClient - { + { /// /// Backtrace Breadcrumbs /// @@ -47,5 +47,11 @@ public interface IBacktraceClient /// Refresh client configuration /// void Refresh(); + + /// + /// Enabled Backtrace database breadcrumbs integration + /// + /// True, if breadcrumbs file was initialized correctly. Otherwise false. + bool EnableBreadcrumbsSupport(); } } \ No newline at end of file From 9b5b50c0d8e6c61c5df840682a3ba12da89d80ad Mon Sep 17 00:00:00 2001 From: kdysput Date: Sun, 9 May 2021 13:44:11 +0200 Subject: [PATCH 14/42] Fixed name --- Runtime/BacktraceClient.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Runtime/BacktraceClient.cs b/Runtime/BacktraceClient.cs index d0e95043..471ae4f4 100644 --- a/Runtime/BacktraceClient.cs +++ b/Runtime/BacktraceClient.cs @@ -525,7 +525,7 @@ public bool EnableBreadcrumbsSupport() return initializationResult; } - public void EnableSessionAgregationSupport(string submissionUrl, long timeIntervalInMs) + public void EnableSessionAggregationSupport() { if (!Configuration.EnableEventAggregationSupport) { From 4b923aedda059ff75d7d85ec82f238ef89663609 Mon Sep 17 00:00:00 2001 From: kdysput Date: Sun, 9 May 2021 15:23:18 +0200 Subject: [PATCH 15/42] breadcrumbs tests --- Runtime/BacktraceDatabase.cs | 1 + .../Model/Breadcrumbs/BacktraceBreadcrumbs.cs | 9 +- .../BacktraceBreadcrumbsEventHandler.cs | 7 +- Runtime/Model/Breadcrumbs/BreadcrumbLevel.cs | 2 +- .../Model/Breadcrumbs/IBacktraceLogManager.cs | 1 - Runtime/Model/Breadcrumbs/InMemory.meta | 8 + .../InMemory/BacktraceInMemoryLogManager.cs | 73 +++ .../BacktraceInMemoryLogManager.cs.meta | 11 + .../InMemory/InMemoryBreadcrumb.cs | 11 + .../InMemory/InMemoryBreadcrumb.cs.meta | 11 + Runtime/Model/Breadcrumbs/Storage.meta | 8 + .../BacktraceStorageLogManager.cs | 572 +++++++++--------- .../BacktraceStorageLogManager.cs.meta | 0 .../Services/BacktraceDatabaseFileContext.cs | 2 +- Tests/Runtime/Breadcrumbs.meta | 8 + .../BacktraceBreadcrumbsTypeTests.cs | 46 ++ .../BacktraceBreadcrumbsTypeTests.cs.meta | 11 + .../Breadcrumbs/BreadcrumbsLogLevelTests.cs | 180 ++++++ .../BreadcrumbsLogLevelTests.cs.meta | 11 + 19 files changed, 678 insertions(+), 294 deletions(-) create mode 100644 Runtime/Model/Breadcrumbs/InMemory.meta create mode 100644 Runtime/Model/Breadcrumbs/InMemory/BacktraceInMemoryLogManager.cs create mode 100644 Runtime/Model/Breadcrumbs/InMemory/BacktraceInMemoryLogManager.cs.meta create mode 100644 Runtime/Model/Breadcrumbs/InMemory/InMemoryBreadcrumb.cs create mode 100644 Runtime/Model/Breadcrumbs/InMemory/InMemoryBreadcrumb.cs.meta create mode 100644 Runtime/Model/Breadcrumbs/Storage.meta rename Runtime/Model/Breadcrumbs/{ => Storage}/BacktraceStorageLogManager.cs (96%) rename Runtime/Model/Breadcrumbs/{ => Storage}/BacktraceStorageLogManager.cs.meta (100%) create mode 100644 Tests/Runtime/Breadcrumbs.meta create mode 100644 Tests/Runtime/Breadcrumbs/BacktraceBreadcrumbsTypeTests.cs create mode 100644 Tests/Runtime/Breadcrumbs/BacktraceBreadcrumbsTypeTests.cs.meta create mode 100644 Tests/Runtime/Breadcrumbs/BreadcrumbsLogLevelTests.cs create mode 100644 Tests/Runtime/Breadcrumbs/BreadcrumbsLogLevelTests.cs.meta diff --git a/Runtime/BacktraceDatabase.cs b/Runtime/BacktraceDatabase.cs index 1174cf7d..d8694c3b 100644 --- a/Runtime/BacktraceDatabase.cs +++ b/Runtime/BacktraceDatabase.cs @@ -2,6 +2,7 @@ using Backtrace.Unity.Interfaces; using Backtrace.Unity.Model; using Backtrace.Unity.Model.Breadcrumbs; +using Backtrace.Unity.Model.Breadcrumbs.Storage; using Backtrace.Unity.Model.Database; using Backtrace.Unity.Services; using Backtrace.Unity.Types; diff --git a/Runtime/Model/Breadcrumbs/BacktraceBreadcrumbs.cs b/Runtime/Model/Breadcrumbs/BacktraceBreadcrumbs.cs index 3d377b60..0d439015 100644 --- a/Runtime/Model/Breadcrumbs/BacktraceBreadcrumbs.cs +++ b/Runtime/Model/Breadcrumbs/BacktraceBreadcrumbs.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using UnityEngine; +[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("Backtrace.Unity.Tests.Runtime")] namespace Backtrace.Unity.Model.Breadcrumbs { internal sealed class BacktraceBreadcrumbs : IBacktraceBreadcrumbs @@ -21,7 +22,7 @@ internal sealed class BacktraceBreadcrumbs : IBacktraceBreadcrumbs /// internal readonly IBacktraceLogManager LogManager; - internal readonly BacktraceBreadcrumbsEventHandler _eventHandler; + internal readonly BacktraceBreadcrumbsEventHandler EventHandler; /// /// Determine if breadcrumbs are enabled @@ -30,11 +31,11 @@ internal sealed class BacktraceBreadcrumbs : IBacktraceBreadcrumbs public BacktraceBreadcrumbs(IBacktraceLogManager logManager) { LogManager = logManager; - _eventHandler = new BacktraceBreadcrumbsEventHandler(this); + EventHandler = new BacktraceBreadcrumbsEventHandler(this); } public void UnregisterEvents() { - _eventHandler.Unregister(); + EventHandler.Unregister(); } public bool ClearBreadcrumbs() @@ -72,7 +73,7 @@ public bool EnableBreadcrumbs(BacktraceBreadcrumbType level, UnityEngineLogLevel { return false; } - _eventHandler.Register(level); + EventHandler.Register(level); return true; } diff --git a/Runtime/Model/Breadcrumbs/BacktraceBreadcrumbsEventHandler.cs b/Runtime/Model/Breadcrumbs/BacktraceBreadcrumbsEventHandler.cs index 87c1bb3f..63cb60c2 100644 --- a/Runtime/Model/Breadcrumbs/BacktraceBreadcrumbsEventHandler.cs +++ b/Runtime/Model/Breadcrumbs/BacktraceBreadcrumbsEventHandler.cs @@ -7,6 +7,7 @@ namespace Backtrace.Unity.Model.Breadcrumbs { internal sealed class BacktraceBreadcrumbsEventHandler { + public bool HasRegisteredEvents { get; set; } = false; private readonly BacktraceBreadcrumbs _breadcrumbs; private BacktraceBreadcrumbType _registeredLevel; private Thread _thread; @@ -22,8 +23,10 @@ public BacktraceBreadcrumbsEventHandler(BacktraceBreadcrumbs breadcrumbs) public void Register(BacktraceBreadcrumbType level) { _registeredLevel = level; - if (!level.HasFlag(BacktraceBreadcrumbType.System)) + HasRegisteredEvents = level.HasFlag(BacktraceBreadcrumbType.System); + if (!HasRegisteredEvents) { + return; } SceneManager.activeSceneChanged += HandleSceneChanged; @@ -41,7 +44,7 @@ public void Register(BacktraceBreadcrumbType level) /// public void Unregister() { - if (!_registeredLevel.HasFlag(BacktraceBreadcrumbType.System)) + if (HasRegisteredEvents) { return; } diff --git a/Runtime/Model/Breadcrumbs/BreadcrumbLevel.cs b/Runtime/Model/Breadcrumbs/BreadcrumbLevel.cs index 62ae29d9..8bc4759e 100644 --- a/Runtime/Model/Breadcrumbs/BreadcrumbLevel.cs +++ b/Runtime/Model/Breadcrumbs/BreadcrumbLevel.cs @@ -1,6 +1,6 @@ namespace Backtrace.Unity.Model.Breadcrumbs { - internal enum BreadcrumbLevel + public enum BreadcrumbLevel { Manual = 1, Log = 2, diff --git a/Runtime/Model/Breadcrumbs/IBacktraceLogManager.cs b/Runtime/Model/Breadcrumbs/IBacktraceLogManager.cs index 184f81b9..08942c2c 100644 --- a/Runtime/Model/Breadcrumbs/IBacktraceLogManager.cs +++ b/Runtime/Model/Breadcrumbs/IBacktraceLogManager.cs @@ -1,5 +1,4 @@ using System.Collections.Generic; -using UnityEngine; namespace Backtrace.Unity.Model.Breadcrumbs { diff --git a/Runtime/Model/Breadcrumbs/InMemory.meta b/Runtime/Model/Breadcrumbs/InMemory.meta new file mode 100644 index 00000000..92f8777b --- /dev/null +++ b/Runtime/Model/Breadcrumbs/InMemory.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 0d7715c4fadffc940ab042ed2224db84 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Model/Breadcrumbs/InMemory/BacktraceInMemoryLogManager.cs b/Runtime/Model/Breadcrumbs/InMemory/BacktraceInMemoryLogManager.cs new file mode 100644 index 00000000..a680dcd3 --- /dev/null +++ b/Runtime/Model/Breadcrumbs/InMemory/BacktraceInMemoryLogManager.cs @@ -0,0 +1,73 @@ +using System.Collections.Generic; + +[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("Backtrace.Unity.Tests.Runtime")] +namespace Backtrace.Unity.Model.Breadcrumbs.InMemory +{ + internal sealed class BacktraceInMemoryLogManager : IBacktraceLogManager + { + /// + /// Default maximum number of in memory breadcrumbs + /// + public const int DefaultMaximumNumberOfInMemoryBreadcrumbs = 100; + + /// + /// Maximum number of in memory breadcrumbs + /// + public int MaximumNumberOfBreadcrumbs { get; set; } = DefaultMaximumNumberOfInMemoryBreadcrumbs; + + /// + /// Lock object + /// + private object _lockObject = new object(); + + /// + /// Breadcrumbs + /// + internal readonly Queue Breadcrumbs = new Queue(DefaultMaximumNumberOfInMemoryBreadcrumbs); + + /// + /// Returns path to breadcrumb file - which is string.Empty for in memory breadcrumb manager + /// + public string BreadcrumbsFilePath + { + get + { + return string.Empty; + } + } + + public bool Add(string message, BreadcrumbLevel level, UnityEngineLogLevel type, IDictionary attributes) + { + lock (_lockObject) + { + if (Breadcrumbs.Count + 1 < MaximumNumberOfBreadcrumbs) + { + while (Breadcrumbs.Count + 1 > MaximumNumberOfBreadcrumbs) + { + Breadcrumbs.Dequeue(); + } + } + } + Breadcrumbs.Enqueue(new InMemoryBreadcrumb() + { + Message = message, + Level = level, + Type = type, + Attributes = attributes + }); + + return true; + } + + public bool Clear() + { + Breadcrumbs.Clear(); + return true; + } + + public bool Enable() + { + return true; + } + } +} diff --git a/Runtime/Model/Breadcrumbs/InMemory/BacktraceInMemoryLogManager.cs.meta b/Runtime/Model/Breadcrumbs/InMemory/BacktraceInMemoryLogManager.cs.meta new file mode 100644 index 00000000..9f958b43 --- /dev/null +++ b/Runtime/Model/Breadcrumbs/InMemory/BacktraceInMemoryLogManager.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: bb1d4a2985e9d144a84f22e37d8b6103 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Model/Breadcrumbs/InMemory/InMemoryBreadcrumb.cs b/Runtime/Model/Breadcrumbs/InMemory/InMemoryBreadcrumb.cs new file mode 100644 index 00000000..011a90cd --- /dev/null +++ b/Runtime/Model/Breadcrumbs/InMemory/InMemoryBreadcrumb.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; +namespace Backtrace.Unity.Model.Breadcrumbs.InMemory +{ + public class InMemoryBreadcrumb + { + public string Message { get; set; } + public BreadcrumbLevel Level { get; set; } + public UnityEngineLogLevel Type { get; set; } + public IDictionary Attributes { get; set; } + } +} diff --git a/Runtime/Model/Breadcrumbs/InMemory/InMemoryBreadcrumb.cs.meta b/Runtime/Model/Breadcrumbs/InMemory/InMemoryBreadcrumb.cs.meta new file mode 100644 index 00000000..f1832bb7 --- /dev/null +++ b/Runtime/Model/Breadcrumbs/InMemory/InMemoryBreadcrumb.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: f5bb3a9f597f1a84099fbf53a54059b8 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Model/Breadcrumbs/Storage.meta b/Runtime/Model/Breadcrumbs/Storage.meta new file mode 100644 index 00000000..d7b7119f --- /dev/null +++ b/Runtime/Model/Breadcrumbs/Storage.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: a1cb0815d2d4cd44fbd02781986ee8c2 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Model/Breadcrumbs/BacktraceStorageLogManager.cs b/Runtime/Model/Breadcrumbs/Storage/BacktraceStorageLogManager.cs similarity index 96% rename from Runtime/Model/Breadcrumbs/BacktraceStorageLogManager.cs rename to Runtime/Model/Breadcrumbs/Storage/BacktraceStorageLogManager.cs index b45a8dea..0416f9e1 100644 --- a/Runtime/Model/Breadcrumbs/BacktraceStorageLogManager.cs +++ b/Runtime/Model/Breadcrumbs/Storage/BacktraceStorageLogManager.cs @@ -1,285 +1,287 @@ -using Backtrace.Unity.Common; -using Backtrace.Unity.Json; -using System; -using System.Collections.Generic; -using System.Globalization; -using System.IO; - -namespace Backtrace.Unity.Model.Breadcrumbs -{ - internal sealed class BacktraceStorageLogManager : IBacktraceLogManager - { - /// - /// Path to the breadcrumbs file - /// - public string BreadcrumbsFilePath { get; private set; } - - /// - /// Minimum size of the breadcrumbs file (10kB) - /// - public const int MinimumBreadcrumbsFileSize = 10 * 1000; - - /// - /// Breadcrumbs file size. - /// - public long BreadcrumbsSize - { - get - { - return _breadcrumbsSize; - } - set - { - if (value < MinimumBreadcrumbsFileSize) - { - throw new ArgumentException("Breadcrumbs size must be greater or equal to 10kB"); - } - _breadcrumbsSize = value; - } - } - - /// - /// Default breacrumbs size. By default breadcrumbs file size is limitted to 64kB. - /// - private long _breadcrumbsSize = 64000; - - /// - /// Default log file name - /// - internal const string BreadcrumbLogFileName = "bt-breadcrumbs-0"; - - /// - /// default breadcrumb row ending - /// - /// Default breadcrumb document ending - /// - private byte[] _endOfDocument = System.Text.Encoding.UTF8.GetBytes("\n]"); - - /// - /// Default breadcrumb end of the document - /// - private byte[] _startOfDocument = System.Text.Encoding.UTF8.GetBytes("[\n"); - - /// - /// Breadcrumb id - /// - private long _breadcrumbId = 0; - - /// - /// Lock object - /// - private object _lockObject = new object(); - - /// - /// Current breadcurmbs fle size - /// - private long currentSize = 0; - - /// - /// Queue that represents number of bytes in each log stored in the breadcrumb file - /// - private readonly Queue _logSize = new Queue(); - - public BacktraceStorageLogManager(string storagePath) - { - if (string.IsNullOrEmpty(storagePath)) - { - throw new ArgumentException("Breadcrumbs storage path is null or empty"); - } - BreadcrumbsFilePath = Path.Combine(storagePath, BreadcrumbLogFileName); - } - - /// - /// Enables breadcrumbs integration - /// - /// true if breadcrumbs file was created. Otherwise false. - public bool Enable() - { - try - { - if (File.Exists(BreadcrumbsFilePath)) - { - File.Delete(BreadcrumbsFilePath); - } - - using (var _breadcrumbStream = new FileStream(BreadcrumbsFilePath, FileMode.CreateNew, FileAccess.Write)) - { - _breadcrumbStream.Write(_startOfDocument, 0, _startOfDocument.Length); - _breadcrumbStream.Write(_endOfDocument, 0, _endOfDocument.Length); - } - currentSize = _startOfDocument.Length + _endOfDocument.Length; - } - catch (Exception e) - { - System.Diagnostics.Debug.WriteLine(string.Format("Cannot initialize breadcrumbs file. Reason: {0}", e.Message)); - return false; - } - return true; - } - - /// - /// Adds breadcrumb entry to the breadcrumbs file. - /// - /// Breadcrumb message - /// Breadcrumb level - /// Breadcrumb type - /// Breadcrumb attributs - /// True if breadcrumb was stored in the breadcrumbs file. Otherwise false. - public bool Add(string message, BreadcrumbLevel level, UnityEngineLogLevel type, IDictionary attributes) - { - byte[] bytes; - lock (_lockObject) - { - long id = _breadcrumbId++; - var jsonObject = CreateBreadcrumbJson(id, message, level, type, attributes); - bytes = System.Text.Encoding.UTF8.GetBytes(jsonObject.ToJson()); - - if (currentSize + bytes.Length > BreadcrumbsSize) - { - try - { - ClearOldLogs(); - } - catch (Exception) - { - return false; - } - } - } - - try - { - return AppendBreadcrumb(bytes); - } - catch (Exception) - { - return false; - } - } - - /// - /// Convert diagnostic data to JSON format - /// - /// Breadcrumbs id - /// breadcrumbs message - /// Breadcrumb level - /// Breadcrumb type - /// Breadcrumb attributes - /// JSON object - private BacktraceJObject CreateBreadcrumbJson( - long id, - string message, - BreadcrumbLevel level, - UnityEngineLogLevel type, - IDictionary attributes) - { - var jsonObject = new BacktraceJObject(); - // breadcrumbs integration accepts timestamp in ms not in sec. - jsonObject.Add("timestamp", DateTimeHelper.TimestampMs(), "F0"); - jsonObject.Add("id", id); - jsonObject.Add("type", Enum.GetName(typeof(BreadcrumbLevel), level).ToLower()); - jsonObject.Add("level", Enum.GetName(typeof(UnityEngineLogLevel), type).ToLower()); - jsonObject.Add("message", message); - jsonObject.Add("attributes", new BacktraceJObject(attributes)); - return jsonObject; - } - /// - /// Append breadcrumb JSON to the end of the breadcrumbs file. - /// - /// Bytes that represents single JSON object with breadcrumb informatiom - private bool AppendBreadcrumb(byte[] bytes) - { - // size of the breadcrumb - it's negative at the beginning because we're removing 2 bytes on start - long appendingSize = _endOfDocument.Length * -1; - using (var breadcrumbStream = new FileStream(BreadcrumbsFilePath, FileMode.Open, FileAccess.Write)) - { - //back to position before end of the document \n} - breadcrumbStream.Position = breadcrumbStream.Length - _endOfDocument.Length; - - // append ,\n when we're appending new row to existing list of rows. If this is first row - // ignore it - if (_breadcrumbId != 1) - { - breadcrumbStream.Write(_newRow, 0, _newRow.Length); - appendingSize += _newRow.Length; - } - // append breadcrumbs json - breadcrumbStream.Write(bytes, 0, bytes.Length); - // and close JSON document - breadcrumbStream.Write(_endOfDocument, 0, _endOfDocument.Length); - appendingSize += (bytes.Length + _endOfDocument.Length); - } - currentSize += appendingSize; - _logSize.Enqueue(bytes.Length); - return true; - } - - /// - /// Remove last n logs from the breadcrumbs file. When breacrumbs file hit - /// the file size limit, this method will clear up the oldest logs to decrease - /// file size. - /// - private void ClearOldLogs() - { - var startPosition = GetNextStartPosition(); - using (FileStream breadcrumbsStream = new FileStream(BreadcrumbsFilePath, FileMode.Open, FileAccess.ReadWrite)) - { - using (MemoryStream ms = new MemoryStream()) - { - var size = breadcrumbsStream.Length - startPosition; - breadcrumbsStream.Seek(size * -1, SeekOrigin.End); - - breadcrumbsStream.CopyTo(ms); - breadcrumbsStream.SetLength(size + _startOfDocument.Length); - - ms.Position = 0; - breadcrumbsStream.Position = 0; - - breadcrumbsStream.Write(_startOfDocument, 0, _startOfDocument.Length); - ms.CopyTo(breadcrumbsStream); - } - } - // decrease a size of the breadcrumb file after removing n breadcrumbs - currentSize -= startPosition; - currentSize += _startOfDocument.Length; - } - - /// - /// Calculate start position of the file that will be used - /// to recreate breadcrumbs file. Position represents place - /// where starts breadcrumbs that we should keep in the recreated file. - /// - /// Breadcrumb start index - private long GetNextStartPosition() - { - double expectedFreedBytes = BreadcrumbsSize - (BreadcrumbsSize * 0.7); - long numberOfFreeBytes = _startOfDocument.Length; - int nextLineBytes = _newRow.Length; - while (numberOfFreeBytes < expectedFreedBytes) - { - numberOfFreeBytes += (_logSize.Dequeue() + nextLineBytes); - } - - return numberOfFreeBytes; - } - - /// - /// Remove breadcrumbs file - /// - public bool Clear() - { - try - { - File.Delete(BreadcrumbsFilePath); - return true; - } - catch (Exception) - { - return false; - } - } - } -} +using Backtrace.Unity.Common; +using Backtrace.Unity.Json; +using System; +using System.Collections.Generic; +using System.IO; + +namespace Backtrace.Unity.Model.Breadcrumbs.Storage +{ + internal sealed class BacktraceStorageLogManager : IBacktraceLogManager + { + /// + /// Path to the breadcrumbs file + /// + public string BreadcrumbsFilePath { get; private set; } + + /// + /// Minimum size of the breadcrumbs file (10kB) + /// + public const int MinimumBreadcrumbsFileSize = 10 * 1000; + + /// + /// Breadcrumbs file size. + /// + public long BreadcrumbsSize + { + get + { + return _breadcrumbsSize; + } + set + { + if (value < MinimumBreadcrumbsFileSize) + { + throw new ArgumentException("Breadcrumbs size must be greater or equal to 10kB"); + } + _breadcrumbsSize = value; + } + } + + /// + /// Default breacrumbs size. By default breadcrumbs file size is limitted to 64kB. + /// + private long _breadcrumbsSize = 64000; + + /// + /// Default log file name + /// + internal const string BreadcrumbLogFileName = "bt-breadcrumbs-0"; + + /// + /// default breadcrumb row ending + /// + /// Default breadcrumb document ending + /// + private byte[] _endOfDocument = System.Text.Encoding.UTF8.GetBytes("\n]"); + + /// + /// Default breadcrumb end of the document + /// + private byte[] _startOfDocument = System.Text.Encoding.UTF8.GetBytes("[\n"); + + /// + /// Breadcrumb id + /// + private long _breadcrumbId = 0; + + /// + /// Lock object + /// + private object _lockObject = new object(); + + /// + /// Current breadcurmbs fle size + /// + private long currentSize = 0; + + /// + /// Queue that represents number of bytes in each log stored in the breadcrumb file + /// + private readonly Queue _logSize = new Queue(); + + public BacktraceStorageLogManager(string storagePath) + { + if (string.IsNullOrEmpty(storagePath)) + { + throw new ArgumentException("Breadcrumbs storage path is null or empty"); + } + BreadcrumbsFilePath = Path.Combine(storagePath, BreadcrumbLogFileName); + } + + /// + /// Enables breadcrumbs integration + /// + /// true if breadcrumbs file was created. Otherwise false. + public bool Enable() + { + try + { + if (File.Exists(BreadcrumbsFilePath)) + { + File.Delete(BreadcrumbsFilePath); + } + + using (var _breadcrumbStream = new FileStream(BreadcrumbsFilePath, FileMode.CreateNew, FileAccess.Write)) + { + _breadcrumbStream.Write(_startOfDocument, 0, _startOfDocument.Length); + _breadcrumbStream.Write(_endOfDocument, 0, _endOfDocument.Length); + } + currentSize = _startOfDocument.Length + _endOfDocument.Length; + } + catch (Exception e) + { + System.Diagnostics.Debug.WriteLine(string.Format("Cannot initialize breadcrumbs file. Reason: {0}", e.Message)); + return false; + } + return true; + } + + /// + /// Adds breadcrumb entry to the breadcrumbs file. + /// + /// Breadcrumb message + /// Breadcrumb level + /// Breadcrumb type + /// Breadcrumb attributs + /// True if breadcrumb was stored in the breadcrumbs file. Otherwise false. + public bool Add(string message, BreadcrumbLevel level, UnityEngineLogLevel type, IDictionary attributes) + { + byte[] bytes; + lock (_lockObject) + { + long id = _breadcrumbId++; + var jsonObject = CreateBreadcrumbJson(id, message, level, type, attributes); + bytes = System.Text.Encoding.UTF8.GetBytes(jsonObject.ToJson()); + + if (currentSize + bytes.Length > BreadcrumbsSize) + { + try + { + ClearOldLogs(); + } + catch (Exception) + { + return false; + } + } + } + + try + { + return AppendBreadcrumb(bytes); + } + catch (Exception) + { + return false; + } + } + + /// + /// Convert diagnostic data to JSON format + /// + /// Breadcrumbs id + /// breadcrumbs message + /// Breadcrumb level + /// Breadcrumb type + /// Breadcrumb attributes + /// JSON object + private BacktraceJObject CreateBreadcrumbJson( + long id, + string message, + BreadcrumbLevel level, + UnityEngineLogLevel type, + IDictionary attributes) + { + var jsonObject = new BacktraceJObject(); + // breadcrumbs integration accepts timestamp in ms not in sec. + jsonObject.Add("timestamp", DateTimeHelper.TimestampMs(), "F0"); + jsonObject.Add("id", id); + jsonObject.Add("type", Enum.GetName(typeof(BreadcrumbLevel), level).ToLower()); + jsonObject.Add("level", Enum.GetName(typeof(UnityEngineLogLevel), type).ToLower()); + jsonObject.Add("message", message); + if (attributes != null && attributes.Count > 0) + { + jsonObject.Add("attributes", new BacktraceJObject(attributes)); + } + return jsonObject; + } + /// + /// Append breadcrumb JSON to the end of the breadcrumbs file. + /// + /// Bytes that represents single JSON object with breadcrumb informatiom + private bool AppendBreadcrumb(byte[] bytes) + { + // size of the breadcrumb - it's negative at the beginning because we're removing 2 bytes on start + long appendingSize = _endOfDocument.Length * -1; + using (var breadcrumbStream = new FileStream(BreadcrumbsFilePath, FileMode.Open, FileAccess.Write)) + { + //back to position before end of the document \n} + breadcrumbStream.Position = breadcrumbStream.Length - _endOfDocument.Length; + + // append ,\n when we're appending new row to existing list of rows. If this is first row + // ignore it + if (_breadcrumbId != 1) + { + breadcrumbStream.Write(_newRow, 0, _newRow.Length); + appendingSize += _newRow.Length; + } + // append breadcrumbs json + breadcrumbStream.Write(bytes, 0, bytes.Length); + // and close JSON document + breadcrumbStream.Write(_endOfDocument, 0, _endOfDocument.Length); + appendingSize += (bytes.Length + _endOfDocument.Length); + } + currentSize += appendingSize; + _logSize.Enqueue(bytes.Length); + return true; + } + + /// + /// Remove last n logs from the breadcrumbs file. When breacrumbs file hit + /// the file size limit, this method will clear up the oldest logs to decrease + /// file size. + /// + private void ClearOldLogs() + { + var startPosition = GetNextStartPosition(); + using (FileStream breadcrumbsStream = new FileStream(BreadcrumbsFilePath, FileMode.Open, FileAccess.ReadWrite)) + { + using (MemoryStream ms = new MemoryStream()) + { + var size = breadcrumbsStream.Length - startPosition; + breadcrumbsStream.Seek(size * -1, SeekOrigin.End); + + breadcrumbsStream.CopyTo(ms); + breadcrumbsStream.SetLength(size + _startOfDocument.Length); + + ms.Position = 0; + breadcrumbsStream.Position = 0; + + breadcrumbsStream.Write(_startOfDocument, 0, _startOfDocument.Length); + ms.CopyTo(breadcrumbsStream); + } + } + // decrease a size of the breadcrumb file after removing n breadcrumbs + currentSize -= startPosition; + currentSize += _startOfDocument.Length; + } + + /// + /// Calculate start position of the file that will be used + /// to recreate breadcrumbs file. Position represents place + /// where starts breadcrumbs that we should keep in the recreated file. + /// + /// Breadcrumb start index + private long GetNextStartPosition() + { + double expectedFreedBytes = BreadcrumbsSize - (BreadcrumbsSize * 0.7); + long numberOfFreeBytes = _startOfDocument.Length; + int nextLineBytes = _newRow.Length; + while (numberOfFreeBytes < expectedFreedBytes) + { + numberOfFreeBytes += (_logSize.Dequeue() + nextLineBytes); + } + + return numberOfFreeBytes; + } + + /// + /// Remove breadcrumbs file + /// + public bool Clear() + { + try + { + File.Delete(BreadcrumbsFilePath); + return true; + } + catch (Exception) + { + return false; + } + } + } +} diff --git a/Runtime/Model/Breadcrumbs/BacktraceStorageLogManager.cs.meta b/Runtime/Model/Breadcrumbs/Storage/BacktraceStorageLogManager.cs.meta similarity index 100% rename from Runtime/Model/Breadcrumbs/BacktraceStorageLogManager.cs.meta rename to Runtime/Model/Breadcrumbs/Storage/BacktraceStorageLogManager.cs.meta diff --git a/Runtime/Services/BacktraceDatabaseFileContext.cs b/Runtime/Services/BacktraceDatabaseFileContext.cs index 832d769c..dfbfd28b 100644 --- a/Runtime/Services/BacktraceDatabaseFileContext.cs +++ b/Runtime/Services/BacktraceDatabaseFileContext.cs @@ -1,6 +1,6 @@ using Backtrace.Unity.Interfaces; using Backtrace.Unity.Model; -using Backtrace.Unity.Model.Breadcrumbs; +using Backtrace.Unity.Model.Breadcrumbs.Storage; using Backtrace.Unity.Model.Database; using System; using System.Collections.Generic; diff --git a/Tests/Runtime/Breadcrumbs.meta b/Tests/Runtime/Breadcrumbs.meta new file mode 100644 index 00000000..5baf83c9 --- /dev/null +++ b/Tests/Runtime/Breadcrumbs.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 46b71c34d732e95439034857ae7f1ef7 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Runtime/Breadcrumbs/BacktraceBreadcrumbsTypeTests.cs b/Tests/Runtime/Breadcrumbs/BacktraceBreadcrumbsTypeTests.cs new file mode 100644 index 00000000..8340b27d --- /dev/null +++ b/Tests/Runtime/Breadcrumbs/BacktraceBreadcrumbsTypeTests.cs @@ -0,0 +1,46 @@ +using Backtrace.Unity.Model.Breadcrumbs; +using Backtrace.Unity.Model.Breadcrumbs.InMemory; +using NUnit.Framework; +using UnityEngine; + +namespace Backtrace.Unity.Tests.Runtime.Breadcrumbs +{ + public class BacktraceBreadcrumbsTypeTests + { + [TestCase(LogType.Log)] + [TestCase(LogType.Warning)] + [TestCase(LogType.Assert)] + [TestCase(LogType.Error)] + [TestCase(LogType.Exception)] + public void TestManualLogs_ShouldFilterAllManualLogs_BreadcrumbsWasntSaved(LogType testedLevel) + { + const string message = "message"; + const int expectedNumberOfLogs = 0; + var inMemoryBreadcrumbStorage = new BacktraceInMemoryLogManager(); + var breadcrumbsManager = new BacktraceBreadcrumbs(inMemoryBreadcrumbStorage); + //anything else than Manual + var breadcrumbType = BacktraceBreadcrumbType.Configuration; + UnityEngineLogLevel level = UnityEngineLogLevel.Debug | UnityEngineLogLevel.Error | UnityEngineLogLevel.Fatal | UnityEngineLogLevel.Info | UnityEngineLogLevel.Warning; + + breadcrumbsManager.EnableBreadcrumbs(breadcrumbType, level); + var result = breadcrumbsManager.AddBreadcrumbs(message, testedLevel); + + Assert.IsFalse(result); + Assert.AreEqual(expectedNumberOfLogs, inMemoryBreadcrumbStorage.Breadcrumbs.Count); + } + + [Test] + public void TestSystemLogs_ShouldEnableThem_EventsAreSet() + { + var inMemoryBreadcrumbStorage = new BacktraceInMemoryLogManager(); + var breadcrumbsManager = new BacktraceBreadcrumbs(inMemoryBreadcrumbStorage); + UnityEngineLogLevel level = UnityEngineLogLevel.Debug | UnityEngineLogLevel.Error | UnityEngineLogLevel.Fatal | UnityEngineLogLevel.Info | UnityEngineLogLevel.Warning; + + breadcrumbsManager.EnableBreadcrumbs(BacktraceBreadcrumbType.System, level); + + Assert.IsTrue(breadcrumbsManager.EventHandler.HasRegisteredEvents); + breadcrumbsManager.UnregisterEvents(); + } + + } +} diff --git a/Tests/Runtime/Breadcrumbs/BacktraceBreadcrumbsTypeTests.cs.meta b/Tests/Runtime/Breadcrumbs/BacktraceBreadcrumbsTypeTests.cs.meta new file mode 100644 index 00000000..9e6d5b5a --- /dev/null +++ b/Tests/Runtime/Breadcrumbs/BacktraceBreadcrumbsTypeTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 8f9d94ccb7262d64f96a2227bc64fa31 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Runtime/Breadcrumbs/BreadcrumbsLogLevelTests.cs b/Tests/Runtime/Breadcrumbs/BreadcrumbsLogLevelTests.cs new file mode 100644 index 00000000..8568a1a5 --- /dev/null +++ b/Tests/Runtime/Breadcrumbs/BreadcrumbsLogLevelTests.cs @@ -0,0 +1,180 @@ +using Backtrace.Unity.Model.Breadcrumbs; +using Backtrace.Unity.Model.Breadcrumbs.InMemory; +using NUnit.Framework; +using System; +using System.Collections.Generic; +using System.Linq; +using UnityEngine; + +namespace Backtrace.Unity.Tests.Runtime.Breadcrumbs +{ + public class BreadcrumbsLogLevelTests + { + private const BacktraceBreadcrumbType ManualBreadcrumbsType = BacktraceBreadcrumbType.Manual; + + [TestCase(LogType.Log)] + [TestCase(LogType.Warning)] + [TestCase(LogType.Assert)] + [TestCase(LogType.Error)] + [TestCase(LogType.Exception)] + public void TestLogLevel_ShouldFilterLogLevel_BreadcrumbIsNotAvailable(LogType testedLevel) + { + const string message = "message"; + const int expectedNumberOfLogs = 0; + var inMemoryBreadcrumbStorage = new BacktraceInMemoryLogManager(); + var breadcrumbsManager = new BacktraceBreadcrumbs(inMemoryBreadcrumbStorage); + + var logTypeThatUnsupportCurrentTestCase = + (Enum.GetValues(typeof(UnityEngineLogLevel)) as IEnumerable) + .First(n => n != breadcrumbsManager.ConvertLogTypeToLogLevel(testedLevel)); + + breadcrumbsManager.EnableBreadcrumbs(ManualBreadcrumbsType, logTypeThatUnsupportCurrentTestCase); + + var result = breadcrumbsManager.AddBreadcrumbs(message, testedLevel); + + Assert.IsFalse(result); + Assert.AreEqual(expectedNumberOfLogs, inMemoryBreadcrumbStorage.Breadcrumbs.Count); + } + + [TestCase(LogType.Log)] + [TestCase(LogType.Warning)] + [TestCase(LogType.Assert)] + [TestCase(LogType.Error)] + [TestCase(LogType.Exception)] + public void TestLogLevel_ShouldtFilterLogLevel_BreadcrumbIsAvailable(LogType testedLevel) + { + const string message = "message"; + const int expectedNumberOfLogs = 1; + var inMemoryBreadcrumbStorage = new BacktraceInMemoryLogManager(); + var breadcrumbsManager = new BacktraceBreadcrumbs(inMemoryBreadcrumbStorage); + var unityEngineLogLevel = breadcrumbsManager.ConvertLogTypeToLogLevel(testedLevel); + var logTypeThatUnsupportCurrentTestCase = + (Enum.GetValues(typeof(UnityEngineLogLevel)) as IEnumerable) + .First(n => n == unityEngineLogLevel); + + breadcrumbsManager.EnableBreadcrumbs(ManualBreadcrumbsType, logTypeThatUnsupportCurrentTestCase); + var result = breadcrumbsManager.AddBreadcrumbs(message, testedLevel); + + Assert.IsTrue(result); + Assert.AreEqual(expectedNumberOfLogs, inMemoryBreadcrumbStorage.Breadcrumbs.Count); + var breadcrumb = inMemoryBreadcrumbStorage.Breadcrumbs.ElementAt(0); + Assert.AreEqual(message, breadcrumb.Message); + Assert.AreEqual(unityEngineLogLevel, breadcrumb.Type); + Assert.AreEqual(BreadcrumbLevel.Manual, breadcrumb.Level); + Assert.IsNull(breadcrumb.Attributes); + } + + [TestCase(LogType.Log)] + [TestCase(LogType.Warning)] + [TestCase(LogType.Assert)] + [TestCase(LogType.Error)] + [TestCase(LogType.Exception)] + public void TestLogLevel_ShouldtFilterLogLevelWithAttributes_BreadcrumbIsAvailable(LogType testedLevel) + { + const string message = "message"; + const string attributeName = "foo"; + const string attributeValue = "bar"; + const int expectedNumberOfLogs = 1; + var inMemoryBreadcrumbStorage = new BacktraceInMemoryLogManager(); + var breadcrumbsManager = new BacktraceBreadcrumbs(inMemoryBreadcrumbStorage); + var unityEngineLogLevel = breadcrumbsManager.ConvertLogTypeToLogLevel(testedLevel); + var logTypeThatUnsupportCurrentTestCase = + (Enum.GetValues(typeof(UnityEngineLogLevel)) as IEnumerable) + .First(n => n == unityEngineLogLevel); + + breadcrumbsManager.EnableBreadcrumbs(ManualBreadcrumbsType, logTypeThatUnsupportCurrentTestCase); + var result = breadcrumbsManager.AddBreadcrumbs(message, testedLevel, new Dictionary() { { attributeName, attributeValue } }); + + Assert.IsTrue(result); + Assert.AreEqual(expectedNumberOfLogs, inMemoryBreadcrumbStorage.Breadcrumbs.Count); + var breadcrumb = inMemoryBreadcrumbStorage.Breadcrumbs.ElementAt(0); + Assert.IsTrue(breadcrumb.Attributes.ContainsKey(attributeName)); + Assert.AreEqual(attributeValue, breadcrumb.Attributes[attributeName]); + } + + [Test] + public void DebugLogLevel_ShouldFilterDebugLogLevel_BreadcrumbsWasntSave() + { + const string message = "message"; + const int expectedNumberOfLogs = 0; + var inMemoryBreadcrumbStorage = new BacktraceInMemoryLogManager(); + var breadcrumbsManager = new BacktraceBreadcrumbs(inMemoryBreadcrumbStorage); + var logTypeThatUnsupportCurrentTestCase = UnityEngineLogLevel.Error; + breadcrumbsManager.EnableBreadcrumbs(ManualBreadcrumbsType, logTypeThatUnsupportCurrentTestCase); + + var result = breadcrumbsManager.Debug(message); + + Assert.IsFalse(result); + Assert.AreEqual(expectedNumberOfLogs, inMemoryBreadcrumbStorage.Breadcrumbs.Count); + } + + [Test] + public void DebugLogLevel_ShouldntFilterDebugLogLevel_BreadcrumbIsAvailable() + { + const string message = "message"; + const int expectedNumberOfLogs = 1; + var inMemoryBreadcrumbStorage = new BacktraceInMemoryLogManager(); + var breadcrumbsManager = new BacktraceBreadcrumbs(inMemoryBreadcrumbStorage); + var supportedLogLevel = UnityEngineLogLevel.Debug; + breadcrumbsManager.EnableBreadcrumbs(ManualBreadcrumbsType, supportedLogLevel); + + var result = breadcrumbsManager.Debug(message); + + Assert.IsTrue(result); + Assert.AreEqual(expectedNumberOfLogs, inMemoryBreadcrumbStorage.Breadcrumbs.Count); + } + + [Test] + public void DebugLogLevel_ShouldntFilterDebugLogLevelWithAttributes_BreadcrumbIsAvailable() + { + const string message = "message"; + const string attributeName = "foo"; + const string attributeValue = "bar"; + const int expectedNumberOfLogs = 1; + var inMemoryBreadcrumbStorage = new BacktraceInMemoryLogManager(); + var breadcrumbsManager = new BacktraceBreadcrumbs(inMemoryBreadcrumbStorage); + var supportedLogLevel = UnityEngineLogLevel.Debug; + + breadcrumbsManager.EnableBreadcrumbs(ManualBreadcrumbsType, supportedLogLevel); + var result = breadcrumbsManager.Debug(message, new Dictionary() { { attributeName, attributeValue } }); + + Assert.IsTrue(result); + Assert.AreEqual(expectedNumberOfLogs, inMemoryBreadcrumbStorage.Breadcrumbs.Count); + var breadcrumbAttributes = inMemoryBreadcrumbStorage.Breadcrumbs.ElementAt(0).Attributes; + Assert.IsTrue(breadcrumbAttributes.ContainsKey(attributeName)); + Assert.AreEqual(attributeValue, breadcrumbAttributes[attributeName]); + } + + [Test] + public void WarningLogLevel_ShouldFilterWarningLogLevel_BreadcrumbsWasntSave() + { + const string message = "message"; + const int expectedNumberOfLogs = 0; + var inMemoryBreadcrumbStorage = new BacktraceInMemoryLogManager(); + var breadcrumbsManager = new BacktraceBreadcrumbs(inMemoryBreadcrumbStorage); + var logTypeThatUnsupportCurrentTestCase = UnityEngineLogLevel.Error; + + breadcrumbsManager.EnableBreadcrumbs(ManualBreadcrumbsType, logTypeThatUnsupportCurrentTestCase); + var result = breadcrumbsManager.Warning(message); + + Assert.IsFalse(result); + Assert.AreEqual(expectedNumberOfLogs, inMemoryBreadcrumbStorage.Breadcrumbs.Count); + } + + [Test] + public void WarningLogLevel_ShouldntFilterWarningLogLevel_BreadcrumbIsAvailable() + { + const string message = "message"; + const int expectedNumberOfLogs = 1; + var inMemoryBreadcrumbStorage = new BacktraceInMemoryLogManager(); + var breadcrumbsManager = new BacktraceBreadcrumbs(inMemoryBreadcrumbStorage); + var supportedLogLevel = UnityEngineLogLevel.Warning; + breadcrumbsManager.EnableBreadcrumbs(ManualBreadcrumbsType, supportedLogLevel); + + var result = breadcrumbsManager.Warning(message); + + Assert.IsTrue(result); + Assert.AreEqual(expectedNumberOfLogs, inMemoryBreadcrumbStorage.Breadcrumbs.Count); + } + } +} diff --git a/Tests/Runtime/Breadcrumbs/BreadcrumbsLogLevelTests.cs.meta b/Tests/Runtime/Breadcrumbs/BreadcrumbsLogLevelTests.cs.meta new file mode 100644 index 00000000..ea6d980c --- /dev/null +++ b/Tests/Runtime/Breadcrumbs/BreadcrumbsLogLevelTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: d92007b56d776374eb0fc6bb99257360 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: From f3bb906889e4dca169272d702e536ff04b1b22b5 Mon Sep 17 00:00:00 2001 From: kdysput Date: Mon, 10 May 2021 12:03:18 +0200 Subject: [PATCH 16/42] Breadcrumbs unit tests - breadcrumb file tests --- .../InMemory/BacktraceInMemoryLogManager.cs | 8 +- .../InMemory/InMemoryBreadcrumb.cs | 51 ++++++++++- .../Storage/BacktraceStorageLogManager.cs | 52 ++++++----- .../Breadcrumbs/Storage/BreadcrumbFile.cs | 38 ++++++++ .../Storage/BreadcrumbFile.cs.meta | 11 +++ .../Breadcrumbs/Storage/IBreadcrumbFile.cs | 17 ++++ .../Storage/IBreadcrumbFile.cs.meta | 11 +++ .../BreadcrumbsFileOperationTests.cs | 91 +++++++++++++++++++ .../BreadcrumbsFileOperationTests.cs.meta | 11 +++ Tests/Runtime/Breadcrumbs/Mocks.meta | 8 ++ .../Mocks/InMemoryBreadcrumbFile.cs | 45 +++++++++ .../Mocks/InMemoryBreadcrumbFile.cs.meta | 11 +++ 12 files changed, 323 insertions(+), 31 deletions(-) create mode 100644 Runtime/Model/Breadcrumbs/Storage/BreadcrumbFile.cs create mode 100644 Runtime/Model/Breadcrumbs/Storage/BreadcrumbFile.cs.meta create mode 100644 Runtime/Model/Breadcrumbs/Storage/IBreadcrumbFile.cs create mode 100644 Runtime/Model/Breadcrumbs/Storage/IBreadcrumbFile.cs.meta create mode 100644 Tests/Runtime/Breadcrumbs/BreadcrumbsFileOperationTests.cs create mode 100644 Tests/Runtime/Breadcrumbs/BreadcrumbsFileOperationTests.cs.meta create mode 100644 Tests/Runtime/Breadcrumbs/Mocks.meta create mode 100644 Tests/Runtime/Breadcrumbs/Mocks/InMemoryBreadcrumbFile.cs create mode 100644 Tests/Runtime/Breadcrumbs/Mocks/InMemoryBreadcrumbFile.cs.meta diff --git a/Runtime/Model/Breadcrumbs/InMemory/BacktraceInMemoryLogManager.cs b/Runtime/Model/Breadcrumbs/InMemory/BacktraceInMemoryLogManager.cs index a680dcd3..7c2e29ec 100644 --- a/Runtime/Model/Breadcrumbs/InMemory/BacktraceInMemoryLogManager.cs +++ b/Runtime/Model/Breadcrumbs/InMemory/BacktraceInMemoryLogManager.cs @@ -1,4 +1,6 @@ -using System.Collections.Generic; +using Backtrace.Unity.Common; +using System.Collections.Generic; +using System.Globalization; [assembly: System.Runtime.CompilerServices.InternalsVisibleTo("Backtrace.Unity.Tests.Runtime")] namespace Backtrace.Unity.Model.Breadcrumbs.InMemory @@ -36,7 +38,7 @@ public string BreadcrumbsFilePath } } - public bool Add(string message, BreadcrumbLevel level, UnityEngineLogLevel type, IDictionary attributes) + public bool Add(string message, BreadcrumbLevel type, UnityEngineLogLevel level, IDictionary attributes) { lock (_lockObject) { @@ -48,9 +50,11 @@ public bool Add(string message, BreadcrumbLevel level, UnityEngineLogLevel type, } } } + Breadcrumbs.Enqueue(new InMemoryBreadcrumb() { Message = message, + Timestamp = DateTimeHelper.TimestampMs().ToString("F0", CultureInfo.InvariantCulture), Level = level, Type = type, Attributes = attributes diff --git a/Runtime/Model/Breadcrumbs/InMemory/InMemoryBreadcrumb.cs b/Runtime/Model/Breadcrumbs/InMemory/InMemoryBreadcrumb.cs index 011a90cd..315df29d 100644 --- a/Runtime/Model/Breadcrumbs/InMemory/InMemoryBreadcrumb.cs +++ b/Runtime/Model/Breadcrumbs/InMemory/InMemoryBreadcrumb.cs @@ -1,11 +1,52 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; namespace Backtrace.Unity.Model.Breadcrumbs.InMemory { + [Serializable] public class InMemoryBreadcrumb { - public string Message { get; set; } - public BreadcrumbLevel Level { get; set; } - public UnityEngineLogLevel Type { get; set; } - public IDictionary Attributes { get; set; } + public string Message + { + get { return message; } + set + { + message = value; + } + } + public string message; + public string Timestamp + { + get { return timestamp; } + set { timestamp = value; } + } + public string timestamp; + + public BreadcrumbLevel Type + { + get + { + return (BreadcrumbLevel)Enum.Parse(typeof(BreadcrumbLevel), type, true); + } + set + { + type = Enum.GetName(typeof(BreadcrumbLevel), value).ToLower(); + } + } + public string type; + + public UnityEngineLogLevel Level + { + get + { + return (UnityEngineLogLevel)Enum.Parse(typeof(UnityEngineLogLevel), level, true); + } + set + { + level = Enum.GetName(typeof(UnityEngineLogLevel), value).ToLower(); + } + } + public string level; + [NonSerialized] + public IDictionary Attributes; } } diff --git a/Runtime/Model/Breadcrumbs/Storage/BacktraceStorageLogManager.cs b/Runtime/Model/Breadcrumbs/Storage/BacktraceStorageLogManager.cs index 0416f9e1..5c2e57fa 100644 --- a/Runtime/Model/Breadcrumbs/Storage/BacktraceStorageLogManager.cs +++ b/Runtime/Model/Breadcrumbs/Storage/BacktraceStorageLogManager.cs @@ -50,17 +50,17 @@ public long BreadcrumbsSize /// /// default breadcrumb row ending /// /// Default breadcrumb document ending /// - private byte[] _endOfDocument = System.Text.Encoding.UTF8.GetBytes("\n]"); + internal static byte[] EndOfDocument = System.Text.Encoding.UTF8.GetBytes("\n]"); /// /// Default breadcrumb end of the document /// - private byte[] _startOfDocument = System.Text.Encoding.UTF8.GetBytes("[\n"); + internal static byte[] StartOfDocument = System.Text.Encoding.UTF8.GetBytes("[\n"); /// /// Breadcrumb id @@ -82,6 +82,8 @@ public long BreadcrumbsSize /// private readonly Queue _logSize = new Queue(); + internal IBreadcrumbFile BreadcrumbFile { get; set; } + public BacktraceStorageLogManager(string storagePath) { if (string.IsNullOrEmpty(storagePath)) @@ -89,6 +91,7 @@ public BacktraceStorageLogManager(string storagePath) throw new ArgumentException("Breadcrumbs storage path is null or empty"); } BreadcrumbsFilePath = Path.Combine(storagePath, BreadcrumbLogFileName); + BreadcrumbFile = new BreadcrumbFile(BreadcrumbsFilePath); } /// @@ -99,17 +102,17 @@ public bool Enable() { try { - if (File.Exists(BreadcrumbsFilePath)) + if (BreadcrumbFile.Exists()) { - File.Delete(BreadcrumbsFilePath); + BreadcrumbFile.Delete(); } - using (var _breadcrumbStream = new FileStream(BreadcrumbsFilePath, FileMode.CreateNew, FileAccess.Write)) + using (var _breadcrumbStream = BreadcrumbFile.GetCreateStream()) { - _breadcrumbStream.Write(_startOfDocument, 0, _startOfDocument.Length); - _breadcrumbStream.Write(_endOfDocument, 0, _endOfDocument.Length); + _breadcrumbStream.Write(StartOfDocument, 0, StartOfDocument.Length); + _breadcrumbStream.Write(EndOfDocument, 0, EndOfDocument.Length); } - currentSize = _startOfDocument.Length + _endOfDocument.Length; + currentSize = StartOfDocument.Length + EndOfDocument.Length; } catch (Exception e) { @@ -153,8 +156,9 @@ public bool Add(string message, BreadcrumbLevel level, UnityEngineLogLevel type, { return AppendBreadcrumb(bytes); } - catch (Exception) + catch (Exception e) { + System.Diagnostics.Debug.WriteLine(string.Format("Cannot append data to the breadcrumbs file. Reason: {0}", e.Message)); return false; } } @@ -195,24 +199,24 @@ private BacktraceJObject CreateBreadcrumbJson( private bool AppendBreadcrumb(byte[] bytes) { // size of the breadcrumb - it's negative at the beginning because we're removing 2 bytes on start - long appendingSize = _endOfDocument.Length * -1; - using (var breadcrumbStream = new FileStream(BreadcrumbsFilePath, FileMode.Open, FileAccess.Write)) + long appendingSize = EndOfDocument.Length * -1; + using (var breadcrumbStream = BreadcrumbFile.GetWriteStream()) { //back to position before end of the document \n} - breadcrumbStream.Position = breadcrumbStream.Length - _endOfDocument.Length; + breadcrumbStream.Position = breadcrumbStream.Length - EndOfDocument.Length; // append ,\n when we're appending new row to existing list of rows. If this is first row // ignore it if (_breadcrumbId != 1) { - breadcrumbStream.Write(_newRow, 0, _newRow.Length); - appendingSize += _newRow.Length; + breadcrumbStream.Write(NewRow, 0, NewRow.Length); + appendingSize += NewRow.Length; } // append breadcrumbs json breadcrumbStream.Write(bytes, 0, bytes.Length); // and close JSON document - breadcrumbStream.Write(_endOfDocument, 0, _endOfDocument.Length); - appendingSize += (bytes.Length + _endOfDocument.Length); + breadcrumbStream.Write(EndOfDocument, 0, EndOfDocument.Length); + appendingSize += (bytes.Length + EndOfDocument.Length); } currentSize += appendingSize; _logSize.Enqueue(bytes.Length); @@ -227,7 +231,7 @@ private bool AppendBreadcrumb(byte[] bytes) private void ClearOldLogs() { var startPosition = GetNextStartPosition(); - using (FileStream breadcrumbsStream = new FileStream(BreadcrumbsFilePath, FileMode.Open, FileAccess.ReadWrite)) + using (var breadcrumbsStream = BreadcrumbFile.GetIOStream()) { using (MemoryStream ms = new MemoryStream()) { @@ -235,18 +239,18 @@ private void ClearOldLogs() breadcrumbsStream.Seek(size * -1, SeekOrigin.End); breadcrumbsStream.CopyTo(ms); - breadcrumbsStream.SetLength(size + _startOfDocument.Length); + breadcrumbsStream.SetLength(size + StartOfDocument.Length); ms.Position = 0; breadcrumbsStream.Position = 0; - breadcrumbsStream.Write(_startOfDocument, 0, _startOfDocument.Length); + breadcrumbsStream.Write(StartOfDocument, 0, StartOfDocument.Length); ms.CopyTo(breadcrumbsStream); } } // decrease a size of the breadcrumb file after removing n breadcrumbs currentSize -= startPosition; - currentSize += _startOfDocument.Length; + currentSize += StartOfDocument.Length; } /// @@ -258,8 +262,8 @@ private void ClearOldLogs() private long GetNextStartPosition() { double expectedFreedBytes = BreadcrumbsSize - (BreadcrumbsSize * 0.7); - long numberOfFreeBytes = _startOfDocument.Length; - int nextLineBytes = _newRow.Length; + long numberOfFreeBytes = StartOfDocument.Length; + int nextLineBytes = NewRow.Length; while (numberOfFreeBytes < expectedFreedBytes) { numberOfFreeBytes += (_logSize.Dequeue() + nextLineBytes); @@ -275,7 +279,7 @@ public bool Clear() { try { - File.Delete(BreadcrumbsFilePath); + BreadcrumbFile.Delete(); return true; } catch (Exception) diff --git a/Runtime/Model/Breadcrumbs/Storage/BreadcrumbFile.cs b/Runtime/Model/Breadcrumbs/Storage/BreadcrumbFile.cs new file mode 100644 index 00000000..2af57635 --- /dev/null +++ b/Runtime/Model/Breadcrumbs/Storage/BreadcrumbFile.cs @@ -0,0 +1,38 @@ +using System.IO; + +namespace Backtrace.Unity.Model.Breadcrumbs.Storage +{ + internal sealed class BreadcrumbFile : IBreadcrumbFile + { + private readonly string _path; + public BreadcrumbFile(string path) + { + _path = path; + } + + public void Delete() + { + File.Delete(_path); + } + + public bool Exists() + { + return File.Exists(_path); + } + + public Stream GetCreateStream() + { + return new FileStream(_path, FileMode.CreateNew, FileAccess.Write); + } + + public Stream GetIOStream() + { + return new FileStream(_path, FileMode.Open, FileAccess.ReadWrite); + } + + public Stream GetWriteStream() + { + return new FileStream(_path, FileMode.Open, FileAccess.Write); + } + } +} diff --git a/Runtime/Model/Breadcrumbs/Storage/BreadcrumbFile.cs.meta b/Runtime/Model/Breadcrumbs/Storage/BreadcrumbFile.cs.meta new file mode 100644 index 00000000..4fad7f60 --- /dev/null +++ b/Runtime/Model/Breadcrumbs/Storage/BreadcrumbFile.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: a5f3ed3ea733ac840b5b393826091f25 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Model/Breadcrumbs/Storage/IBreadcrumbFile.cs b/Runtime/Model/Breadcrumbs/Storage/IBreadcrumbFile.cs new file mode 100644 index 00000000..3b1bd549 --- /dev/null +++ b/Runtime/Model/Breadcrumbs/Storage/IBreadcrumbFile.cs @@ -0,0 +1,17 @@ + +using System.IO; + +[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("Backtrace.Unity.Tests.Runtime")] +namespace Backtrace.Unity.Model.Breadcrumbs.Storage +{ + internal interface IBreadcrumbFile + { + bool Exists(); + void Delete(); + Stream GetCreateStream(); + + Stream GetIOStream(); + Stream GetWriteStream(); + + } +} diff --git a/Runtime/Model/Breadcrumbs/Storage/IBreadcrumbFile.cs.meta b/Runtime/Model/Breadcrumbs/Storage/IBreadcrumbFile.cs.meta new file mode 100644 index 00000000..88332185 --- /dev/null +++ b/Runtime/Model/Breadcrumbs/Storage/IBreadcrumbFile.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 45573102a83353c49b59a63d7a5c8613 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Runtime/Breadcrumbs/BreadcrumbsFileOperationTests.cs b/Tests/Runtime/Breadcrumbs/BreadcrumbsFileOperationTests.cs new file mode 100644 index 00000000..0ae349c3 --- /dev/null +++ b/Tests/Runtime/Breadcrumbs/BreadcrumbsFileOperationTests.cs @@ -0,0 +1,91 @@ +using Backtrace.Unity.Common; +using Backtrace.Unity.Model.Breadcrumbs; +using Backtrace.Unity.Model.Breadcrumbs.InMemory; +using Backtrace.Unity.Model.Breadcrumbs.Storage; +using Backtrace.Unity.Tests.Runtime.Breadcrumbs.Mocks; +using NUnit.Framework; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using UnityEngine; + +namespace Backtrace.Unity.Tests.Runtime.Breadcrumbs +{ + public class BreadcrumbsFileOperationTests + { + private readonly string _startOfDocumentString = Encoding.UTF8.GetString(BacktraceStorageLogManager.StartOfDocument); + private readonly string _endOfDocumentString = Encoding.UTF8.GetString(BacktraceStorageLogManager.EndOfDocument); + private readonly string _newRow = Encoding.UTF8.GetString(BacktraceStorageLogManager.NewRow); + private const BacktraceBreadcrumbType ManualBreadcrumbsType = BacktraceBreadcrumbType.Manual; + + [Test] + public void TestFileCreation_ShouldRecreateFileIfFileExists_SuccessfullyRecreatedFile() + { + var breadcrumbFile = new InMemoryBreadcrumbFile(); + var breadcrumbsStorageManager = new BacktraceStorageLogManager(Application.temporaryCachePath) + { + BreadcrumbFile = breadcrumbFile + }; + string emptyDocumentText = _startOfDocumentString + _endOfDocumentString; + + var enableResult = breadcrumbsStorageManager.Enable(); + + Assert.IsTrue(enableResult); + Assert.AreEqual(Encoding.UTF8.GetBytes(emptyDocumentText), breadcrumbFile.MemoryStream.ToArray()); + } + + [TestCase(LogType.Log)] + [TestCase(LogType.Warning)] + [TestCase(LogType.Assert)] + [TestCase(LogType.Error)] + [TestCase(LogType.Exception)] + public void TestFileCreation_AddNewBreadcrumbToFile_SuccessfullyAddedBreadcrumb(LogType testedLevel) + { + var currentTime = DateTimeHelper.TimestampMs(); + const string breadcrumbMessage = "foo"; + var breadcrumbFile = new InMemoryBreadcrumbFile(); + var breadcrumbsStorageManager = new BacktraceStorageLogManager(Application.temporaryCachePath) + { + BreadcrumbFile = breadcrumbFile + }; + var breadcrumbsManager = new BacktraceBreadcrumbs(breadcrumbsStorageManager); + var unityEngineLogLevel = breadcrumbsManager.ConvertLogTypeToLogLevel(testedLevel); + var logTypeThatUnsupportCurrentTestCase = + (Enum.GetValues(typeof(UnityEngineLogLevel)) as IEnumerable) + .First(n => n == unityEngineLogLevel); + + breadcrumbsManager.EnableBreadcrumbs(ManualBreadcrumbsType, logTypeThatUnsupportCurrentTestCase); + var added = breadcrumbsManager.AddBreadcrumbs(breadcrumbMessage, testedLevel); + + Assert.IsTrue(added); + var json = Encoding.UTF8.GetString(breadcrumbFile.MemoryStream.ToArray()); + var data = ConvertToBreadcrumbs(json); + Assert.AreEqual(1, data.Count()); + var breadcrumb = data.First(); + Assert.AreEqual(ManualBreadcrumbsType, (BacktraceBreadcrumbType)breadcrumb.Type); + Assert.AreEqual(unityEngineLogLevel, breadcrumb.Level); + Assert.AreEqual(breadcrumbMessage, breadcrumb.Message); + + } + + + private IEnumerable ConvertToBreadcrumbs(string json) + { + if (!json.StartsWith(_startOfDocumentString) || !json.EndsWith(_endOfDocumentString)) + { + throw new ArgumentException("Invalid JSON file"); + } + var dataJson = json + .Substring(json.IndexOf(_startOfDocumentString) + _startOfDocumentString.Length, json.Length - _startOfDocumentString.Length - _endOfDocumentString.Length) + .Split(new string[1] { _newRow }, StringSplitOptions.None); + + var result = new List(); + foreach (var data in dataJson) + { + result.Add(JsonUtility.FromJson(data)); + } + return result; + } + } +} diff --git a/Tests/Runtime/Breadcrumbs/BreadcrumbsFileOperationTests.cs.meta b/Tests/Runtime/Breadcrumbs/BreadcrumbsFileOperationTests.cs.meta new file mode 100644 index 00000000..a9b414e0 --- /dev/null +++ b/Tests/Runtime/Breadcrumbs/BreadcrumbsFileOperationTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: d162af157dcc53245be5c5ebae95d367 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Runtime/Breadcrumbs/Mocks.meta b/Tests/Runtime/Breadcrumbs/Mocks.meta new file mode 100644 index 00000000..c848c2a4 --- /dev/null +++ b/Tests/Runtime/Breadcrumbs/Mocks.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: b5b4ed973cb1fb84aa8276bdd63afce2 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Runtime/Breadcrumbs/Mocks/InMemoryBreadcrumbFile.cs b/Tests/Runtime/Breadcrumbs/Mocks/InMemoryBreadcrumbFile.cs new file mode 100644 index 00000000..74844c23 --- /dev/null +++ b/Tests/Runtime/Breadcrumbs/Mocks/InMemoryBreadcrumbFile.cs @@ -0,0 +1,45 @@ +using Backtrace.Unity.Model.Breadcrumbs.Storage; +using System.IO; + +namespace Backtrace.Unity.Tests.Runtime.Breadcrumbs.Mocks +{ + public class InMemoryBreadcrumbFile : IBreadcrumbFile + { + public MemoryStream MemoryStream = new MemoryStream(); + public bool FileExists { get; set; } = true; + public void Delete() + { + return; + } + + public bool Exists() + { + return FileExists; + } + + public Stream GetCreateStream() + { + return MemoryStream; + } + + public Stream GetIOStream() + { + RecreateMemoryStream(); + return MemoryStream; + } + + public Stream GetWriteStream() + { + RecreateMemoryStream(); + return MemoryStream; + } + + private void RecreateMemoryStream() + { + var memoryStream = new MemoryStream(); + var content = MemoryStream.ToArray(); + memoryStream.Write(content, 0, content.Length); + MemoryStream = memoryStream; + } + } +} diff --git a/Tests/Runtime/Breadcrumbs/Mocks/InMemoryBreadcrumbFile.cs.meta b/Tests/Runtime/Breadcrumbs/Mocks/InMemoryBreadcrumbFile.cs.meta new file mode 100644 index 00000000..49e30e88 --- /dev/null +++ b/Tests/Runtime/Breadcrumbs/Mocks/InMemoryBreadcrumbFile.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 32fc120546680a5489cdb0747ea00cc4 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: From 326b1ce529a0e1234325ee9d6f07650fde0d3dd7 Mon Sep 17 00:00:00 2001 From: kdysput Date: Mon, 10 May 2021 12:08:19 +0200 Subject: [PATCH 17/42] Renamed enums --- .../Runtime/Breadcrumbs/BreadcrumbsFileOperationTests.cs | 8 +++++--- Tests/Runtime/Breadcrumbs/BreadcrumbsLogLevelTests.cs | 4 ++-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/Tests/Runtime/Breadcrumbs/BreadcrumbsFileOperationTests.cs b/Tests/Runtime/Breadcrumbs/BreadcrumbsFileOperationTests.cs index 0ae349c3..8c1973dd 100644 --- a/Tests/Runtime/Breadcrumbs/BreadcrumbsFileOperationTests.cs +++ b/Tests/Runtime/Breadcrumbs/BreadcrumbsFileOperationTests.cs @@ -59,8 +59,7 @@ public void TestFileCreation_AddNewBreadcrumbToFile_SuccessfullyAddedBreadcrumb( var added = breadcrumbsManager.AddBreadcrumbs(breadcrumbMessage, testedLevel); Assert.IsTrue(added); - var json = Encoding.UTF8.GetString(breadcrumbFile.MemoryStream.ToArray()); - var data = ConvertToBreadcrumbs(json); + var data = ConvertToBreadcrumbs(breadcrumbFile); Assert.AreEqual(1, data.Count()); var breadcrumb = data.First(); Assert.AreEqual(ManualBreadcrumbsType, (BacktraceBreadcrumbType)breadcrumb.Type); @@ -69,7 +68,10 @@ public void TestFileCreation_AddNewBreadcrumbToFile_SuccessfullyAddedBreadcrumb( } - + private IEnumerable ConvertToBreadcrumbs(InMemoryBreadcrumbFile file) + { + return ConvertToBreadcrumbs(Encoding.UTF8.GetString(file.MemoryStream.ToArray())); + } private IEnumerable ConvertToBreadcrumbs(string json) { if (!json.StartsWith(_startOfDocumentString) || !json.EndsWith(_endOfDocumentString)) diff --git a/Tests/Runtime/Breadcrumbs/BreadcrumbsLogLevelTests.cs b/Tests/Runtime/Breadcrumbs/BreadcrumbsLogLevelTests.cs index 8568a1a5..6b03fd5e 100644 --- a/Tests/Runtime/Breadcrumbs/BreadcrumbsLogLevelTests.cs +++ b/Tests/Runtime/Breadcrumbs/BreadcrumbsLogLevelTests.cs @@ -59,8 +59,8 @@ public void TestLogLevel_ShouldtFilterLogLevel_BreadcrumbIsAvailable(LogType tes Assert.AreEqual(expectedNumberOfLogs, inMemoryBreadcrumbStorage.Breadcrumbs.Count); var breadcrumb = inMemoryBreadcrumbStorage.Breadcrumbs.ElementAt(0); Assert.AreEqual(message, breadcrumb.Message); - Assert.AreEqual(unityEngineLogLevel, breadcrumb.Type); - Assert.AreEqual(BreadcrumbLevel.Manual, breadcrumb.Level); + Assert.AreEqual(unityEngineLogLevel, breadcrumb.Level); + Assert.AreEqual(BreadcrumbLevel.Manual, breadcrumb.Type); Assert.IsNull(breadcrumb.Attributes); } From 3fbf5d7048c3454fccab7d35643f0b809a14ed10 Mon Sep 17 00:00:00 2001 From: kdysput Date: Mon, 10 May 2021 18:19:40 +0200 Subject: [PATCH 18/42] Log cleanup tests --- .../InMemory/BacktraceInMemoryLogManager.cs | 2 +- .../InMemory/InMemoryBreadcrumb.cs | 8 +++-- .../BreadcrumbsFileOperationTests.cs | 29 +++++++++++++++++++ .../Mocks/InMemoryBreadcrumbFile.cs | 8 +++++ 4 files changed, 43 insertions(+), 4 deletions(-) diff --git a/Runtime/Model/Breadcrumbs/InMemory/BacktraceInMemoryLogManager.cs b/Runtime/Model/Breadcrumbs/InMemory/BacktraceInMemoryLogManager.cs index 7c2e29ec..fc7c94d6 100644 --- a/Runtime/Model/Breadcrumbs/InMemory/BacktraceInMemoryLogManager.cs +++ b/Runtime/Model/Breadcrumbs/InMemory/BacktraceInMemoryLogManager.cs @@ -54,7 +54,7 @@ public bool Add(string message, BreadcrumbLevel type, UnityEngineLogLevel level, Breadcrumbs.Enqueue(new InMemoryBreadcrumb() { Message = message, - Timestamp = DateTimeHelper.TimestampMs().ToString("F0", CultureInfo.InvariantCulture), + Timestamp = DateTimeHelper.TimestampMs(), Level = level, Type = type, Attributes = attributes diff --git a/Runtime/Model/Breadcrumbs/InMemory/InMemoryBreadcrumb.cs b/Runtime/Model/Breadcrumbs/InMemory/InMemoryBreadcrumb.cs index 315df29d..f9d92944 100644 --- a/Runtime/Model/Breadcrumbs/InMemory/InMemoryBreadcrumb.cs +++ b/Runtime/Model/Breadcrumbs/InMemory/InMemoryBreadcrumb.cs @@ -1,5 +1,7 @@ using System; using System.Collections.Generic; +using System.Globalization; + namespace Backtrace.Unity.Model.Breadcrumbs.InMemory { [Serializable] @@ -14,10 +16,10 @@ public string Message } } public string message; - public string Timestamp + public double Timestamp { - get { return timestamp; } - set { timestamp = value; } + get { return Convert.ToDouble(timestamp); } + set { timestamp = value.ToString("F0", CultureInfo.InvariantCulture); } } public string timestamp; diff --git a/Tests/Runtime/Breadcrumbs/BreadcrumbsFileOperationTests.cs b/Tests/Runtime/Breadcrumbs/BreadcrumbsFileOperationTests.cs index 8c1973dd..2a9c6597 100644 --- a/Tests/Runtime/Breadcrumbs/BreadcrumbsFileOperationTests.cs +++ b/Tests/Runtime/Breadcrumbs/BreadcrumbsFileOperationTests.cs @@ -65,7 +65,36 @@ public void TestFileCreation_AddNewBreadcrumbToFile_SuccessfullyAddedBreadcrumb( Assert.AreEqual(ManualBreadcrumbsType, (BacktraceBreadcrumbType)breadcrumb.Type); Assert.AreEqual(unityEngineLogLevel, breadcrumb.Level); Assert.AreEqual(breadcrumbMessage, breadcrumb.Message); + Assert.That(currentTime, Is.LessThan(breadcrumb.Timestamp)); + } + + [Test] + public void TestFileLimit_ShouldCleanupTheSpace_SpaceWasCleaned() + { + const string breadcrumbMessage = "foo"; + const int minimalSize = 10 * 1000; + var breadcrumbFile = new InMemoryBreadcrumbFile(); + var breadcrumbsStorageManager = new BacktraceStorageLogManager(Application.temporaryCachePath) + { + BreadcrumbFile = breadcrumbFile, + BreadcrumbsSize = minimalSize + }; + var breadcrumbsManager = new BacktraceBreadcrumbs(breadcrumbsStorageManager); + var unityEngineLogLevel = UnityEngineLogLevel.Debug; + breadcrumbsManager.EnableBreadcrumbs(ManualBreadcrumbsType, unityEngineLogLevel); + breadcrumbsManager.AddBreadcrumbs(breadcrumbMessage, LogType.Assert); + var breadcrumbSize = breadcrumbFile.Size - 2; + while (breadcrumbFile.Size + breadcrumbSize < minimalSize != false) + { + breadcrumbsManager.AddBreadcrumbs(breadcrumbMessage, LogType.Assert); + } + var sizeBeforeCleanup = breadcrumbFile.Size; + breadcrumbsManager.AddBreadcrumbs(breadcrumbMessage, LogType.Assert); + + Assert.That(breadcrumbFile.Size, Is.LessThan(sizeBeforeCleanup)); + var data = ConvertToBreadcrumbs(breadcrumbFile); + Assert.IsNotEmpty(data); } private IEnumerable ConvertToBreadcrumbs(InMemoryBreadcrumbFile file) diff --git a/Tests/Runtime/Breadcrumbs/Mocks/InMemoryBreadcrumbFile.cs b/Tests/Runtime/Breadcrumbs/Mocks/InMemoryBreadcrumbFile.cs index 74844c23..44389555 100644 --- a/Tests/Runtime/Breadcrumbs/Mocks/InMemoryBreadcrumbFile.cs +++ b/Tests/Runtime/Breadcrumbs/Mocks/InMemoryBreadcrumbFile.cs @@ -6,6 +6,14 @@ namespace Backtrace.Unity.Tests.Runtime.Breadcrumbs.Mocks public class InMemoryBreadcrumbFile : IBreadcrumbFile { public MemoryStream MemoryStream = new MemoryStream(); + + public long Size + { + get + { + return MemoryStream.ToArray().Length; + } + } public bool FileExists { get; set; } = true; public void Delete() { From 34be4840ca74da113a3064091f21309312d7af8b Mon Sep 17 00:00:00 2001 From: kdysput Date: Mon, 10 May 2021 18:45:17 +0200 Subject: [PATCH 19/42] Size consistency test --- Runtime/Model/Breadcrumbs/IBacktraceLogManager.cs | 2 ++ .../InMemory/BacktraceInMemoryLogManager.cs | 14 +++++++++++++- .../Storage/BacktraceStorageLogManager.cs | 12 ++++++++++++ .../Breadcrumbs/BreadcrumbsFileOperationTests.cs | 10 +++++++++- 4 files changed, 36 insertions(+), 2 deletions(-) diff --git a/Runtime/Model/Breadcrumbs/IBacktraceLogManager.cs b/Runtime/Model/Breadcrumbs/IBacktraceLogManager.cs index 08942c2c..53be53c6 100644 --- a/Runtime/Model/Breadcrumbs/IBacktraceLogManager.cs +++ b/Runtime/Model/Breadcrumbs/IBacktraceLogManager.cs @@ -8,5 +8,7 @@ internal interface IBacktraceLogManager bool Add(string message, BreadcrumbLevel level, UnityEngineLogLevel type, IDictionary attributes); bool Clear(); bool Enable(); + int Length(); + long BreadcrumbId(); } } diff --git a/Runtime/Model/Breadcrumbs/InMemory/BacktraceInMemoryLogManager.cs b/Runtime/Model/Breadcrumbs/InMemory/BacktraceInMemoryLogManager.cs index fc7c94d6..929aaf34 100644 --- a/Runtime/Model/Breadcrumbs/InMemory/BacktraceInMemoryLogManager.cs +++ b/Runtime/Model/Breadcrumbs/InMemory/BacktraceInMemoryLogManager.cs @@ -1,6 +1,5 @@ using Backtrace.Unity.Common; using System.Collections.Generic; -using System.Globalization; [assembly: System.Runtime.CompilerServices.InternalsVisibleTo("Backtrace.Unity.Tests.Runtime")] namespace Backtrace.Unity.Model.Breadcrumbs.InMemory @@ -27,6 +26,8 @@ internal sealed class BacktraceInMemoryLogManager : IBacktraceLogManager /// internal readonly Queue Breadcrumbs = new Queue(DefaultMaximumNumberOfInMemoryBreadcrumbs); + private long _breadcrumbId = 0; + /// /// Returns path to breadcrumb file - which is string.Empty for in memory breadcrumb manager /// @@ -49,6 +50,7 @@ public bool Add(string message, BreadcrumbLevel type, UnityEngineLogLevel level, Breadcrumbs.Dequeue(); } } + _breadcrumbId++; } Breadcrumbs.Enqueue(new InMemoryBreadcrumb() @@ -73,5 +75,15 @@ public bool Enable() { return true; } + + public int Length() + { + return Breadcrumbs.Count; + } + + public long BreadcrumbId() + { + return _breadcrumbId; + } } } diff --git a/Runtime/Model/Breadcrumbs/Storage/BacktraceStorageLogManager.cs b/Runtime/Model/Breadcrumbs/Storage/BacktraceStorageLogManager.cs index 5c2e57fa..fd2362c1 100644 --- a/Runtime/Model/Breadcrumbs/Storage/BacktraceStorageLogManager.cs +++ b/Runtime/Model/Breadcrumbs/Storage/BacktraceStorageLogManager.cs @@ -279,6 +279,8 @@ public bool Clear() { try { + currentSize = 0; + _logSize.Clear(); BreadcrumbFile.Delete(); return true; } @@ -287,5 +289,15 @@ public bool Clear() return false; } } + + public int Length() + { + return _logSize.Count; + } + + public long BreadcrumbId() + { + return _breadcrumbId; + } } } diff --git a/Tests/Runtime/Breadcrumbs/BreadcrumbsFileOperationTests.cs b/Tests/Runtime/Breadcrumbs/BreadcrumbsFileOperationTests.cs index 2a9c6597..891f7883 100644 --- a/Tests/Runtime/Breadcrumbs/BreadcrumbsFileOperationTests.cs +++ b/Tests/Runtime/Breadcrumbs/BreadcrumbsFileOperationTests.cs @@ -65,7 +65,8 @@ public void TestFileCreation_AddNewBreadcrumbToFile_SuccessfullyAddedBreadcrumb( Assert.AreEqual(ManualBreadcrumbsType, (BacktraceBreadcrumbType)breadcrumb.Type); Assert.AreEqual(unityEngineLogLevel, breadcrumb.Level); Assert.AreEqual(breadcrumbMessage, breadcrumb.Message); - Assert.That(currentTime, Is.LessThan(breadcrumb.Timestamp)); + // round timestamp because timestamp value in the final json will reduce decimal part. + Assert.That(currentTime, Is.LessThanOrEqualTo(Math.Round(breadcrumb.Timestamp, 0))); } [Test] @@ -83,18 +84,25 @@ public void TestFileLimit_ShouldCleanupTheSpace_SpaceWasCleaned() var unityEngineLogLevel = UnityEngineLogLevel.Debug; breadcrumbsManager.EnableBreadcrumbs(ManualBreadcrumbsType, unityEngineLogLevel); + int numberOfAddedBreadcrumbs = 1; breadcrumbsManager.AddBreadcrumbs(breadcrumbMessage, LogType.Assert); var breadcrumbSize = breadcrumbFile.Size - 2; while (breadcrumbFile.Size + breadcrumbSize < minimalSize != false) { breadcrumbsManager.AddBreadcrumbs(breadcrumbMessage, LogType.Assert); + numberOfAddedBreadcrumbs++; } var sizeBeforeCleanup = breadcrumbFile.Size; + var numberOfBreadcurmbsBeforeCleanUp = numberOfAddedBreadcrumbs; breadcrumbsManager.AddBreadcrumbs(breadcrumbMessage, LogType.Assert); + numberOfAddedBreadcrumbs++; Assert.That(breadcrumbFile.Size, Is.LessThan(sizeBeforeCleanup)); var data = ConvertToBreadcrumbs(breadcrumbFile); Assert.IsNotEmpty(data); + Assert.AreEqual(numberOfAddedBreadcrumbs, breadcrumbsStorageManager.BreadcrumbId()); + Assert.That(breadcrumbsStorageManager.Length(), Is.LessThan(numberOfBreadcurmbsBeforeCleanUp)); + } private IEnumerable ConvertToBreadcrumbs(InMemoryBreadcrumbFile file) From 0edf4d52a4bcd886447d16235dc4a47d6a4b40a3 Mon Sep 17 00:00:00 2001 From: kdysput Date: Mon, 10 May 2021 18:53:21 +0200 Subject: [PATCH 20/42] Add stack trace attribute only to error/exception logs --- .../Model/Breadcrumbs/BacktraceBreadcrumbsEventHandler.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Runtime/Model/Breadcrumbs/BacktraceBreadcrumbsEventHandler.cs b/Runtime/Model/Breadcrumbs/BacktraceBreadcrumbsEventHandler.cs index 63cb60c2..4c360c2d 100644 --- a/Runtime/Model/Breadcrumbs/BacktraceBreadcrumbsEventHandler.cs +++ b/Runtime/Model/Breadcrumbs/BacktraceBreadcrumbsEventHandler.cs @@ -99,7 +99,10 @@ private void HandleBackgroundMessage(string condition, string stackTrace, LogTyp private void HandleMessage(string condition, string stackTrace, LogType type) { - Log(condition, type, new Dictionary { { "stackTrace", stackTrace } }); + var attributes = type == LogType.Error || type == LogType.Exception + ? new Dictionary { { "stackTrace", stackTrace } } + : null; + Log(condition, type, attributes); } private void Application_focusChanged(bool hasFocus) From 4d8389bdb91e3ffb0a4eff7536cfa78410e886d8 Mon Sep 17 00:00:00 2001 From: kdysput Date: Mon, 10 May 2021 19:29:59 +0200 Subject: [PATCH 21/42] Adjusted native build --- Runtime/BacktraceClient.cs | 10 +++--- Runtime/Native/Android/NativeClient.cs | 9 +++-- Runtime/Native/NativeClientFactory.cs | 6 ++-- Runtime/Native/iOS/NativeClient.cs | 10 +++--- .../BreadcrumbsFileOperationTests.cs | 35 +++++++++++++++++++ 5 files changed, 51 insertions(+), 19 deletions(-) diff --git a/Runtime/BacktraceClient.cs b/Runtime/BacktraceClient.cs index 471ae4f4..ec17f36f 100644 --- a/Runtime/BacktraceClient.cs +++ b/Runtime/BacktraceClient.cs @@ -496,7 +496,7 @@ public void Refresh() } } - _nativeClient = NativeClientFactory.CreateNativeClient(Configuration, name, AttributeProvider.Get()); + _nativeClient = NativeClientFactory.CreateNativeClient(Configuration, name, AttributeProvider.Get(), _clientReportAttachments); AttributeProvider.AddDynamicAttributeProvider(_nativeClient); if (Configuration.SendUnhandledGameCrashesOnGameStartup && isActiveAndEnabled) @@ -830,10 +830,10 @@ internal void OnAnrDetected(string stackTrace) Debug.LogWarning("Please enable BacktraceClient first."); return; } - Breadcrumbs?.FromMonoBehavior("Application ANR Detected", LogType.Warning, null); + const string anrMessage = "ANRException: Blocked thread detected"; - _backtraceLogManager.Enqueue(new BacktraceUnityMessage(anrMessage, stackTrace, LogType.Error)); var hang = new BacktraceUnhandledException(anrMessage, stackTrace); + Breadcrumbs?.FromMonoBehavior(anrMessage, LogType.Warning, new Dictionary { { "stackTrace", stackTrace } }); SendUnhandledException(hang); } #endif @@ -886,8 +886,8 @@ internal void HandleLowMemory() _nativeClient.OnOOM(); } - const string lowMemoryMessage = "OOMException: Out of memory detected."; - _backtraceLogManager.Enqueue(new BacktraceUnityMessage(lowMemoryMessage, string.Empty, LogType.Error)); + const string lowMemoryMessage = "Low memory warning detected."; + Breadcrumbs?.FromMonoBehavior(lowMemoryMessage, LogType.Warning, null); } #endif diff --git a/Runtime/Native/Android/NativeClient.cs b/Runtime/Native/Android/NativeClient.cs index 6093edb0..09c59152 100644 --- a/Runtime/Native/Android/NativeClient.cs +++ b/Runtime/Native/Android/NativeClient.cs @@ -112,7 +112,7 @@ private void SetDefaultAttributeMaps() private bool _captureNativeCrashes = false; private readonly bool _handlerANR = false; - public NativeClient(string gameObjectName, BacktraceConfiguration configuration, IDictionary clientAttributes) + public NativeClient(string gameObjectName, BacktraceConfiguration configuration, IDictionary clientAttributes, IEnumerable attachments) { _configuration = configuration; SetDefaultAttributeMaps(); @@ -123,7 +123,7 @@ public NativeClient(string gameObjectName, BacktraceConfiguration configuration, #if UNITY_ANDROID _handlerANR = _configuration.HandleANR; - HandleNativeCrashes(clientAttributes); + HandleNativeCrashes(clientAttributes, attachments); HandleAnr(gameObjectName, "OnAnrDetected"); // read device manufacturer @@ -153,7 +153,7 @@ private string GetNativeDirectoryPath() /// Start crashpad process to handle native Android crashes /// - private void HandleNativeCrashes(IDictionary backtraceAttributes) + private void HandleNativeCrashes(IDictionary backtraceAttributes, IEnumerable attachments) { // make sure database is enabled var integrationDisabled = @@ -207,7 +207,6 @@ private void HandleNativeCrashes(IDictionary backtraceAttributes } var minidumpUrl = new BacktraceCredentials(_configuration.GetValidServerUrl()).GetMinidumpSubmissionUrl().ToString(); - var attachments = _configuration.GetAttachmentPaths().ToArray(); // reassign to captureNativeCrashes // to avoid doing anything on crashpad binary, when crashpad @@ -218,7 +217,7 @@ private void HandleNativeCrashes(IDictionary backtraceAttributes AndroidJNI.NewStringUTF(crashpadHandlerPath), AndroidJNIHelper.ConvertToJNIArray(backtraceAttributes.Keys.ToArray()), AndroidJNIHelper.ConvertToJNIArray(backtraceAttributes.Values.ToArray()), - AndroidJNIHelper.ConvertToJNIArray(attachments)); + AndroidJNIHelper.ConvertToJNIArray(attachments.ToArray())); if (!_captureNativeCrashes) { Debug.LogWarning("Backtrace native integration status: Cannot initialize Crashpad client"); diff --git a/Runtime/Native/NativeClientFactory.cs b/Runtime/Native/NativeClientFactory.cs index 48512a23..c7863141 100644 --- a/Runtime/Native/NativeClientFactory.cs +++ b/Runtime/Native/NativeClientFactory.cs @@ -5,15 +5,15 @@ namespace Backtrace.Unity.Runtime.Native { internal static class NativeClientFactory { - internal static INativeClient CreateNativeClient(BacktraceConfiguration configuration, string gameObjectName, IDictionary attributes) + internal static INativeClient CreateNativeClient(BacktraceConfiguration configuration, string gameObjectName, IDictionary attributes, IEnumerable attachments) { #if UNITY_EDITOR return null; #else #if UNITY_ANDROID - return new Android.NativeClient(gameObjectName, configuration, attributes); + return new Android.NativeClient(gameObjectName, configuration, attributes, attachments); #elif UNITY_IOS - return new iOS.NativeClient(configuration, attributes); + return new iOS.NativeClient(configuration, attributes, attachments); #else return null; #endif diff --git a/Runtime/Native/iOS/NativeClient.cs b/Runtime/Native/iOS/NativeClient.cs index ab151829..acad41be 100644 --- a/Runtime/Native/iOS/NativeClient.cs +++ b/Runtime/Native/iOS/NativeClient.cs @@ -68,7 +68,7 @@ internal struct Entry #endif - public NativeClient(BacktraceConfiguration configuration, , IDictionary clientAttributes) + public NativeClient(BacktraceConfiguration configuration, IDictionary clientAttributes, IEnumerable attachments) { if (INITIALIZED || !_enabled) { @@ -76,7 +76,7 @@ public NativeClient(BacktraceConfiguration configuration, , IDictionary - private void HandleNativeCrashes(BacktraceConfiguration configuration, IDictionary attributes) + private void HandleNativeCrashes(BacktraceConfiguration configuration, IDictionary attributes, IEnumerable attachments) { var databasePath = configuration.GetFullDatabasePath(); // make sure database is enabled @@ -104,7 +104,6 @@ private void HandleNativeCrashes(BacktraceConfiguration configuration, IDictiona } var plcrashreporterUrl = new BacktraceCredentials(configuration.GetValidServerUrl()).GetPlCrashReporterSubmissionUrl(); - var backtraceAttributes = new Model.JsonData.BacktraceAttributes(null, null, true); // add exception.type attribute to PLCrashReporter reports // The library will send PLCrashReporter crashes to Backtrace @@ -112,9 +111,8 @@ private void HandleNativeCrashes(BacktraceConfiguration configuration, IDictiona attributes["error.type"] = "Crash"; var attributeKeys = attributes.Keys.ToArray(); var attributeValues = attributes.Values.ToArray(); - var attachments = configuration.GetAttachmentPaths().ToArray(); - Start(plcrashreporterUrl.ToString(), attributeKeys, attributeValues, attributeValues.Length, configuration.OomReports, attachments, attachments.Length); + Start(plcrashreporterUrl.ToString(), attributeKeys, attributeValues, attributeValues.Length, configuration.OomReports, attachments.ToArray(), attachments.Count()); } /// diff --git a/Tests/Runtime/Breadcrumbs/BreadcrumbsFileOperationTests.cs b/Tests/Runtime/Breadcrumbs/BreadcrumbsFileOperationTests.cs index 891f7883..eb8c57d6 100644 --- a/Tests/Runtime/Breadcrumbs/BreadcrumbsFileOperationTests.cs +++ b/Tests/Runtime/Breadcrumbs/BreadcrumbsFileOperationTests.cs @@ -9,6 +9,7 @@ using System.Linq; using System.Text; using UnityEngine; +using UnityEngine.TestTools; namespace Backtrace.Unity.Tests.Runtime.Breadcrumbs { @@ -105,6 +106,40 @@ public void TestFileLimit_ShouldCleanupTheSpace_SpaceWasCleaned() } + [Test] + public void TestBreadcrumbs_BasicBreadcrumbsTestForAllEvents_ShouldStoreEvents() + { + const int expectedNumberOfBreadcrumbs = 3; + string[] messages = new string[expectedNumberOfBreadcrumbs] { + "CustomUserBreadcrumb1", + "PlayerStarted", + "unhandled exception custom message from breadcrumbs test case" + }; + + var breadcrumb1Attributes = new Dictionary() { { "name", "CustomUserBreadcrumb1Value" } }; + + var breadcrumbFile = new InMemoryBreadcrumbFile(); + var breadcrumbsStorageManager = new BacktraceStorageLogManager(Application.temporaryCachePath) + { + BreadcrumbFile = breadcrumbFile + }; + var breadcrumbsManager = new BacktraceBreadcrumbs(breadcrumbsStorageManager); + var unityEngineLogLevel = UnityEngineLogLevel.Debug | UnityEngineLogLevel.Warning | UnityEngineLogLevel.Info | UnityEngineLogLevel.Error | UnityEngineLogLevel.Fatal; + + breadcrumbsManager.EnableBreadcrumbs(BacktraceBreadcrumbType.Manual | BacktraceBreadcrumbType.System, unityEngineLogLevel); + breadcrumbsManager.Warning(messages[0], breadcrumb1Attributes); + breadcrumbsManager.Info(messages[1]); + breadcrumbsManager.Exception(messages[2]); + + Assert.AreEqual(expectedNumberOfBreadcrumbs, breadcrumbsStorageManager.Length()); + Assert.AreEqual(expectedNumberOfBreadcrumbs, breadcrumbsStorageManager.BreadcrumbId()); + var breadcrumbs = ConvertToBreadcrumbs(breadcrumbFile); + for (int i = 0; i < expectedNumberOfBreadcrumbs; i++) + { + Assert.AreEqual(messages[i], breadcrumbs.ElementAt(i).Message); + } + } + private IEnumerable ConvertToBreadcrumbs(InMemoryBreadcrumbFile file) { return ConvertToBreadcrumbs(Encoding.UTF8.GetString(file.MemoryStream.ToArray())); From 6be6210e96b127c1f40bb3e3080efe8361ef0c99 Mon Sep 17 00:00:00 2001 From: kdysput Date: Mon, 10 May 2021 19:43:25 +0200 Subject: [PATCH 22/42] Safe native attributes --- Runtime/BacktraceDatabase.cs | 11 +++++++++-- Runtime/Native/Android/NativeClient.cs | 4 ++-- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/Runtime/BacktraceDatabase.cs b/Runtime/BacktraceDatabase.cs index d8694c3b..9044859f 100644 --- a/Runtime/BacktraceDatabase.cs +++ b/Runtime/BacktraceDatabase.cs @@ -536,8 +536,15 @@ protected virtual bool InitializeDatabasePaths() // handle situation when Backtrace plugin should create database directory if (!databaseDirExists && Configuration.CreateDatabase) { - var dirInfo = Directory.CreateDirectory(DatabasePath); - databaseDirExists = dirInfo.Exists; + try + { + var dirInfo = Directory.CreateDirectory(DatabasePath); + databaseDirExists = dirInfo.Exists; + } + catch (Exception) + { + return false; + } } if (!databaseDirExists) diff --git a/Runtime/Native/Android/NativeClient.cs b/Runtime/Native/Android/NativeClient.cs index 09c59152..5bb8cf21 100644 --- a/Runtime/Native/Android/NativeClient.cs +++ b/Runtime/Native/Android/NativeClient.cs @@ -249,7 +249,7 @@ public void GetAttributes(IDictionary result) // rewrite built in attributes to report attributes foreach (var builtInAttribute in _builtInAttributes) { - result.Add(builtInAttribute.Key, builtInAttribute.Value); + result[builtInAttribute.Key] = builtInAttribute.Value; } var processId = System.Diagnostics.Process.GetCurrentProcess().Id; @@ -278,7 +278,7 @@ public void GetAttributes(IDictionary result) { value = value.Substring(0, value.LastIndexOf("k")).Trim(); } - result.Add(key, value); + result[key] = value; } } } From 56c5a25ca79ebf878a86bcc13700a9d5256703f6 Mon Sep 17 00:00:00 2001 From: konraddysput Date: Mon, 10 May 2021 19:43:50 +0200 Subject: [PATCH 23/42] Safe native attribute ios --- Runtime/Native/iOS/NativeClient.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Runtime/Native/iOS/NativeClient.cs b/Runtime/Native/iOS/NativeClient.cs index acad41be..46d08919 100644 --- a/Runtime/Native/iOS/NativeClient.cs +++ b/Runtime/Native/iOS/NativeClient.cs @@ -131,7 +131,7 @@ public void GetAttributes(IDictionary result) { var address = pUnmanagedArray + i * 16; Entry entry = Marshal.PtrToStructure(address); - result.Add(entry.Key, entry.Value); + result[entry.Key] = entry.Value; } Marshal.FreeHGlobal(pUnmanagedArray); From 735e38532420f361e7854aadb8c4161ae1d9e090 Mon Sep 17 00:00:00 2001 From: konraddysput Date: Tue, 11 May 2021 13:03:57 +0200 Subject: [PATCH 24/42] Fixed timestamp comparision --- Tests/Runtime/Breadcrumbs/BreadcrumbsFileOperationTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/Runtime/Breadcrumbs/BreadcrumbsFileOperationTests.cs b/Tests/Runtime/Breadcrumbs/BreadcrumbsFileOperationTests.cs index eb8c57d6..d86519da 100644 --- a/Tests/Runtime/Breadcrumbs/BreadcrumbsFileOperationTests.cs +++ b/Tests/Runtime/Breadcrumbs/BreadcrumbsFileOperationTests.cs @@ -67,7 +67,7 @@ public void TestFileCreation_AddNewBreadcrumbToFile_SuccessfullyAddedBreadcrumb( Assert.AreEqual(unityEngineLogLevel, breadcrumb.Level); Assert.AreEqual(breadcrumbMessage, breadcrumb.Message); // round timestamp because timestamp value in the final json will reduce decimal part. - Assert.That(currentTime, Is.LessThanOrEqualTo(Math.Round(breadcrumb.Timestamp, 0))); + Assert.That(Math.Round(currentTime, 0), Is.LessThanOrEqualTo(Math.Round(breadcrumb.Timestamp, 0))); } [Test] From cabadc05fac7304497bedbb3e2ff60633a69b06b Mon Sep 17 00:00:00 2001 From: kdysput Date: Wed, 12 May 2021 16:32:02 +0200 Subject: [PATCH 25/42] Fixed missing namespace + line endings --- Runtime/BacktraceDatabase.cs | 34 +++++++++--------- .../IBacktraceDatabaseFileContext.cs | 36 +++++++++---------- Runtime/Model/BacktraceConfiguration.cs | 1 + 3 files changed, 36 insertions(+), 35 deletions(-) diff --git a/Runtime/BacktraceDatabase.cs b/Runtime/BacktraceDatabase.cs index 9044859f..4aa609c0 100644 --- a/Runtime/BacktraceDatabase.cs +++ b/Runtime/BacktraceDatabase.cs @@ -2,7 +2,7 @@ using Backtrace.Unity.Interfaces; using Backtrace.Unity.Model; using Backtrace.Unity.Model.Breadcrumbs; -using Backtrace.Unity.Model.Breadcrumbs.Storage; +using Backtrace.Unity.Model.Breadcrumbs.Storage; using Backtrace.Unity.Model.Database; using Backtrace.Unity.Services; using Backtrace.Unity.Types; @@ -216,8 +216,8 @@ private void Start() RemoveOrphaned(); if (DatabaseSettings.AutoSendMode) { - _lastConnection = Time.unscaledTime; - SendData(BacktraceDatabaseContext.FirstOrDefault()); + _lastConnection = Time.unscaledTime; + SendData(BacktraceDatabaseContext.FirstOrDefault()); } } @@ -433,9 +433,9 @@ record = BacktraceDatabaseContext.FirstOrDefault(); private void SendData(BacktraceDatabaseRecord record) { - if (record == null) - { - return; + if (record == null) + { + return; } var stopWatch = Configuration.PerformanceStatistics ? System.Diagnostics.Stopwatch.StartNew() @@ -536,14 +536,14 @@ protected virtual bool InitializeDatabasePaths() // handle situation when Backtrace plugin should create database directory if (!databaseDirExists && Configuration.CreateDatabase) { - try - { - var dirInfo = Directory.CreateDirectory(DatabasePath); - databaseDirExists = dirInfo.Exists; - } - catch (Exception) - { - return false; + try + { + var dirInfo = Directory.CreateDirectory(DatabasePath); + databaseDirExists = dirInfo.Exists; + } + catch (Exception) + { + return false; } } @@ -574,9 +574,9 @@ protected virtual void LoadReports() continue; } if (!BacktraceDatabaseFileContext.IsValidRecord(record)) - { - Debug.Log("Removing record from Backtrace Database path - invalid record."); - BacktraceDatabaseFileContext.Delete(record); + { + Debug.Log("Removing record from Backtrace Database path - invalid record."); + BacktraceDatabaseFileContext.Delete(record); continue; } BacktraceDatabaseContext.Add(record); diff --git a/Runtime/Interfaces/IBacktraceDatabaseFileContext.cs b/Runtime/Interfaces/IBacktraceDatabaseFileContext.cs index 24f045b1..63cfdbac 100644 --- a/Runtime/Interfaces/IBacktraceDatabaseFileContext.cs +++ b/Runtime/Interfaces/IBacktraceDatabaseFileContext.cs @@ -1,4 +1,4 @@ -using Backtrace.Unity.Model; +using Backtrace.Unity.Model; using Backtrace.Unity.Model.Database; using System.Collections.Generic; using System.IO; @@ -35,29 +35,29 @@ internal interface IBacktraceDatabaseFileContext /// void Clear(); - /// - /// Deletes backtrace database record from persistent data storage - /// + /// + /// Deletes backtrace database record from persistent data storage + /// /// Database record void Delete(BacktraceDatabaseRecord record); - /// - /// Generates list of attachments for current diagnostic data record - /// + /// + /// Generates list of attachments for current diagnostic data record + /// /// Backtrace data IEnumerable GenerateRecordAttachments(BacktraceData data); - /// - /// Saves BacktraceDatabaseRerord on the hard drive - /// - /// BacktraceDatabaseRecord - /// true if file context was able to save data on the hard drive. Otherwise false - bool Save(BacktraceDatabaseRecord record); - - /// - /// Determine if BacktraceDatabaseRecord is valid. - /// - /// Database record + /// + /// Saves BacktraceDatabaseRerord on the hard drive + /// + /// BacktraceDatabaseRecord + /// true if file context was able to save data on the hard drive. Otherwise false + bool Save(BacktraceDatabaseRecord record); + + /// + /// Determine if BacktraceDatabaseRecord is valid. + /// + /// Database record /// True, if the record exists. Otherwise false. bool IsValidRecord(BacktraceDatabaseRecord record); } diff --git a/Runtime/Model/BacktraceConfiguration.cs b/Runtime/Model/BacktraceConfiguration.cs index 21b8fabe..7ab32bc9 100644 --- a/Runtime/Model/BacktraceConfiguration.cs +++ b/Runtime/Model/BacktraceConfiguration.cs @@ -1,4 +1,5 @@ using Backtrace.Unity.Common; +using Backtrace.Unity.Model.Breadcrumbs; using Backtrace.Unity.Services; using Backtrace.Unity.Types; using System; From e6455f07dde4ea279b78e0f96948786b9e8bbe70 Mon Sep 17 00:00:00 2001 From: kdysput Date: Wed, 12 May 2021 16:38:23 +0200 Subject: [PATCH 26/42] API update + removed two low memory warnings --- Runtime/BacktraceClient.cs | 2 -- .../Model/Breadcrumbs/BacktraceBreadcrumbs.cs | 30 +++++++++---------- .../Breadcrumbs/IBacktraceBreadcrumbs.cs | 6 ++-- .../BacktraceBreadcrumbsTypeTests.cs | 2 +- .../BreadcrumbsFileOperationTests.cs | 8 ++--- .../Breadcrumbs/BreadcrumbsLogLevelTests.cs | 6 ++-- 6 files changed, 26 insertions(+), 28 deletions(-) diff --git a/Runtime/BacktraceClient.cs b/Runtime/BacktraceClient.cs index 74f78326..f8448c66 100644 --- a/Runtime/BacktraceClient.cs +++ b/Runtime/BacktraceClient.cs @@ -883,8 +883,6 @@ internal void HandleLowMemory() _nativeClient.OnOOM(); } - const string lowMemoryMessage = "Low memory warning detected."; - Breadcrumbs?.FromMonoBehavior(lowMemoryMessage, LogType.Warning, null); } #endif diff --git a/Runtime/Model/Breadcrumbs/BacktraceBreadcrumbs.cs b/Runtime/Model/Breadcrumbs/BacktraceBreadcrumbs.cs index 0d439015..45b07033 100644 --- a/Runtime/Model/Breadcrumbs/BacktraceBreadcrumbs.cs +++ b/Runtime/Model/Breadcrumbs/BacktraceBreadcrumbs.cs @@ -44,19 +44,19 @@ public bool ClearBreadcrumbs() } - public bool AddBreadcrumbs(string message, LogType type) + public bool AddBreadcrumb(string message, LogType type) { - return AddBreadcrumbs(message, type, null); + return AddBreadcrumb(message, type, null); } - public bool AddBreadcrumbs(string message) + public bool AddBreadcrumb(string message) { - return AddBreadcrumbs(message, LogType.Log); + return AddBreadcrumb(message, LogType.Log); } public bool Debug(string message) { - return AddBreadcrumbs(message, LogType.Assert); + return AddBreadcrumb(message, LogType.Assert); } public bool EnableBreadcrumbs(BacktraceBreadcrumbType level, UnityEngineLogLevel unityLogLevel) @@ -79,12 +79,12 @@ public bool EnableBreadcrumbs(BacktraceBreadcrumbType level, UnityEngineLogLevel public bool Exception(Exception exception) { - return AddBreadcrumbs(exception.Message, LogType.Error, null); + return AddBreadcrumb(exception.Message, LogType.Error, null); } public bool Exception(string message) { - return AddBreadcrumbs(message, LogType.Error, null); + return AddBreadcrumb(message, LogType.Error, null); } public bool FromBacktrace(BacktraceReport report) @@ -113,39 +113,39 @@ public string GetBreadcrumbLogPath() public bool Info(string message) { - return AddBreadcrumbs(message, LogType.Log, null); + return AddBreadcrumb(message, LogType.Log, null); } public bool Warning(string message) { - return AddBreadcrumbs(message, LogType.Warning, null); + return AddBreadcrumb(message, LogType.Warning, null); } public bool Debug(string message, IDictionary attributes) { - return AddBreadcrumbs(message, LogType.Assert, attributes); + return AddBreadcrumb(message, LogType.Assert, attributes); } public bool Info(string message, IDictionary attributes) { - return AddBreadcrumbs(message, LogType.Assert, attributes); + return AddBreadcrumb(message, LogType.Assert, attributes); } public bool Warning(string message, IDictionary attributes) { - return AddBreadcrumbs(message, LogType.Warning, attributes); + return AddBreadcrumb(message, LogType.Warning, attributes); } public bool Exception(Exception exception, IDictionary attributes) { - return AddBreadcrumbs(exception.Message, LogType.Exception, attributes); + return AddBreadcrumb(exception.Message, LogType.Exception, attributes); } public bool Exception(string message, IDictionary attributes) { - return AddBreadcrumbs(message, LogType.Exception, attributes); + return AddBreadcrumb(message, LogType.Exception, attributes); } - public bool AddBreadcrumbs(string message, LogType logType, IDictionary attributes) + public bool AddBreadcrumb(string message, LogType logType, IDictionary attributes) { var type = ConvertLogTypeToLogLevel(logType); if (!ShouldLog(type)) diff --git a/Runtime/Model/Breadcrumbs/IBacktraceBreadcrumbs.cs b/Runtime/Model/Breadcrumbs/IBacktraceBreadcrumbs.cs index fa8f8b1c..6e2f3b72 100644 --- a/Runtime/Model/Breadcrumbs/IBacktraceBreadcrumbs.cs +++ b/Runtime/Model/Breadcrumbs/IBacktraceBreadcrumbs.cs @@ -9,9 +9,9 @@ public interface IBacktraceBreadcrumbs BacktraceBreadcrumbType BreadcrumbsLevel { get; } bool EnableBreadcrumbs(BacktraceBreadcrumbType level, UnityEngineLogLevel unityLogLevel); bool ClearBreadcrumbs(); - bool AddBreadcrumbs(string message, LogType type, IDictionary attributes); - bool AddBreadcrumbs(string message, LogType type); - bool AddBreadcrumbs(string message); + bool AddBreadcrumb(string message, LogType type, IDictionary attributes); + bool AddBreadcrumb(string message, LogType type); + bool AddBreadcrumb(string message); bool Debug(string message); bool Debug(string message, IDictionary attributes); bool Info(string message); diff --git a/Tests/Runtime/Breadcrumbs/BacktraceBreadcrumbsTypeTests.cs b/Tests/Runtime/Breadcrumbs/BacktraceBreadcrumbsTypeTests.cs index 8340b27d..9301cace 100644 --- a/Tests/Runtime/Breadcrumbs/BacktraceBreadcrumbsTypeTests.cs +++ b/Tests/Runtime/Breadcrumbs/BacktraceBreadcrumbsTypeTests.cs @@ -23,7 +23,7 @@ public void TestManualLogs_ShouldFilterAllManualLogs_BreadcrumbsWasntSaved(LogTy UnityEngineLogLevel level = UnityEngineLogLevel.Debug | UnityEngineLogLevel.Error | UnityEngineLogLevel.Fatal | UnityEngineLogLevel.Info | UnityEngineLogLevel.Warning; breadcrumbsManager.EnableBreadcrumbs(breadcrumbType, level); - var result = breadcrumbsManager.AddBreadcrumbs(message, testedLevel); + var result = breadcrumbsManager.AddBreadcrumb(message, testedLevel); Assert.IsFalse(result); Assert.AreEqual(expectedNumberOfLogs, inMemoryBreadcrumbStorage.Breadcrumbs.Count); diff --git a/Tests/Runtime/Breadcrumbs/BreadcrumbsFileOperationTests.cs b/Tests/Runtime/Breadcrumbs/BreadcrumbsFileOperationTests.cs index d86519da..3af26c79 100644 --- a/Tests/Runtime/Breadcrumbs/BreadcrumbsFileOperationTests.cs +++ b/Tests/Runtime/Breadcrumbs/BreadcrumbsFileOperationTests.cs @@ -57,7 +57,7 @@ public void TestFileCreation_AddNewBreadcrumbToFile_SuccessfullyAddedBreadcrumb( .First(n => n == unityEngineLogLevel); breadcrumbsManager.EnableBreadcrumbs(ManualBreadcrumbsType, logTypeThatUnsupportCurrentTestCase); - var added = breadcrumbsManager.AddBreadcrumbs(breadcrumbMessage, testedLevel); + var added = breadcrumbsManager.AddBreadcrumb(breadcrumbMessage, testedLevel); Assert.IsTrue(added); var data = ConvertToBreadcrumbs(breadcrumbFile); @@ -86,16 +86,16 @@ public void TestFileLimit_ShouldCleanupTheSpace_SpaceWasCleaned() breadcrumbsManager.EnableBreadcrumbs(ManualBreadcrumbsType, unityEngineLogLevel); int numberOfAddedBreadcrumbs = 1; - breadcrumbsManager.AddBreadcrumbs(breadcrumbMessage, LogType.Assert); + breadcrumbsManager.AddBreadcrumb(breadcrumbMessage, LogType.Assert); var breadcrumbSize = breadcrumbFile.Size - 2; while (breadcrumbFile.Size + breadcrumbSize < minimalSize != false) { - breadcrumbsManager.AddBreadcrumbs(breadcrumbMessage, LogType.Assert); + breadcrumbsManager.AddBreadcrumb(breadcrumbMessage, LogType.Assert); numberOfAddedBreadcrumbs++; } var sizeBeforeCleanup = breadcrumbFile.Size; var numberOfBreadcurmbsBeforeCleanUp = numberOfAddedBreadcrumbs; - breadcrumbsManager.AddBreadcrumbs(breadcrumbMessage, LogType.Assert); + breadcrumbsManager.AddBreadcrumb(breadcrumbMessage, LogType.Assert); numberOfAddedBreadcrumbs++; Assert.That(breadcrumbFile.Size, Is.LessThan(sizeBeforeCleanup)); diff --git a/Tests/Runtime/Breadcrumbs/BreadcrumbsLogLevelTests.cs b/Tests/Runtime/Breadcrumbs/BreadcrumbsLogLevelTests.cs index 6b03fd5e..1f981dfe 100644 --- a/Tests/Runtime/Breadcrumbs/BreadcrumbsLogLevelTests.cs +++ b/Tests/Runtime/Breadcrumbs/BreadcrumbsLogLevelTests.cs @@ -30,7 +30,7 @@ public void TestLogLevel_ShouldFilterLogLevel_BreadcrumbIsNotAvailable(LogType t breadcrumbsManager.EnableBreadcrumbs(ManualBreadcrumbsType, logTypeThatUnsupportCurrentTestCase); - var result = breadcrumbsManager.AddBreadcrumbs(message, testedLevel); + var result = breadcrumbsManager.AddBreadcrumb(message, testedLevel); Assert.IsFalse(result); Assert.AreEqual(expectedNumberOfLogs, inMemoryBreadcrumbStorage.Breadcrumbs.Count); @@ -53,7 +53,7 @@ public void TestLogLevel_ShouldtFilterLogLevel_BreadcrumbIsAvailable(LogType tes .First(n => n == unityEngineLogLevel); breadcrumbsManager.EnableBreadcrumbs(ManualBreadcrumbsType, logTypeThatUnsupportCurrentTestCase); - var result = breadcrumbsManager.AddBreadcrumbs(message, testedLevel); + var result = breadcrumbsManager.AddBreadcrumb(message, testedLevel); Assert.IsTrue(result); Assert.AreEqual(expectedNumberOfLogs, inMemoryBreadcrumbStorage.Breadcrumbs.Count); @@ -83,7 +83,7 @@ public void TestLogLevel_ShouldtFilterLogLevelWithAttributes_BreadcrumbIsAvailab .First(n => n == unityEngineLogLevel); breadcrumbsManager.EnableBreadcrumbs(ManualBreadcrumbsType, logTypeThatUnsupportCurrentTestCase); - var result = breadcrumbsManager.AddBreadcrumbs(message, testedLevel, new Dictionary() { { attributeName, attributeValue } }); + var result = breadcrumbsManager.AddBreadcrumb(message, testedLevel, new Dictionary() { { attributeName, attributeValue } }); Assert.IsTrue(result); Assert.AreEqual(expectedNumberOfLogs, inMemoryBreadcrumbStorage.Breadcrumbs.Count); From 316afb270566fdb57f31f64856d8f547b395af9b Mon Sep 17 00:00:00 2001 From: kdysput Date: Wed, 12 May 2021 17:32:18 +0200 Subject: [PATCH 27/42] breadcrumbs adjustements --- .../Model/Breadcrumbs/BacktraceBreadcrumbs.cs | 3 +- .../BacktraceBreadcrumbsEventHandler.cs | 77 ++++++++++++------- Runtime/Services/BacktraceApi.cs | 2 +- 3 files changed, 50 insertions(+), 32 deletions(-) diff --git a/Runtime/Model/Breadcrumbs/BacktraceBreadcrumbs.cs b/Runtime/Model/Breadcrumbs/BacktraceBreadcrumbs.cs index 45b07033..d924c1ad 100644 --- a/Runtime/Model/Breadcrumbs/BacktraceBreadcrumbs.cs +++ b/Runtime/Model/Breadcrumbs/BacktraceBreadcrumbs.cs @@ -177,9 +177,8 @@ internal UnityEngineLogLevel ConvertLogTypeToLogLevel(LogType type) { case LogType.Warning: return UnityEngineLogLevel.Warning; - case LogType.Exception: - return UnityEngineLogLevel.Fatal; case LogType.Error: + case LogType.Exception: return UnityEngineLogLevel.Error; case LogType.Assert: return UnityEngineLogLevel.Debug; diff --git a/Runtime/Model/Breadcrumbs/BacktraceBreadcrumbsEventHandler.cs b/Runtime/Model/Breadcrumbs/BacktraceBreadcrumbsEventHandler.cs index 4c360c2d..749f2254 100644 --- a/Runtime/Model/Breadcrumbs/BacktraceBreadcrumbsEventHandler.cs +++ b/Runtime/Model/Breadcrumbs/BacktraceBreadcrumbsEventHandler.cs @@ -23,20 +23,28 @@ public BacktraceBreadcrumbsEventHandler(BacktraceBreadcrumbs breadcrumbs) public void Register(BacktraceBreadcrumbType level) { _registeredLevel = level; - HasRegisteredEvents = level.HasFlag(BacktraceBreadcrumbType.System); - if (!HasRegisteredEvents) + if (_registeredLevel.HasFlag(BacktraceBreadcrumbType.Navigation)) { + HasRegisteredEvents = true; + SceneManager.activeSceneChanged += HandleSceneChanged; + SceneManager.sceneLoaded += SceneManager_sceneLoaded; + SceneManager.sceneUnloaded += SceneManager_sceneUnloaded; + } - return; + if (_registeredLevel.HasFlag(BacktraceBreadcrumbType.System)) + { + HasRegisteredEvents = true; + Application.lowMemory += HandleLowMemory; + Application.quitting += HandleApplicationQuitting; + Application.focusChanged += Application_focusChanged; + } + + if (_registeredLevel.HasFlag(BacktraceBreadcrumbType.Log)) + { + HasRegisteredEvents = true; + Application.logMessageReceived += HandleMessage; + Application.logMessageReceivedThreaded += HandleBackgroundMessage; } - SceneManager.activeSceneChanged += HandleSceneChanged; - SceneManager.sceneLoaded += SceneManager_sceneLoaded; - SceneManager.sceneUnloaded += SceneManager_sceneUnloaded; - Application.lowMemory += HandleLowMemory; - Application.quitting += HandleApplicationQuitting; - Application.focusChanged += Application_focusChanged; - Application.logMessageReceived += HandleMessage; - Application.logMessageReceivedThreaded += HandleBackgroundMessage; } /// @@ -44,46 +52,57 @@ public void Register(BacktraceBreadcrumbType level) /// public void Unregister() { - if (HasRegisteredEvents) + if (HasRegisteredEvents == false) { return; } - SceneManager.activeSceneChanged -= HandleSceneChanged; - SceneManager.sceneLoaded -= SceneManager_sceneLoaded; - SceneManager.sceneUnloaded -= SceneManager_sceneUnloaded; - Application.lowMemory -= HandleLowMemory; - Application.quitting -= HandleApplicationQuitting; - Application.logMessageReceived -= HandleMessage; - Application.logMessageReceivedThreaded -= HandleBackgroundMessage; - Application.focusChanged -= Application_focusChanged; + if (_registeredLevel.HasFlag(BacktraceBreadcrumbType.Navigation)) + { + SceneManager.activeSceneChanged -= HandleSceneChanged; + SceneManager.sceneLoaded -= SceneManager_sceneLoaded; + SceneManager.sceneUnloaded -= SceneManager_sceneUnloaded; + } + + if (_registeredLevel.HasFlag(BacktraceBreadcrumbType.System)) + { + Application.lowMemory -= HandleLowMemory; + Application.quitting -= HandleApplicationQuitting; + Application.focusChanged -= Application_focusChanged; + } + + if (_registeredLevel.HasFlag(BacktraceBreadcrumbType.Log)) + { + Application.logMessageReceived -= HandleMessage; + Application.logMessageReceivedThreaded -= HandleBackgroundMessage; + } } private void SceneManager_sceneUnloaded(Scene scene) { var message = string.Format("SceneManager:scene {0} unloaded", scene.name); - Log(message, LogType.Assert); + Log(message, LogType.Assert, BreadcrumbLevel.Navigation); } private void SceneManager_sceneLoaded(Scene scene, LoadSceneMode loadSceneMode) { var message = string.Format("SceneManager:scene {0} loaded", scene.name); - Log(message, LogType.Assert, new Dictionary() { { "LoadSceneMode", loadSceneMode.ToString() } }); + Log(message, LogType.Assert, BreadcrumbLevel.Navigation, new Dictionary() { { "LoadSceneMode", loadSceneMode.ToString() } }); } private void HandleSceneChanged(Scene sceneFrom, Scene sceneTo) { var message = string.Format("SceneManager:scene changed from {0} to {1}", sceneFrom.name, sceneTo.name); - Log(message, LogType.Assert); + Log(message, LogType.Assert, BreadcrumbLevel.Navigation, new Dictionary() { { "from", sceneFrom.name }, { "to", sceneTo.name } }); } private void HandleLowMemory() { - Log("Application:low memory", LogType.Warning); + Log("Application:low memory", LogType.Warning, BreadcrumbLevel.System); } private void HandleApplicationQuitting() { - Log("Application:quitting", LogType.Log); + Log("Application:quitting", LogType.Log, BreadcrumbLevel.System); } private void HandleBackgroundMessage(string condition, string stackTrace, LogType type) @@ -102,22 +121,22 @@ private void HandleMessage(string condition, string stackTrace, LogType type) var attributes = type == LogType.Error || type == LogType.Exception ? new Dictionary { { "stackTrace", stackTrace } } : null; - Log(condition, type, attributes); + Log(condition, type, BreadcrumbLevel.Log, attributes); } private void Application_focusChanged(bool hasFocus) { - Log("Application:focus changed.", LogType.Assert, new Dictionary { { "hasFocus", hasFocus.ToString() } }); + Log("Application:focus changed.", LogType.Assert, BreadcrumbLevel.System, new Dictionary { { "hasFocus", hasFocus.ToString() } }); } - private void Log(string message, LogType level, IDictionary attributes = null) + private void Log(string message, LogType level, BreadcrumbLevel breadcrumbLevel, IDictionary attributes = null) { var type = _breadcrumbs.ConvertLogTypeToLogLevel(level); if (!_breadcrumbs.ShouldLog(type)) { return; } - _breadcrumbs.AddBreadcrumbs(message, BreadcrumbLevel.System, type, attributes); + _breadcrumbs.AddBreadcrumbs(message, breadcrumbLevel, type, attributes); } } } diff --git a/Runtime/Services/BacktraceApi.cs b/Runtime/Services/BacktraceApi.cs index a0cb683c..434ea3dd 100644 --- a/Runtime/Services/BacktraceApi.cs +++ b/Runtime/Services/BacktraceApi.cs @@ -243,7 +243,7 @@ public IEnumerator Send(string json, List attachments, Dictionary Date: Wed, 12 May 2021 17:35:25 +0200 Subject: [PATCH 28/42] Unit tests that validates navigation and logs events --- .../BacktraceBreadcrumbsTypeTests.cs | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/Tests/Runtime/Breadcrumbs/BacktraceBreadcrumbsTypeTests.cs b/Tests/Runtime/Breadcrumbs/BacktraceBreadcrumbsTypeTests.cs index 9301cace..d103edcf 100644 --- a/Tests/Runtime/Breadcrumbs/BacktraceBreadcrumbsTypeTests.cs +++ b/Tests/Runtime/Breadcrumbs/BacktraceBreadcrumbsTypeTests.cs @@ -42,5 +42,31 @@ public void TestSystemLogs_ShouldEnableThem_EventsAreSet() breadcrumbsManager.UnregisterEvents(); } + [Test] + public void TestNavigationLogs_ShouldEnableThem_EventsAreSet() + { + var inMemoryBreadcrumbStorage = new BacktraceInMemoryLogManager(); + var breadcrumbsManager = new BacktraceBreadcrumbs(inMemoryBreadcrumbStorage); + UnityEngineLogLevel level = UnityEngineLogLevel.Debug | UnityEngineLogLevel.Error | UnityEngineLogLevel.Fatal | UnityEngineLogLevel.Info | UnityEngineLogLevel.Warning; + + breadcrumbsManager.EnableBreadcrumbs(BacktraceBreadcrumbType.Navigation, level); + + Assert.IsTrue(breadcrumbsManager.EventHandler.HasRegisteredEvents); + breadcrumbsManager.UnregisterEvents(); + } + + [Test] + public void TestLogLogs_ShouldEnableThem_EventsAreSet() + { + var inMemoryBreadcrumbStorage = new BacktraceInMemoryLogManager(); + var breadcrumbsManager = new BacktraceBreadcrumbs(inMemoryBreadcrumbStorage); + UnityEngineLogLevel level = UnityEngineLogLevel.Debug | UnityEngineLogLevel.Error | UnityEngineLogLevel.Fatal | UnityEngineLogLevel.Info | UnityEngineLogLevel.Warning; + + breadcrumbsManager.EnableBreadcrumbs(BacktraceBreadcrumbType.Log, level); + + Assert.IsTrue(breadcrumbsManager.EventHandler.HasRegisteredEvents); + breadcrumbsManager.UnregisterEvents(); + } + } } From b046da6f1c55e1a42c3bd669c582c994d8d00ee5 Mon Sep 17 00:00:00 2001 From: kdysput Date: Thu, 13 May 2021 16:22:21 +0200 Subject: [PATCH 29/42] Renamed AddBreadcrumb to Log --- .../Model/Breadcrumbs/BacktraceBreadcrumbs.cs | 31 ++++++++----------- .../Breadcrumbs/IBacktraceBreadcrumbs.cs | 6 ++-- .../BacktraceBreadcrumbsTypeTests.cs | 2 +- .../BreadcrumbsFileOperationTests.cs | 8 ++--- .../Breadcrumbs/BreadcrumbsLogLevelTests.cs | 6 ++-- 5 files changed, 23 insertions(+), 30 deletions(-) diff --git a/Runtime/Model/Breadcrumbs/BacktraceBreadcrumbs.cs b/Runtime/Model/Breadcrumbs/BacktraceBreadcrumbs.cs index d924c1ad..598f0950 100644 --- a/Runtime/Model/Breadcrumbs/BacktraceBreadcrumbs.cs +++ b/Runtime/Model/Breadcrumbs/BacktraceBreadcrumbs.cs @@ -44,19 +44,14 @@ public bool ClearBreadcrumbs() } - public bool AddBreadcrumb(string message, LogType type) + public bool Log(string message, LogType type) { - return AddBreadcrumb(message, type, null); - } - - public bool AddBreadcrumb(string message) - { - return AddBreadcrumb(message, LogType.Log); + return Log(message, type, null); } public bool Debug(string message) { - return AddBreadcrumb(message, LogType.Assert); + return Log(message, LogType.Assert); } public bool EnableBreadcrumbs(BacktraceBreadcrumbType level, UnityEngineLogLevel unityLogLevel) @@ -79,12 +74,12 @@ public bool EnableBreadcrumbs(BacktraceBreadcrumbType level, UnityEngineLogLevel public bool Exception(Exception exception) { - return AddBreadcrumb(exception.Message, LogType.Error, null); + return Log(exception.Message, LogType.Error, null); } public bool Exception(string message) { - return AddBreadcrumb(message, LogType.Error, null); + return Log(message, LogType.Error, null); } public bool FromBacktrace(BacktraceReport report) @@ -113,39 +108,39 @@ public string GetBreadcrumbLogPath() public bool Info(string message) { - return AddBreadcrumb(message, LogType.Log, null); + return Log(message, LogType.Log, null); } public bool Warning(string message) { - return AddBreadcrumb(message, LogType.Warning, null); + return Log(message, LogType.Warning, null); } public bool Debug(string message, IDictionary attributes) { - return AddBreadcrumb(message, LogType.Assert, attributes); + return Log(message, LogType.Assert, attributes); } public bool Info(string message, IDictionary attributes) { - return AddBreadcrumb(message, LogType.Assert, attributes); + return Log(message, LogType.Assert, attributes); } public bool Warning(string message, IDictionary attributes) { - return AddBreadcrumb(message, LogType.Warning, attributes); + return Log(message, LogType.Warning, attributes); } public bool Exception(Exception exception, IDictionary attributes) { - return AddBreadcrumb(exception.Message, LogType.Exception, attributes); + return Log(exception.Message, LogType.Exception, attributes); } public bool Exception(string message, IDictionary attributes) { - return AddBreadcrumb(message, LogType.Exception, attributes); + return Log(message, LogType.Exception, attributes); } - public bool AddBreadcrumb(string message, LogType logType, IDictionary attributes) + public bool Log(string message, LogType logType, IDictionary attributes) { var type = ConvertLogTypeToLogLevel(logType); if (!ShouldLog(type)) diff --git a/Runtime/Model/Breadcrumbs/IBacktraceBreadcrumbs.cs b/Runtime/Model/Breadcrumbs/IBacktraceBreadcrumbs.cs index 6e2f3b72..cddf546c 100644 --- a/Runtime/Model/Breadcrumbs/IBacktraceBreadcrumbs.cs +++ b/Runtime/Model/Breadcrumbs/IBacktraceBreadcrumbs.cs @@ -9,9 +9,8 @@ public interface IBacktraceBreadcrumbs BacktraceBreadcrumbType BreadcrumbsLevel { get; } bool EnableBreadcrumbs(BacktraceBreadcrumbType level, UnityEngineLogLevel unityLogLevel); bool ClearBreadcrumbs(); - bool AddBreadcrumb(string message, LogType type, IDictionary attributes); - bool AddBreadcrumb(string message, LogType type); - bool AddBreadcrumb(string message); + bool Log(string message, LogType type, IDictionary attributes); + bool Log(string message, LogType type); bool Debug(string message); bool Debug(string message, IDictionary attributes); bool Info(string message); @@ -26,6 +25,5 @@ public interface IBacktraceBreadcrumbs bool FromMonoBehavior(string message, LogType type, IDictionary attributes); string GetBreadcrumbLogPath(); void UnregisterEvents(); - } } diff --git a/Tests/Runtime/Breadcrumbs/BacktraceBreadcrumbsTypeTests.cs b/Tests/Runtime/Breadcrumbs/BacktraceBreadcrumbsTypeTests.cs index d103edcf..d34bc23a 100644 --- a/Tests/Runtime/Breadcrumbs/BacktraceBreadcrumbsTypeTests.cs +++ b/Tests/Runtime/Breadcrumbs/BacktraceBreadcrumbsTypeTests.cs @@ -23,7 +23,7 @@ public void TestManualLogs_ShouldFilterAllManualLogs_BreadcrumbsWasntSaved(LogTy UnityEngineLogLevel level = UnityEngineLogLevel.Debug | UnityEngineLogLevel.Error | UnityEngineLogLevel.Fatal | UnityEngineLogLevel.Info | UnityEngineLogLevel.Warning; breadcrumbsManager.EnableBreadcrumbs(breadcrumbType, level); - var result = breadcrumbsManager.AddBreadcrumb(message, testedLevel); + var result = breadcrumbsManager.Log(message, testedLevel); Assert.IsFalse(result); Assert.AreEqual(expectedNumberOfLogs, inMemoryBreadcrumbStorage.Breadcrumbs.Count); diff --git a/Tests/Runtime/Breadcrumbs/BreadcrumbsFileOperationTests.cs b/Tests/Runtime/Breadcrumbs/BreadcrumbsFileOperationTests.cs index 3af26c79..dc029928 100644 --- a/Tests/Runtime/Breadcrumbs/BreadcrumbsFileOperationTests.cs +++ b/Tests/Runtime/Breadcrumbs/BreadcrumbsFileOperationTests.cs @@ -57,7 +57,7 @@ public void TestFileCreation_AddNewBreadcrumbToFile_SuccessfullyAddedBreadcrumb( .First(n => n == unityEngineLogLevel); breadcrumbsManager.EnableBreadcrumbs(ManualBreadcrumbsType, logTypeThatUnsupportCurrentTestCase); - var added = breadcrumbsManager.AddBreadcrumb(breadcrumbMessage, testedLevel); + var added = breadcrumbsManager.Log(breadcrumbMessage, testedLevel); Assert.IsTrue(added); var data = ConvertToBreadcrumbs(breadcrumbFile); @@ -86,16 +86,16 @@ public void TestFileLimit_ShouldCleanupTheSpace_SpaceWasCleaned() breadcrumbsManager.EnableBreadcrumbs(ManualBreadcrumbsType, unityEngineLogLevel); int numberOfAddedBreadcrumbs = 1; - breadcrumbsManager.AddBreadcrumb(breadcrumbMessage, LogType.Assert); + breadcrumbsManager.Log(breadcrumbMessage, LogType.Assert); var breadcrumbSize = breadcrumbFile.Size - 2; while (breadcrumbFile.Size + breadcrumbSize < minimalSize != false) { - breadcrumbsManager.AddBreadcrumb(breadcrumbMessage, LogType.Assert); + breadcrumbsManager.Log(breadcrumbMessage, LogType.Assert); numberOfAddedBreadcrumbs++; } var sizeBeforeCleanup = breadcrumbFile.Size; var numberOfBreadcurmbsBeforeCleanUp = numberOfAddedBreadcrumbs; - breadcrumbsManager.AddBreadcrumb(breadcrumbMessage, LogType.Assert); + breadcrumbsManager.Log(breadcrumbMessage, LogType.Assert); numberOfAddedBreadcrumbs++; Assert.That(breadcrumbFile.Size, Is.LessThan(sizeBeforeCleanup)); diff --git a/Tests/Runtime/Breadcrumbs/BreadcrumbsLogLevelTests.cs b/Tests/Runtime/Breadcrumbs/BreadcrumbsLogLevelTests.cs index 1f981dfe..ac1810dc 100644 --- a/Tests/Runtime/Breadcrumbs/BreadcrumbsLogLevelTests.cs +++ b/Tests/Runtime/Breadcrumbs/BreadcrumbsLogLevelTests.cs @@ -30,7 +30,7 @@ public void TestLogLevel_ShouldFilterLogLevel_BreadcrumbIsNotAvailable(LogType t breadcrumbsManager.EnableBreadcrumbs(ManualBreadcrumbsType, logTypeThatUnsupportCurrentTestCase); - var result = breadcrumbsManager.AddBreadcrumb(message, testedLevel); + var result = breadcrumbsManager.Log(message, testedLevel); Assert.IsFalse(result); Assert.AreEqual(expectedNumberOfLogs, inMemoryBreadcrumbStorage.Breadcrumbs.Count); @@ -53,7 +53,7 @@ public void TestLogLevel_ShouldtFilterLogLevel_BreadcrumbIsAvailable(LogType tes .First(n => n == unityEngineLogLevel); breadcrumbsManager.EnableBreadcrumbs(ManualBreadcrumbsType, logTypeThatUnsupportCurrentTestCase); - var result = breadcrumbsManager.AddBreadcrumb(message, testedLevel); + var result = breadcrumbsManager.Log(message, testedLevel); Assert.IsTrue(result); Assert.AreEqual(expectedNumberOfLogs, inMemoryBreadcrumbStorage.Breadcrumbs.Count); @@ -83,7 +83,7 @@ public void TestLogLevel_ShouldtFilterLogLevelWithAttributes_BreadcrumbIsAvailab .First(n => n == unityEngineLogLevel); breadcrumbsManager.EnableBreadcrumbs(ManualBreadcrumbsType, logTypeThatUnsupportCurrentTestCase); - var result = breadcrumbsManager.AddBreadcrumb(message, testedLevel, new Dictionary() { { attributeName, attributeValue } }); + var result = breadcrumbsManager.Log(message, testedLevel, new Dictionary() { { attributeName, attributeValue } }); Assert.IsTrue(result); Assert.AreEqual(expectedNumberOfLogs, inMemoryBreadcrumbStorage.Breadcrumbs.Count); From e2e1ead9608f5e9af8a73073319762c0509c9c43 Mon Sep 17 00:00:00 2001 From: kdysput Date: Fri, 14 May 2021 00:15:00 +0200 Subject: [PATCH 30/42] Send unique attachments (or include (#n) to attachment name) --- Runtime/BacktraceClient.cs | 15 ++++----- Runtime/BacktraceDatabase.cs | 5 +++ Runtime/Interfaces/IBacktraceAPI.cs | 4 +-- Runtime/Model/BacktraceData.cs | 4 +-- Runtime/Model/BacktraceHttpClient.cs | 32 ++++++++++++++++--- .../Model/Database/BacktraceDatabaseRecord.cs | 4 +-- Runtime/Services/BacktraceApi.cs | 4 +-- Tests/Runtime/Mocks/BacktraceApiMock.cs | 4 +-- 8 files changed, 49 insertions(+), 23 deletions(-) diff --git a/Runtime/BacktraceClient.cs b/Runtime/BacktraceClient.cs index 73e5b86a..5c487d15 100644 --- a/Runtime/BacktraceClient.cs +++ b/Runtime/BacktraceClient.cs @@ -480,6 +480,7 @@ public void Refresh() DontDestroyOnLoad(gameObject); _instance = this; } + var nativeAttachments = new HashSet(_clientReportAttachments); if (Configuration.Enabled) { Database = GetComponent(); @@ -488,11 +489,14 @@ public void Refresh() Database.Reload(); Database.SetApi(BacktraceApi); Database.SetReportWatcher(_reportLimitWatcher); - EnableBreadcrumbsSupport(); + if (EnableBreadcrumbsSupport()) + { + nativeAttachments.Add(Database.Breadcrumbs.GetBreadcrumbLogPath()); + } } } - _nativeClient = NativeClientFactory.CreateNativeClient(Configuration, name, AttributeProvider.Get(), _clientReportAttachments); + _nativeClient = NativeClientFactory.CreateNativeClient(Configuration, name, AttributeProvider.Get(), nativeAttachments); AttributeProvider.AddDynamicAttributeProvider(_nativeClient); if (Configuration.SendUnhandledGameCrashesOnGameStartup && isActiveAndEnabled) @@ -513,12 +517,7 @@ public bool EnableBreadcrumbsSupport() { return false; } - var initializationResult = Database.EnableBreadcrumbsSupport(); - if (initializationResult) - { - _clientReportAttachments.Add(Breadcrumbs.GetBreadcrumbLogPath()); - } - return initializationResult; + return Database.EnableBreadcrumbsSupport(); } public void EnableMetrics() diff --git a/Runtime/BacktraceDatabase.cs b/Runtime/BacktraceDatabase.cs index 4aa609c0..6b9c60ed 100644 --- a/Runtime/BacktraceDatabase.cs +++ b/Runtime/BacktraceDatabase.cs @@ -326,6 +326,11 @@ public BacktraceDatabaseRecord Add(BacktraceData data, bool @lock = true) { record.Unlock(); } + // add to fresh new record breadcrumb attachment + if (Breadcrumbs != null) + { + record.BacktraceData.Attachments.Add(Breadcrumbs.GetBreadcrumbLogPath()); + } return record; } diff --git a/Runtime/Interfaces/IBacktraceAPI.cs b/Runtime/Interfaces/IBacktraceAPI.cs index 966c601c..e21312da 100644 --- a/Runtime/Interfaces/IBacktraceAPI.cs +++ b/Runtime/Interfaces/IBacktraceAPI.cs @@ -28,7 +28,7 @@ public interface IBacktraceApi /// Deduplication count /// Coroutine callback /// - IEnumerator Send(string json, List attachments, int deduplication, Action callback); + IEnumerator Send(string json, IEnumerable attachments, int deduplication, Action callback); /// @@ -39,7 +39,7 @@ public interface IBacktraceApi /// Query string /// Coroutine callback /// - IEnumerator Send(string json, List attachments, Dictionary queryAttributes, Action callback); + IEnumerator Send(string json, IEnumerable attachments, Dictionary queryAttributes, Action callback); /// /// Set an event executed when received bad request, unauthorize request or other information from server diff --git a/Runtime/Model/BacktraceData.cs b/Runtime/Model/BacktraceData.cs index 636c4232..bbdedbf8 100644 --- a/Runtime/Model/BacktraceData.cs +++ b/Runtime/Model/BacktraceData.cs @@ -85,7 +85,7 @@ internal string UuidString /// /// Get a path to report attachments /// - public List Attachments; + public ICollection Attachments; /// /// Current BacktraceReport @@ -123,7 +123,7 @@ public BacktraceData(BacktraceReport report, Dictionary clientAt SetAttributes(clientAttributes, gameObjectDepth); SetThreadInformations(); - Attachments = Report.AttachmentPaths.Distinct().ToList(); + Attachments = new HashSet(Report.AttachmentPaths); } /// diff --git a/Runtime/Model/BacktraceHttpClient.cs b/Runtime/Model/BacktraceHttpClient.cs index ca1da529..b55b9c92 100644 --- a/Runtime/Model/BacktraceHttpClient.cs +++ b/Runtime/Model/BacktraceHttpClient.cs @@ -6,6 +6,7 @@ using System.Text; using UnityEngine; using UnityEngine.Networking; +using System.Linq; namespace Backtrace.Unity.Model { @@ -124,18 +125,39 @@ private List CreateMinidumpFormData(byte[] minidump, IEnu private void AddAttachmentToFormData(List formData, IEnumerable attachments) { + if (attachments == null) + { + return; + } // make sure attachments are not bigger than 10 Mb. const int maximumAttachmentSize = 10000000; const string attachmentPrefix = "attachment_"; - var uniqueAttachments = new HashSet(attachments); + + var uniqueAttachments = new HashSet(attachments.Reverse()); + var addedFiles = new Dictionary(); + foreach (var file in uniqueAttachments) { - if (File.Exists(file) && new FileInfo(file).Length < maximumAttachmentSize) + if (File.Exists(file) == false && new FileInfo(file).Length > maximumAttachmentSize) { - formData.Add(new MultipartFormFileSection( - string.Format("{0}{1}", attachmentPrefix, Path.GetFileName(file)), - File.ReadAllBytes(file))); + continue; } + + var fileName = Path.GetFileName(file); + if (addedFiles.ContainsKey(fileName)) + { + addedFiles[fileName]++; + fileName = string.Format("{0}({1}){2}", Path.GetFileName(fileName), addedFiles[fileName], Path.GetExtension(fileName)); + } + else + { + addedFiles[fileName] = 0; + } + + formData.Add(new MultipartFormFileSection( + string.Format("{0}{1}", attachmentPrefix, fileName), + File.ReadAllBytes(file))); + } } } diff --git a/Runtime/Model/Database/BacktraceDatabaseRecord.cs b/Runtime/Model/Database/BacktraceDatabaseRecord.cs index 565befea..757c0009 100644 --- a/Runtime/Model/Database/BacktraceDatabaseRecord.cs +++ b/Runtime/Model/Database/BacktraceDatabaseRecord.cs @@ -48,7 +48,7 @@ public class BacktraceDatabaseRecord /// /// Attachments path /// - public List Attachments { get; private set; } + public ICollection Attachments { get; private set; } private string _diagnosticDataJson; @@ -127,7 +127,7 @@ public string ToJson() dataPath = DiagnosticDataPath, size = Size, hash = Hash, - attachments = Attachments + attachments = new List(Attachments) }; return JsonUtility.ToJson(rawRecord, false); } diff --git a/Runtime/Services/BacktraceApi.cs b/Runtime/Services/BacktraceApi.cs index 434ea3dd..a09b63ec 100644 --- a/Runtime/Services/BacktraceApi.cs +++ b/Runtime/Services/BacktraceApi.cs @@ -169,7 +169,7 @@ public IEnumerator Send(BacktraceData data, Action callback = n /// Deduplication count /// coroutine callback /// Server response - public IEnumerator Send(string json, List attachments, int deduplication, Action callback) + public IEnumerator Send(string json, IEnumerable attachments, int deduplication, Action callback) { var queryAttributes = new Dictionary(); if (deduplication > 0) @@ -188,7 +188,7 @@ public IEnumerator Send(string json, List attachments, int deduplication /// Query string attributes /// coroutine callback /// Server response - public IEnumerator Send(string json, List attachments, Dictionary queryAttributes, Action callback) + public IEnumerator Send(string json, IEnumerable attachments, Dictionary queryAttributes, Action callback) { var stopWatch = EnablePerformanceStatistics ? System.Diagnostics.Stopwatch.StartNew() diff --git a/Tests/Runtime/Mocks/BacktraceApiMock.cs b/Tests/Runtime/Mocks/BacktraceApiMock.cs index e58c3c59..b097be72 100644 --- a/Tests/Runtime/Mocks/BacktraceApiMock.cs +++ b/Tests/Runtime/Mocks/BacktraceApiMock.cs @@ -33,7 +33,7 @@ public IEnumerator Send(BacktraceData data, Action callback = n yield return null; } - public IEnumerator Send(string json, List attachments, int deduplication, Action callback) + public IEnumerator Send(string json, IEnumerable attachments, int deduplication, Action callback) { if (callback != null) { @@ -42,7 +42,7 @@ public IEnumerator Send(string json, List attachments, int deduplication yield return null; } - public IEnumerator Send(string json, List attachments, Dictionary queryAttributes, Action callback) + public IEnumerator Send(string json, IEnumerable attachments, Dictionary queryAttributes, Action callback) { if (callback != null) { From 837e01505687534c91b5b0fa7f76d709602527bb Mon Sep 17 00:00:00 2001 From: kdysput Date: Fri, 14 May 2021 00:17:38 +0200 Subject: [PATCH 31/42] Fixed condition + handling empty nullable files --- Runtime/Model/BacktraceHttpClient.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Runtime/Model/BacktraceHttpClient.cs b/Runtime/Model/BacktraceHttpClient.cs index b55b9c92..1dae5892 100644 --- a/Runtime/Model/BacktraceHttpClient.cs +++ b/Runtime/Model/BacktraceHttpClient.cs @@ -138,7 +138,7 @@ private void AddAttachmentToFormData(List formData, IEnum foreach (var file in uniqueAttachments) { - if (File.Exists(file) == false && new FileInfo(file).Length > maximumAttachmentSize) + if (string.IsNullOrEmpty(file) || File.Exists(file) == false || new FileInfo(file).Length > maximumAttachmentSize) { continue; } From 28e92c0144b3cf9ed2a7e1b09260bb2999002a98 Mon Sep 17 00:00:00 2001 From: konraddysput Date: Fri, 14 May 2021 21:09:07 +0200 Subject: [PATCH 32/42] Order beadcrumbs --- Runtime/BacktraceClient.cs | 6 +++++- Runtime/Native/NativeClientFactory.cs | 2 +- Runtime/Native/iOS/NativeClient.cs | 2 +- iOS/libBacktrace-Unity-Cocoa.a | Bin 11712312 -> 11801016 bytes 4 files changed, 7 insertions(+), 3 deletions(-) diff --git a/Runtime/BacktraceClient.cs b/Runtime/BacktraceClient.cs index 5c487d15..c06a7670 100644 --- a/Runtime/BacktraceClient.cs +++ b/Runtime/BacktraceClient.cs @@ -11,6 +11,7 @@ using System.Collections; using System.Collections.Generic; using System.Globalization; +using System.Linq; using System.Threading; using UnityEngine; @@ -480,7 +481,10 @@ public void Refresh() DontDestroyOnLoad(gameObject); _instance = this; } - var nativeAttachments = new HashSet(_clientReportAttachments); + var nativeAttachments = _clientReportAttachments.ToList() + .Where(n => !string.IsNullOrEmpty(n)) + .OrderBy(System.IO.Path.GetFileName, StringComparer.InvariantCultureIgnoreCase).ToList(); + if (Configuration.Enabled) { Database = GetComponent(); diff --git a/Runtime/Native/NativeClientFactory.cs b/Runtime/Native/NativeClientFactory.cs index c7863141..6e0ec93d 100644 --- a/Runtime/Native/NativeClientFactory.cs +++ b/Runtime/Native/NativeClientFactory.cs @@ -5,7 +5,7 @@ namespace Backtrace.Unity.Runtime.Native { internal static class NativeClientFactory { - internal static INativeClient CreateNativeClient(BacktraceConfiguration configuration, string gameObjectName, IDictionary attributes, IEnumerable attachments) + internal static INativeClient CreateNativeClient(BacktraceConfiguration configuration, string gameObjectName, IDictionary attributes, ICollection attachments) { #if UNITY_EDITOR return null; diff --git a/Runtime/Native/iOS/NativeClient.cs b/Runtime/Native/iOS/NativeClient.cs index d2bc9aa9..88ccb0ce 100644 --- a/Runtime/Native/iOS/NativeClient.cs +++ b/Runtime/Native/iOS/NativeClient.cs @@ -68,7 +68,7 @@ internal struct Entry #endif - public NativeClient(BacktraceConfiguration configuration, IDictionary clientAttributes, IEnumerable attachments) + public NativeClient(BacktraceConfiguration configuration, IDictionary clientAttributes, ICollection attachments) { if (INITIALIZED || !_enabled) { diff --git a/iOS/libBacktrace-Unity-Cocoa.a b/iOS/libBacktrace-Unity-Cocoa.a index 2e7bf4afff2c739910377dba719e66bed47ec9ba..3af6c2b33a4a33582e7c1f972816d19344dafeec 100644 GIT binary patch delta 578573 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