diff --git a/AssemblyVersionInfo.cs b/AssemblyVersionInfo.cs index 86489ac0..a28f2cb5 100644 --- a/AssemblyVersionInfo.cs +++ b/AssemblyVersionInfo.cs @@ -10,5 +10,5 @@ // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("5.10.2.0")] -[assembly: AssemblyFileVersion("5.10.2.0")] +[assembly: AssemblyVersion("5.11.0.0")] +[assembly: AssemblyFileVersion("5.11.0.0")] diff --git a/Mindscape.Raygun4Net.AspNetCore/Mindscape.Raygun4Net.AspNetCore.csproj b/Mindscape.Raygun4Net.AspNetCore/Mindscape.Raygun4Net.AspNetCore.csproj index c701162e..944c138d 100644 --- a/Mindscape.Raygun4Net.AspNetCore/Mindscape.Raygun4Net.AspNetCore.csproj +++ b/Mindscape.Raygun4Net.AspNetCore/Mindscape.Raygun4Net.AspNetCore.csproj @@ -7,7 +7,7 @@ Raygun .NetStandard library for targeting ASP.Net Core applications Mindscape.Raygun4Net.AspNetCore - 6.3.0 + 6.3.1 false https://github.com/MindscapeHQ/raygun4net/blob/master/LICENSE https://github.com/MindscapeHQ/raygun4net diff --git a/Mindscape.Raygun4Net.AspNetCore/Properties/AssemblyInfo.cs b/Mindscape.Raygun4Net.AspNetCore/Properties/AssemblyInfo.cs index 7fbf3f30..e687445b 100644 --- a/Mindscape.Raygun4Net.AspNetCore/Properties/AssemblyInfo.cs +++ b/Mindscape.Raygun4Net.AspNetCore/Properties/AssemblyInfo.cs @@ -1,8 +1,7 @@ using System.Reflection; -using System.Runtime.CompilerServices; using System.Runtime.InteropServices; -// General Information about an assembly is controlled through the following +// General Information about an assembly is controlled through the following // set of attributes. Change these attribute values to modify the information // associated with an assembly. [assembly: AssemblyTitle("Raygun4Net.AspNetCore")] @@ -14,8 +13,8 @@ [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from // COM, set the ComVisible attribute to true on that type. [assembly: ComVisible(false)] diff --git a/Mindscape.Raygun4Net.AspNetCore/Properties/AssemblyVersionInfo.cs b/Mindscape.Raygun4Net.AspNetCore/Properties/AssemblyVersionInfo.cs index 10258aba..d3b951b4 100644 --- a/Mindscape.Raygun4Net.AspNetCore/Properties/AssemblyVersionInfo.cs +++ b/Mindscape.Raygun4Net.AspNetCore/Properties/AssemblyVersionInfo.cs @@ -1,6 +1,4 @@ using System.Reflection; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; // Version information for an assembly consists of the following four values: // @@ -12,5 +10,5 @@ // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("6.3.0")] -[assembly: AssemblyFileVersion("6.3.0")] +[assembly: AssemblyVersion("6.3.1")] +[assembly: AssemblyFileVersion("6.3.1")] diff --git a/Mindscape.Raygun4Net.AspNetCore/readme.txt b/Mindscape.Raygun4Net.AspNetCore/README.md similarity index 100% rename from Mindscape.Raygun4Net.AspNetCore/readme.txt rename to Mindscape.Raygun4Net.AspNetCore/README.md diff --git a/Mindscape.Raygun4Net.AspNetCore/RaygunClient.cs b/Mindscape.Raygun4Net.AspNetCore/RaygunClient.cs index b681f927..9d2f8fcf 100644 --- a/Mindscape.Raygun4Net.AspNetCore/RaygunClient.cs +++ b/Mindscape.Raygun4Net.AspNetCore/RaygunClient.cs @@ -13,11 +13,11 @@ namespace Mindscape.Raygun4Net.AspNetCore public class RaygunClient : RaygunClientBase { protected readonly RaygunRequestMessageOptions _requestMessageOptions = new RaygunRequestMessageOptions(); - + private readonly ThreadLocal _currentHttpContext = new ThreadLocal(() => null); private readonly ThreadLocal _currentRequestMessage = new ThreadLocal(() => null); private readonly ThreadLocal _currentResponseMessage = new ThreadLocal(() => null); - + public RaygunClient(string apiKey) : this(new RaygunSettings {ApiKey = apiKey}) { @@ -196,7 +196,7 @@ public bool UseKeyValuePairRawDataFilter /// /// Add an implementation to be used when capturing the raw data - /// of a HTTP request. This filter will be passed the request raw data and is expected to remove + /// of a HTTP request. This filter will be passed the request raw data and is expected to remove /// or replace values whose keys are found in the list supplied to the Filter method. /// /// Custom raw data filter implementation. @@ -211,13 +211,13 @@ protected override bool CanSend(RaygunMessage message) { return true; } - + RaygunSettings settings = GetSettings(); if (settings.ExcludedStatusCodes == null) { return true; } - + return !settings.ExcludedStatusCodes.Contains(message.Details.Response.StatusCode); } diff --git a/Mindscape.Raygun4Net.Azure.WebJob.nuspec b/Mindscape.Raygun4Net.Azure.WebJob.nuspec index 648effc2..d27cf9da 100644 --- a/Mindscape.Raygun4Net.Azure.WebJob.nuspec +++ b/Mindscape.Raygun4Net.Azure.WebJob.nuspec @@ -23,6 +23,6 @@ - + diff --git a/Mindscape.Raygun4Net.Azure.WebJob/readme.txt b/Mindscape.Raygun4Net.Azure.WebJob/README.md similarity index 100% rename from Mindscape.Raygun4Net.Azure.WebJob/readme.txt rename to Mindscape.Raygun4Net.Azure.WebJob/README.md diff --git a/Mindscape.Raygun4Net.ClientProfile.Tests/RaygunSettingsTests.cs b/Mindscape.Raygun4Net.ClientProfile.Tests/RaygunSettingsTests.cs index 804fdda7..fc9005d8 100644 --- a/Mindscape.Raygun4Net.ClientProfile.Tests/RaygunSettingsTests.cs +++ b/Mindscape.Raygun4Net.ClientProfile.Tests/RaygunSettingsTests.cs @@ -18,7 +18,7 @@ public void Apikey_EmptyByDefault() [Test] public void ApiEndPoint_DefaultValue() { - Assert.AreEqual("https://api.raygun.io/entries", RaygunSettings.Settings.ApiEndpoint.AbsoluteUri); + Assert.AreEqual("https://api.raygun.com/entries", RaygunSettings.Settings.ApiEndpoint.AbsoluteUri); } [Test] diff --git a/Mindscape.Raygun4Net.ClientProfile/Mindscape.Raygun4Net.ClientProfile.csproj b/Mindscape.Raygun4Net.ClientProfile/Mindscape.Raygun4Net.ClientProfile.csproj index cfd1e858..b8709be9 100644 --- a/Mindscape.Raygun4Net.ClientProfile/Mindscape.Raygun4Net.ClientProfile.csproj +++ b/Mindscape.Raygun4Net.ClientProfile/Mindscape.Raygun4Net.ClientProfile.csproj @@ -76,6 +76,15 @@ IRaygunMessageBuilder.cs + + Logging\IRaygunLogger.cs + + + Logging\RaygunLogger.cs + + + Logging\RaygunLogLevel.cs + Messages\RaygunClientMessage.cs @@ -106,6 +115,21 @@ SimpleJson.cs + + Storage\IRaygunFile.cs + + + Storage\IRaygunOfflineStorage.cs + + + Storage\RaygunFile.cs + + + Storage\IsolatedRaygunOfflineStorage.cs + + + Utils\Singleton.cs + diff --git a/Mindscape.Raygun4Net.ClientProfile/Properties/AssemblyInfo.cs b/Mindscape.Raygun4Net.ClientProfile/Properties/AssemblyInfo.cs index e2a7b698..da52caaf 100644 --- a/Mindscape.Raygun4Net.ClientProfile/Properties/AssemblyInfo.cs +++ b/Mindscape.Raygun4Net.ClientProfile/Properties/AssemblyInfo.cs @@ -1,8 +1,7 @@ using System.Reflection; -using System.Runtime.CompilerServices; using System.Runtime.InteropServices; -// General Information about an assembly is controlled through the following +// General Information about an assembly is controlled through the following // set of attributes. Change these attribute values to modify the information // associated with an assembly. [assembly: AssemblyTitle("Raygun4Net.ClientProfile")] @@ -10,12 +9,12 @@ [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("Raygun")] [assembly: AssemblyProduct("Raygun4Net")] -[assembly: AssemblyCopyright("Copyright © Raygun 2015-2017")] +[assembly: AssemblyCopyright("Copyright © Raygun 2015-2020")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from // COM, set the ComVisible attribute to true on that type. [assembly: ComVisible(false)] diff --git a/Mindscape.Raygun4Net.ClientProfile/RaygunClient.cs b/Mindscape.Raygun4Net.ClientProfile/RaygunClient.cs index 21358125..c50425f4 100644 --- a/Mindscape.Raygun4Net.ClientProfile/RaygunClient.cs +++ b/Mindscape.Raygun4Net.ClientProfile/RaygunClient.cs @@ -3,34 +3,33 @@ using System.Collections.Generic; using System.Linq; using System.Net; -using Mindscape.Raygun4Net.Messages; - using System.Threading; using System.Reflection; -using Mindscape.Raygun4Net.Builders; -using System.IO; -using System.IO.IsolatedStorage; -using System.Text; +using Mindscape.Raygun4Net.Logging; +using Mindscape.Raygun4Net.Messages; +using Mindscape.Raygun4Net.Storage; namespace Mindscape.Raygun4Net { public class RaygunClient : RaygunClientBase { + private static object _sendLock = new object(); + private readonly string _apiKey; private readonly List _wrapperExceptions = new List(); + private IRaygunOfflineStorage _offlineStorage = new IsolatedRaygunOfflineStorage(); + /// - /// Initializes a new instance of the class. + /// Gets or sets the username/password credentials which are used to authenticate with the system default Proxy server, if one is set + /// and requires credentials. /// - /// The API key. - public RaygunClient(string apiKey) - { - _apiKey = apiKey; - - _wrapperExceptions.Add(typeof(TargetInvocationException)); + public ICredentials ProxyCredentials { get; set; } - ThreadPool.QueueUserWorkItem(state => { SendStoredMessages(); }); - } + /// + /// Gets or sets an IWebProxy instance which can be used to override the default system proxy server settings + /// + public IWebProxy WebProxy { get; set; } /// /// Initializes a new instance of the class. @@ -41,26 +40,18 @@ public RaygunClient() { } - protected bool ValidateApiKey() - { - if (string.IsNullOrEmpty(_apiKey)) - { - System.Diagnostics.Debug.WriteLine("ApiKey has not been provided, exception will not be logged"); - return false; - } - return true; - } - /// - /// Gets or sets the username/password credentials which are used to authenticate with the system default Proxy server, if one is set - /// and requires credentials. + /// Initializes a new instance of the class. /// - public ICredentials ProxyCredentials { get; set; } + /// The API key. + public RaygunClient(string apiKey) + { + _apiKey = apiKey; - /// - /// Gets or sets an IWebProxy instance which can be used to override the default system proxy server settings - /// - public IWebProxy WebProxy { get; set; } + _wrapperExceptions.Add(typeof(TargetInvocationException)); + + ThreadPool.QueueUserWorkItem(state => { SendStoredMessages(); }); + } /// /// Adds a list of outer exceptions that will be stripped, leaving only the valuable inner exception. @@ -93,9 +84,11 @@ public void RemoveWrapperExceptions(params Type[] wrapperExceptions) _wrapperExceptions.Remove(wrapper); } } - + + #region Message Send Methods + /// - /// Transmits an exception to Raygun.io synchronously, using the version number of the originating assembly. + /// Transmits an exception to Raygun synchronously, using the version number of the originating assembly. /// /// The exception to deliver. public override void Send(Exception exception) @@ -104,7 +97,7 @@ public override void Send(Exception exception) } /// - /// Transmits an exception to Raygun.io synchronously specifying a list of string tags associated + /// Transmits an exception to Raygun synchronously specifying a list of string tags associated /// with the message for identification. This uses the version number of the originating assembly. /// /// The exception to deliver. @@ -115,7 +108,7 @@ public void Send(Exception exception, IList tags) } /// - /// Transmits an exception to Raygun.io synchronously specifying a list of string tags associated + /// Transmits an exception to Raygun synchronously specifying a list of string tags associated /// with the message for identification, as well as sending a key-value collection of custom data. /// This uses the version number of the originating assembly. /// @@ -128,7 +121,7 @@ public void Send(Exception exception, IList tags, IDictionary userCustom } /// - /// Transmits an exception to Raygun.io synchronously specifying a list of string tags associated + /// Transmits an exception to Raygun synchronously specifying a list of string tags associated /// with the message for identification, as well as sending a key-value collection of custom data. /// This uses the version number of the originating assembly. /// @@ -146,7 +139,7 @@ public void Send(Exception exception, IList tags, IDictionary userCustom } /// - /// Asynchronously transmits a message to Raygun.io. + /// Asynchronously transmits a message to Raygun. /// /// The exception to deliver. public void SendInBackground(Exception exception) @@ -155,7 +148,7 @@ public void SendInBackground(Exception exception) } /// - /// Asynchronously transmits an exception to Raygun.io. + /// Asynchronously transmits an exception to Raygun. /// /// The exception to deliver. /// A list of strings associated with the message. @@ -165,7 +158,7 @@ public void SendInBackground(Exception exception, IList tags) } /// - /// Asynchronously transmits an exception to Raygun.io. + /// Asynchronously transmits an exception to Raygun. /// /// The exception to deliver. /// A list of strings associated with the message. @@ -176,7 +169,7 @@ public void SendInBackground(Exception exception, IList tags, IDictionar } /// - /// Asynchronously transmits an exception to Raygun.io. + /// Asynchronously transmits an exception to Raygun. /// /// The exception to deliver. /// A list of strings associated with the message. @@ -208,7 +201,7 @@ public void SendInBackground(Exception exception, IList tags, IDictionar } /// - /// Asynchronously transmits a message to Raygun.io. + /// Asynchronously transmits a message to Raygun. /// /// The RaygunMessage to send. This needs its OccurredOn property /// set to a valid DateTime and as much of the Details property as is available. @@ -217,6 +210,89 @@ public void SendInBackground(RaygunMessage raygunMessage) ThreadPool.QueueUserWorkItem(c => Send(raygunMessage)); } + /// + /// Posts a RaygunMessage to the Raygun API endpoint. + /// + /// The RaygunMessage to send. This needs its OccurredOn property + /// set to a valid DateTime and as much of the Details property as is available. + public override void Send(RaygunMessage raygunMessage) + { + if (!ValidateApiKey()) + { + RaygunLogger.Instance.Warning("Failed to send error report due to invalid API key."); + return; + } + + bool canSend = OnSendingMessage(raygunMessage); + + if (!canSend) + { + return; + } + + string message = null; + + try + { + message = SimpleJson.SerializeObject(raygunMessage); + } + catch (Exception ex) + { + RaygunLogger.Instance.Error($"Failed to serialize report due to: {ex.Message}"); + + if (RaygunSettings.Settings.ThrowOnError) + { + throw; + } + } + + if (string.IsNullOrEmpty(message)) + { + return; + } + + bool successfullySentReport = true; + + try + { + Send(message); + } + catch (Exception ex) + { + successfullySentReport = false; + + RaygunLogger.Instance.Error($"Failed to send report to Raygun due to: {ex.Message}"); + + SaveMessage(message); + + if (RaygunSettings.Settings.ThrowOnError) + { + throw; + } + } + + if (successfullySentReport) + { + SendStoredMessages(); + } + } + + private void Send(string message) + { + RaygunLogger.Instance.Verbose("Sending Payload --------------"); + RaygunLogger.Instance.Verbose(message); + RaygunLogger.Instance.Verbose("------------------------------"); + + using (var client = CreateWebClient()) + { + client.UploadString(RaygunSettings.Settings.ApiEndpoint, message); + } + } + + #endregion // Message Send Methods + + #region Message Building Methods + protected RaygunMessage BuildMessage(Exception exception, IList tags, IDictionary userCustomData) { return BuildMessage(exception, tags, userCustomData, null, null); @@ -261,62 +337,100 @@ private Exception StripWrapperExceptions(Exception exception) return exception; } - /// - /// Posts a RaygunMessage to the Raygun.io api endpoint. - /// - /// The RaygunMessage to send. This needs its OccurredOn property - /// set to a valid DateTime and as much of the Details property as is available. - public override void Send(RaygunMessage raygunMessage) + #endregion // Message Building Methods + + #region Message Offline Storage + + private void SaveMessage(string message) { - bool canSend = OnSendingMessage(raygunMessage); - if (canSend) + if (!RaygunSettings.Settings.CrashReportingOfflineStorageEnabled) + { + RaygunLogger.Instance.Warning("Offline storage is disabled, skipping saving report."); + return; + } + + if (!ValidateApiKey()) + { + RaygunLogger.Instance.Warning("Failed to save report due to invalid API key."); + return; + } + + // Avoid writing and reading from disk at the same time with `SendStoredMessages`. + lock (_sendLock) { - string message = null; try { - message = SimpleJson.SerializeObject(raygunMessage); + if (!_offlineStorage.Store(message, _apiKey)) + { + RaygunLogger.Instance.Warning("Failed to save report to offline storage."); + } } catch (Exception ex) { - System.Diagnostics.Debug.WriteLine(string.Format("Error serializing exception {0}", ex.Message)); - - if (RaygunSettings.Settings.ThrowOnError) - { - throw; - } + RaygunLogger.Instance.Error($"Failed to save report to offline storage due to: {ex.Message}"); } + } + } + + private void SendStoredMessages() + { + if (!RaygunSettings.Settings.CrashReportingOfflineStorageEnabled) + { + RaygunLogger.Instance.Warning("Offline storage is disabled, skipping sending stored reports."); + return; + } + + if (!ValidateApiKey()) + { + RaygunLogger.Instance.Warning("Failed to send offline reports due to invalid API key."); + return; + } - if (message != null) + lock (_sendLock) + { + try { - try - { - Send(message); - } - catch (Exception ex) + var files = _offlineStorage.FetchAll(_apiKey); + + foreach (var file in files) { - SaveMessage(message); - System.Diagnostics.Debug.WriteLine(string.Format("Error Logging Exception to Raygun.io {0}", ex.Message)); + try + { + // Send the stored report. + Send(file.Contents); - if (RaygunSettings.Settings.ThrowOnError) + // Remove the stored report from local storage. + if (_offlineStorage.Remove(file.Name, _apiKey)) + { + RaygunLogger.Instance.Info("Successfully removed report from offline storage."); + } + else + { + RaygunLogger.Instance.Warning("Failed to remove report from offline storage."); + } + } + catch (Exception ex) { - throw; + RaygunLogger.Instance.Error($"Failed to send stored report to Raygun due to: {ex.Message}"); + + // If just one message fails to send, then don't delete the message, + // and don't attempt sending anymore until later. + return; } } - - SendStoredMessages(); + } + catch (Exception ex) + { + RaygunLogger.Instance.Error($"Failed to send stored report to Raygun due to: {ex.Message}"); } } } - private void Send(string message) + #endregion // Message Offline Storage + + protected bool ValidateApiKey() { - if (ValidateApiKey()) - { - using (var client = CreateWebClient()) - { - client.UploadString(RaygunSettings.Settings.ApiEndpoint, message); - } - } + return !string.IsNullOrEmpty(_apiKey); } protected WebClient CreateWebClient() @@ -352,135 +466,5 @@ protected WebClient CreateWebClient() } return client; } - - private void SaveMessage(string message) - { - try - { - using (IsolatedStorageFile isolatedStorage = GetIsolatedStorageScope()) - { - string directoryName = "RaygunOfflineStorage"; - string[] directories = isolatedStorage.GetDirectoryNames("*"); - if (!FileExists(directories, directoryName)) - { - isolatedStorage.CreateDirectory(directoryName); - } - - int number = 1; - string[] files = isolatedStorage.GetFileNames(directoryName + "\\*.txt"); - while (true) - { - bool exists = FileExists(files, "RaygunErrorMessage" + number + ".txt"); - if (!exists) - { - string nextFileName = "RaygunErrorMessage" + (number + 1) + ".txt"; - exists = FileExists(files, nextFileName); - if (exists) - { - isolatedStorage.DeleteFile(directoryName + "\\" + nextFileName); - } - break; - } - number++; - } - - if (number == 11) - { - string firstFileName = "RaygunErrorMessage1.txt"; - if (FileExists(files, firstFileName)) - { - isolatedStorage.DeleteFile(directoryName + "\\" + firstFileName); - } - } - using (IsolatedStorageFileStream isoStream = new IsolatedStorageFileStream(directoryName + "\\RaygunErrorMessage" + number + ".txt", FileMode.OpenOrCreate, FileAccess.Write, isolatedStorage)) - { - using (StreamWriter writer = new StreamWriter(isoStream, Encoding.Unicode)) - { - writer.Write(message); - writer.Flush(); - writer.Close(); - } - } - System.Diagnostics.Trace.WriteLine("Saved message: " + "RaygunErrorMessage" + number + ".txt"); - } - } - catch (Exception ex) - { - System.Diagnostics.Trace.WriteLine(string.Format("Error saving message to isolated storage {0}", ex.Message)); - } - } - - private bool FileExists(string[] files, string fileName) - { - foreach (string str in files) - { - if (fileName.Equals(str)) - { - return true; - } - } - return false; - } - - private static object _sendLock = new object(); - - private void SendStoredMessages() - { - lock (_sendLock) - { - try - { - using (IsolatedStorageFile isolatedStorage = GetIsolatedStorageScope()) - { - string directoryName = "RaygunOfflineStorage"; - string[] directories = isolatedStorage.GetDirectoryNames("*"); - if (FileExists(directories, directoryName)) - { - string[] fileNames = isolatedStorage.GetFileNames(directoryName + "\\*.txt"); - foreach (string name in fileNames) - { - IsolatedStorageFileStream isoFileStream = new IsolatedStorageFileStream(directoryName + "\\" + name, FileMode.Open, isolatedStorage); - using (StreamReader reader = new StreamReader(isoFileStream)) - { - string text = reader.ReadToEnd(); - try - { - Send(text); - } - catch - { - // If just one message fails to send, then don't delete the message, and don't attempt sending anymore until later. - return; - } - System.Diagnostics.Debug.WriteLine("Sent " + name); - } - isolatedStorage.DeleteFile(directoryName + "\\" + name); - } - if (isolatedStorage.GetFileNames(directoryName + "\\*.txt").Length == 0) - { - System.Diagnostics.Debug.WriteLine("Successfully sent all pending messages"); - isolatedStorage.DeleteDirectory(directoryName); - } - } - } - } - catch (Exception ex) - { - System.Diagnostics.Debug.WriteLine(string.Format("Error sending stored messages to Raygun.io {0}", ex.Message)); - } - } - } - - private IsolatedStorageFile GetIsolatedStorageScope() - { - if (AppDomain.CurrentDomain != null && AppDomain.CurrentDomain.ActivationContext != null) - { - return IsolatedStorageFile.GetUserStoreForApplication(); - } - else - { - return IsolatedStorageFile.GetUserStoreForAssembly(); - } - } } } diff --git a/Mindscape.Raygun4Net.ClientProfile/RaygunSettings.cs b/Mindscape.Raygun4Net.ClientProfile/RaygunSettings.cs index dbbd72e5..91c8ab3e 100644 --- a/Mindscape.Raygun4Net.ClientProfile/RaygunSettings.cs +++ b/Mindscape.Raygun4Net.ClientProfile/RaygunSettings.cs @@ -1,6 +1,7 @@ using System; using System.Configuration; using System.Linq; +using Mindscape.Raygun4Net.Logging; namespace Mindscape.Raygun4Net { @@ -8,7 +9,9 @@ public class RaygunSettings : ConfigurationSection { private static readonly RaygunSettings settings = ConfigurationManager.GetSection("RaygunSettings") as RaygunSettings ?? new RaygunSettings(); - private const string DefaultApiEndPoint = "https://api.raygun.io/entries"; + private const string DefaultApiEndPoint = "https://api.raygun.com/entries"; + + public const int MaxCrashReportsStoredOfflineHardLimit = 64; public static RaygunSettings Settings { @@ -50,7 +53,43 @@ public string ApplicationVersion get { return (string)this["applicationVersion"]; } set { this["applicationVersion"] = value; } } - + + /// + /// Gets or sets the max crash reports stored on the device. + /// There is a hard upper limit of 64 reports. + /// + /// The max crash reports stored on device. + [ConfigurationProperty("maxCrashReportsStoredOffline", IsRequired = false, DefaultValue = MaxCrashReportsStoredOfflineHardLimit)] + public int MaxCrashReportsStoredOffline + { + get { return (int)this["maxCrashReportsStoredOffline"]; } + set { this["maxCrashReportsStoredOffline"] = value; } + } + + /// + /// Allows for crash reports to be stored to local storage when there is no available network connection. + /// + /// true if allowing crash reports to be stored offline; otherwise, false. + [ConfigurationProperty("crashReportingOfflineStorageEnabled", IsRequired = false, DefaultValue = true)] + public bool CrashReportingOfflineStorageEnabled + { + get { return (bool)this["crashReportingOfflineStorageEnabled"]; } + set { this["crashReportingOfflineStorageEnabled"] = value; } + } + + /// + /// Gets or sets the log level controlling the amount of information printed to system consoles. + /// Setting the level to will print the raw Crash Reporting being + /// posted to the API endpoints. + /// + /// The log level. + [ConfigurationProperty("logLevel", IsRequired = false, DefaultValue = RaygunLogLevel.Warning)] + public RaygunLogLevel LogLevel + { + get { return (RaygunLogLevel)this["logLevel"]; } + set { this["logLevel"] = value; } + } + /// /// Return false. /// diff --git a/Mindscape.Raygun4Net.Core.Signed.nuspec b/Mindscape.Raygun4Net.Core.Signed.nuspec index 86bf2f40..2001d7b0 100644 --- a/Mindscape.Raygun4Net.Core.Signed.nuspec +++ b/Mindscape.Raygun4Net.Core.Signed.nuspec @@ -2,7 +2,7 @@ Mindscape.Raygun4Net.Core.Signed - 5.10.2 + 5.11.0 <authors>Raygun</authors> <owners /> diff --git a/Mindscape.Raygun4Net.Core.nuspec b/Mindscape.Raygun4Net.Core.nuspec index bb07f560..d74c42a4 100644 --- a/Mindscape.Raygun4Net.Core.nuspec +++ b/Mindscape.Raygun4Net.Core.nuspec @@ -2,7 +2,7 @@ <package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd"> <metadata minClientVersion="2.5"> <id>Mindscape.Raygun4Net.Core</id> - <version>5.10.2</version> + <version>5.11.0</version> <title /> <authors>Raygun</authors> <owners /> diff --git a/Mindscape.Raygun4Net.Core/Mindscape.Raygun4Net.Core.csproj b/Mindscape.Raygun4Net.Core/Mindscape.Raygun4Net.Core.csproj index 5590a09b..7467bd7b 100644 --- a/Mindscape.Raygun4Net.Core/Mindscape.Raygun4Net.Core.csproj +++ b/Mindscape.Raygun4Net.Core/Mindscape.Raygun4Net.Core.csproj @@ -81,6 +81,15 @@ <Compile Include="..\Mindscape.Raygun4Net\IRaygunMessageBuilder.cs"> <Link>IRaygunMessageBuilder.cs</Link> </Compile> + <Compile Include="..\Mindscape.Raygun4Net\Logging\IRaygunLogger.cs"> + <Link>Logging\IRaygunLogger.cs</Link> + </Compile> + <Compile Include="..\Mindscape.Raygun4Net\Logging\RaygunLogger.cs"> + <Link>Logging\RaygunLogger.cs</Link> + </Compile> + <Compile Include="..\Mindscape.Raygun4Net\Logging\RaygunLogLevel.cs"> + <Link>Logging\RaygunLogLevel.cs</Link> + </Compile> <Compile Include="..\Mindscape.Raygun4Net\Messages\RaygunClientMessage.cs"> <Link>Messages\RaygunClientMessage.cs</Link> </Compile> @@ -123,6 +132,21 @@ <Compile Include="..\Mindscape.Raygun4Net\SimpleJson.cs"> <Link>SimpleJson.cs</Link> </Compile> + <Compile Include="..\Mindscape.Raygun4Net\Storage\IRaygunFile.cs"> + <Link>Storage\IRaygunFile.cs</Link> + </Compile> + <Compile Include="..\Mindscape.Raygun4Net\Storage\IRaygunOfflineStorage.cs"> + <Link>Storage\IRaygunOfflineStorage.cs</Link> + </Compile> + <Compile Include="..\Mindscape.Raygun4Net\Storage\RaygunFile.cs"> + <Link>Storage\RaygunFile.cs</Link> + </Compile> + <Compile Include="..\Mindscape.Raygun4Net\Storage\IsolatedRaygunOfflineStorage.cs"> + <Link>Storage\IsolatedRaygunOfflineStorage.cs</Link> + </Compile> + <Compile Include="..\Mindscape.Raygun4Net\Utils\Singleton.cs"> + <Link>Utils\Singleton.cs</Link> + </Compile> <Compile Include="Builders\RaygunErrorMessageBuilder.cs" /> <Compile Include="Messages\RaygunErrorMessage.cs" /> <Compile Include="Properties\AssemblyInfo.cs" /> diff --git a/Mindscape.Raygun4Net.Core/Properties/AssemblyInfo.cs b/Mindscape.Raygun4Net.Core/Properties/AssemblyInfo.cs index c1fb18e9..a65b8dc5 100644 --- a/Mindscape.Raygun4Net.Core/Properties/AssemblyInfo.cs +++ b/Mindscape.Raygun4Net.Core/Properties/AssemblyInfo.cs @@ -1,8 +1,7 @@ using System.Reflection; -using System.Runtime.CompilerServices; using System.Runtime.InteropServices; -// General Information about an assembly is controlled through the following +// General Information about an assembly is controlled through the following // set of attributes. Change these attribute values to modify the information // associated with an assembly. [assembly: AssemblyTitle("Raygun4Net.Core")] @@ -14,8 +13,8 @@ [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from // COM, set the ComVisible attribute to true on that type. [assembly: ComVisible(false)] diff --git a/Mindscape.Raygun4Net.Mvc.Signed.nuspec b/Mindscape.Raygun4Net.Mvc.Signed.nuspec index e62d9243..117bca1e 100644 --- a/Mindscape.Raygun4Net.Mvc.Signed.nuspec +++ b/Mindscape.Raygun4Net.Mvc.Signed.nuspec @@ -2,7 +2,7 @@ <package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd"> <metadata minClientVersion="2.5"> <id>Mindscape.Raygun4Net.Mvc.Signed</id> - <version>5.10.3</version> + <version>5.11.0</version> <title /> <authors>Raygun</authors> <owners /> @@ -12,7 +12,7 @@ <projectUrl>https://github.com/MindscapeHQ/raygun4net</projectUrl> <licenseUrl>https://raw.github.com/MindscapeHQ/raygun4net/master/LICENSE</licenseUrl> <dependencies> - <dependency id="Mindscape.Raygun4Net.Core.Signed" version="5.10.2" /> + <dependency id="Mindscape.Raygun4Net.Core.Signed" version="5.11.0" /> </dependencies> </metadata> <files> @@ -25,6 +25,6 @@ <file src="build\signed\mvc\Mindscape.Raygun4Net4.dll" target="lib\net40\Mindscape.Raygun4Net4.dll" /> <file src="build\signed\mvc\Mindscape.Raygun4Net4.pdb" target="lib\net40\Mindscape.Raygun4Net4.pdb" /> - <file src="Mindscape.Raygun4Net.Mvc\readme.txt" /> + <file src="Mindscape.Raygun4Net.Mvc\README.md" /> </files> </package> diff --git a/Mindscape.Raygun4Net.Mvc.nuspec b/Mindscape.Raygun4Net.Mvc.nuspec index 68d8074f..79334daa 100644 --- a/Mindscape.Raygun4Net.Mvc.nuspec +++ b/Mindscape.Raygun4Net.Mvc.nuspec @@ -2,7 +2,7 @@ <package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd"> <metadata minClientVersion="2.5"> <id>Mindscape.Raygun4Net.Mvc</id> - <version>5.10.3</version> + <version>5.11.0</version> <title /> <authors>Raygun</authors> <owners /> @@ -12,7 +12,7 @@ <projectUrl>https://github.com/MindscapeHQ/raygun4net</projectUrl> <licenseUrl>https://raw.github.com/MindscapeHQ/raygun4net/master/LICENSE</licenseUrl> <dependencies> - <dependency id="Mindscape.Raygun4Net.Core" version="5.10.2" /> + <dependency id="Mindscape.Raygun4Net.Core" version="5.11.0" /> </dependencies> </metadata> <files> @@ -25,6 +25,6 @@ <file src="build\mvc\Mindscape.Raygun4Net4.dll" target="lib\net40\Mindscape.Raygun4Net4.dll" /> <file src="build\mvc\Mindscape.Raygun4Net4.pdb" target="lib\net40\Mindscape.Raygun4Net4.pdb" /> - <file src="Mindscape.Raygun4Net.Mvc\readme.txt" /> + <file src="Mindscape.Raygun4Net.Mvc\README.md" /> </files> </package> diff --git a/Mindscape.Raygun4Net.Mvc/Properties/AssemblyInfo.cs b/Mindscape.Raygun4Net.Mvc/Properties/AssemblyInfo.cs index ea67a8f5..27f0b378 100644 --- a/Mindscape.Raygun4Net.Mvc/Properties/AssemblyInfo.cs +++ b/Mindscape.Raygun4Net.Mvc/Properties/AssemblyInfo.cs @@ -1,8 +1,7 @@ using System.Reflection; -using System.Runtime.CompilerServices; using System.Runtime.InteropServices; -// General Information about an assembly is controlled through the following +// General Information about an assembly is controlled through the following // set of attributes. Change these attribute values to modify the information // associated with an assembly. [assembly: AssemblyTitle("Raygun4Net.Mvc")] @@ -14,8 +13,8 @@ [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from // COM, set the ComVisible attribute to true on that type. [assembly: ComVisible(false)] diff --git a/Mindscape.Raygun4Net.Mvc/Properties/AssemblyVersionInfo.cs b/Mindscape.Raygun4Net.Mvc/Properties/AssemblyVersionInfo.cs index 6af10067..a28f2cb5 100644 --- a/Mindscape.Raygun4Net.Mvc/Properties/AssemblyVersionInfo.cs +++ b/Mindscape.Raygun4Net.Mvc/Properties/AssemblyVersionInfo.cs @@ -10,5 +10,5 @@ // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("5.10.3.0")] -[assembly: AssemblyFileVersion("5.10.3.0")] +[assembly: AssemblyVersion("5.11.0.0")] +[assembly: AssemblyFileVersion("5.11.0.0")] diff --git a/Mindscape.Raygun4Net.Mvc/readme.txt b/Mindscape.Raygun4Net.Mvc/README.md similarity index 100% rename from Mindscape.Raygun4Net.Mvc/readme.txt rename to Mindscape.Raygun4Net.Mvc/README.md diff --git a/Mindscape.Raygun4Net.NetCore.Common/Messages/RaygunClientMessage.cs b/Mindscape.Raygun4Net.NetCore.Common/Messages/RaygunClientMessage.cs index e220efb7..f461c3ea 100644 --- a/Mindscape.Raygun4Net.NetCore.Common/Messages/RaygunClientMessage.cs +++ b/Mindscape.Raygun4Net.NetCore.Common/Messages/RaygunClientMessage.cs @@ -5,10 +5,10 @@ public class RaygunClientMessage public RaygunClientMessage() { Name = "Raygun4Net.NetCore"; - Version = "6.2.0"; + Version = "6.3.1"; ClientUrl = @"https://github.com/MindscapeHQ/raygun4net"; } - + public string Name { get; set; } public string Version { get; set; } diff --git a/Mindscape.Raygun4Net.NetCore.Common/Mindscape.Raygun4Net.NetCore.Common.csproj b/Mindscape.Raygun4Net.NetCore.Common/Mindscape.Raygun4Net.NetCore.Common.csproj index 5b308a4d..c454ab73 100644 --- a/Mindscape.Raygun4Net.NetCore.Common/Mindscape.Raygun4Net.NetCore.Common.csproj +++ b/Mindscape.Raygun4Net.NetCore.Common/Mindscape.Raygun4Net.NetCore.Common.csproj @@ -8,7 +8,7 @@ <Authors>Raygun</Authors> <Description>.NetStandard library .NetCore applications</Description> <PackageId>Mindscape.Raygun4Net.NetCore.Common</PackageId> - <PackageVersion>6.3.0</PackageVersion> + <PackageVersion>6.3.1</PackageVersion> <PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance> <PackageLicenseUrl>https://github.com/MindscapeHQ/raygun4net/blob/master/LICENSE</PackageLicenseUrl> <PackageProjectUrl>https://github.com/MindscapeHQ/raygun4net</PackageProjectUrl> diff --git a/Mindscape.Raygun4Net.NetCore.Common/RaygunClientBase.cs b/Mindscape.Raygun4Net.NetCore.Common/RaygunClientBase.cs index a7c8c706..51653157 100644 --- a/Mindscape.Raygun4Net.NetCore.Common/RaygunClientBase.cs +++ b/Mindscape.Raygun4Net.NetCore.Common/RaygunClientBase.cs @@ -12,24 +12,26 @@ namespace Mindscape.Raygun4Net { public abstract class RaygunClientBase { + private static readonly HttpClient Client = new HttpClient(); + private readonly string _apiKey; private readonly List<Type> _wrapperExceptions = new List<Type>(); - protected readonly RaygunSettingsBase _settings; + private bool _handlingRecursiveErrorSending; + private bool _handlingRecursiveGrouping; + + protected readonly RaygunSettingsBase _settings; protected internal const string SentKey = "AlreadySentByRaygun"; - - public RaygunClientBase(RaygunSettingsBase settings) - { - _settings = settings; - _apiKey = settings.ApiKey; - _wrapperExceptions.Add(typeof(TargetInvocationException)); - - if (!string.IsNullOrEmpty(settings.ApplicationVersion)) - { - ApplicationVersion = settings.ApplicationVersion; - } - } + /// <summary> + /// Raised just before a message is sent. This can be used to make final adjustments to the <see cref="RaygunMessage"/>, or to cancel the send. + /// </summary> + public event EventHandler<RaygunSendingMessageEventArgs> SendingMessage; + + /// <summary> + /// Raised before a message is sent. This can be used to add a custom grouping key to a RaygunMessage before sending it to the Raygun service. + /// </summary> + public event EventHandler<RaygunCustomGroupingKeyEventArgs> CustomGroupingKey; /// <summary> /// Gets or sets the user identity string. @@ -46,6 +48,19 @@ public RaygunClientBase(RaygunSettingsBase settings) /// </summary> public string ApplicationVersion { get; set; } + public RaygunClientBase(RaygunSettingsBase settings) + { + _settings = settings; + _apiKey = settings.ApiKey; + + _wrapperExceptions.Add(typeof(TargetInvocationException)); + + if (!string.IsNullOrEmpty(settings.ApplicationVersion)) + { + ApplicationVersion = settings.ApplicationVersion; + } + } + /// <summary> /// Adds a list of outer exceptions that will be stripped, leaving only the valuable inner exception. /// This can be used when a wrapper exception, e.g. TargetInvocationException or HttpUnhandledException, @@ -90,7 +105,7 @@ protected void FlagAsSent(Exception exception) try { Type[] genericTypes = exception.Data.GetType().GetTypeInfo().GenericTypeArguments; - + if (genericTypes.Length == 0 || genericTypes[0].GetTypeInfo().IsAssignableFrom(typeof(string))) { exception.Data[SentKey] = true; @@ -103,13 +118,6 @@ protected void FlagAsSent(Exception exception) } } - /// <summary> - /// Raised just before a message is sent. This can be used to make final adjustments to the <see cref="RaygunMessage"/>, or to cancel the send. - /// </summary> - public event EventHandler<RaygunSendingMessageEventArgs> SendingMessage; - - private bool _handlingRecursiveErrorSending; - // Returns true if the message can be sent, false if the sending is canceled. protected bool OnSendingMessage(RaygunMessage raygunMessage) { @@ -117,12 +125,12 @@ protected bool OnSendingMessage(RaygunMessage raygunMessage) if (!_handlingRecursiveErrorSending) { - EventHandler<RaygunSendingMessageEventArgs> handler = SendingMessage; - + var handler = SendingMessage; + if (handler != null) { - RaygunSendingMessageEventArgs args = new RaygunSendingMessageEventArgs(raygunMessage); - + var args = new RaygunSendingMessageEventArgs(raygunMessage); + try { handler(this, args); @@ -132,12 +140,12 @@ protected bool OnSendingMessage(RaygunMessage raygunMessage) // Catch and send exceptions that occur in the SendingMessage event handler. // Set the _handlingRecursiveErrorSending flag to prevent infinite errors. _handlingRecursiveErrorSending = true; - + Send(e); - + _handlingRecursiveErrorSending = false; } - + result = !args.Cancel; } } @@ -145,25 +153,18 @@ protected bool OnSendingMessage(RaygunMessage raygunMessage) return result; } - /// <summary> - /// Raised before a message is sent. This can be used to add a custom grouping key to a RaygunMessage before sending it to the Raygun service. - /// </summary> - public event EventHandler<RaygunCustomGroupingKeyEventArgs> CustomGroupingKey; - - private bool _handlingRecursiveGrouping; - protected async Task<string> OnCustomGroupingKey(Exception exception, RaygunMessage message) { string result = null; - + if (!_handlingRecursiveGrouping) { var handler = CustomGroupingKey; - + if (handler != null) { var args = new RaygunCustomGroupingKeyEventArgs(exception, message); - + try { handler(this, args); @@ -171,19 +172,19 @@ protected async Task<string> OnCustomGroupingKey(Exception exception, RaygunMess catch (Exception e) { _handlingRecursiveGrouping = true; - + await SendAsync(e, null, null); - + _handlingRecursiveGrouping = false; } - + result = args.CustomGroupingKey; } } - + return result; } - + protected bool ValidateApiKey() { if (string.IsNullOrEmpty(_apiKey)) @@ -191,7 +192,7 @@ protected bool ValidateApiKey() Debug.WriteLine("ApiKey has not been provided, exception will not be logged"); return false; } - + return true; } @@ -231,7 +232,7 @@ public void Send(Exception exception, IList<string> tags, IDictionary userCustom { SendAsync(exception, tags, userCustomData).Wait(); } - + protected virtual async Task SendAsync(Exception exception, IList<string> tags, IDictionary userCustomData) { if (CanSend(exception)) @@ -286,9 +287,9 @@ public virtual async Task SendInBackground(Exception exception, IList<string> ta { await StripAndSend(exception, tags, userCustomData, userInfo); }); - + FlagAsSent(exception); - + await task; } } @@ -307,7 +308,7 @@ internal void FlagExceptionAsSent(Exception exception) { FlagAsSent(exception); } - + protected virtual async Task<RaygunMessage> BuildMessage(Exception exception, IList<string> tags, IDictionary userCustomData, RaygunIdentifierMessage userInfo) { var message = RaygunMessageBuilder.New(_settings) @@ -322,7 +323,7 @@ protected virtual async Task<RaygunMessage> BuildMessage(Exception exception, IL .Build(); var customGroupingKey = await OnCustomGroupingKey(exception, message); - + if (string.IsNullOrEmpty(customGroupingKey) == false) { message.Details.GroupingKey = customGroupingKey; @@ -344,7 +345,7 @@ protected IEnumerable<Exception> StripWrapperExceptions(Exception exception) if (exception != null && _wrapperExceptions.Any(wrapperException => exception.GetType() == wrapperException && exception.InnerException != null)) { AggregateException aggregate = exception as AggregateException; - + if (aggregate != null) { foreach (Exception e in aggregate.InnerExceptions) @@ -376,47 +377,47 @@ protected IEnumerable<Exception> StripWrapperExceptions(Exception exception) /// set to a valid DateTime and as much of the Details property as is available.</param> public async Task Send(RaygunMessage raygunMessage) { - if (ValidateApiKey()) + if (!ValidateApiKey()) { - bool canSend = OnSendingMessage(raygunMessage) && CanSend(raygunMessage); - - if (canSend) - { - using (var client = new HttpClient()) - { - var requestMessage = new HttpRequestMessage(HttpMethod.Post, _settings.ApiEndpoint); + return; + } - requestMessage.Headers.Add("X-ApiKey", _apiKey); + bool canSend = OnSendingMessage(raygunMessage) && CanSend(raygunMessage); - try - { - var message = SimpleJson.SerializeObject(raygunMessage); - requestMessage.Content = new StringContent(message, Encoding.UTF8, "application/json"); - - var result = await client.SendAsync(requestMessage); - - if (!result.IsSuccessStatusCode) - { - Debug.WriteLine($"Error Logging Exception to Raygun {result.ReasonPhrase}"); - - if (_settings.ThrowOnError) - { - throw new Exception("Could not log to Raygun"); - } - } - } - catch (Exception ex) - { - Debug.WriteLine($"Error Logging Exception to Raygun {ex.Message}"); + if (!canSend) + { + return; + } - if (_settings.ThrowOnError) - { - throw; - } - } + var requestMessage = new HttpRequestMessage(HttpMethod.Post, _settings.ApiEndpoint); + requestMessage.Headers.Add("X-ApiKey", _apiKey); + + try + { + var message = SimpleJson.SerializeObject(raygunMessage); + requestMessage.Content = new StringContent(message, Encoding.UTF8, "application/json"); + + var result = await Client.SendAsync(requestMessage); + + if (!result.IsSuccessStatusCode) + { + Debug.WriteLine($"Error Logging Exception to Raygun {result.ReasonPhrase}"); + + if (_settings.ThrowOnError) + { + throw new Exception("Could not log to Raygun"); } } } + catch (Exception ex) + { + Debug.WriteLine($"Error Logging Exception to Raygun {ex.Message}"); + + if (_settings.ThrowOnError) + { + throw; + } + } } } } diff --git a/Mindscape.Raygun4Net.NetCore/Mindscape.Raygun4Net.NetCore.csproj b/Mindscape.Raygun4Net.NetCore/Mindscape.Raygun4Net.NetCore.csproj index 861d53f7..da81e068 100644 --- a/Mindscape.Raygun4Net.NetCore/Mindscape.Raygun4Net.NetCore.csproj +++ b/Mindscape.Raygun4Net.NetCore/Mindscape.Raygun4Net.NetCore.csproj @@ -4,7 +4,6 @@ <AssemblyName>Mindscape.Raygun4Net.NetCore</AssemblyName> <EnableDefaultCompileItems>false</EnableDefaultCompileItems> <TargetFrameworks>netstandard1.6;netstandard2.0</TargetFrameworks> - <TargetFrameworkIdentifier>.NETStandard</TargetFrameworkIdentifier> <GenerateAssemblyInfo>false</GenerateAssemblyInfo> <Configurations>Debug;Release;Sign</Configurations> <Platforms>AnyCPU</Platforms> @@ -14,7 +13,7 @@ <Authors>Raygun</Authors> <Description>.NetStandard library for targeting .Net Core applications</Description> <PackageId>Mindscape.Raygun4Net.NetCore</PackageId> - <PackageVersion>6.3.0</PackageVersion> + <PackageVersion>6.3.1</PackageVersion> <PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance> <PackageLicenseUrl>https://github.com/MindscapeHQ/raygun4net/blob/master/LICENSE</PackageLicenseUrl> <PackageProjectUrl>https://github.com/MindscapeHQ/raygun4net</PackageProjectUrl> diff --git a/Mindscape.Raygun4Net.NetCore/Properties/AssemblyInfo.cs b/Mindscape.Raygun4Net.NetCore/Properties/AssemblyInfo.cs index 8d493103..6cf760b5 100644 --- a/Mindscape.Raygun4Net.NetCore/Properties/AssemblyInfo.cs +++ b/Mindscape.Raygun4Net.NetCore/Properties/AssemblyInfo.cs @@ -1,8 +1,7 @@ using System.Reflection; -using System.Runtime.CompilerServices; using System.Runtime.InteropServices; -// General Information about an assembly is controlled through the following +// General Information about an assembly is controlled through the following // set of attributes. Change these attribute values to modify the information // associated with an assembly. [assembly: AssemblyTitle("Raygun4Net.NetCore")] @@ -14,8 +13,8 @@ [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from // COM, set the ComVisible attribute to true on that type. [assembly: ComVisible(false)] diff --git a/Mindscape.Raygun4Net.NetCore/Properties/AssemblyVersionInfo.cs b/Mindscape.Raygun4Net.NetCore/Properties/AssemblyVersionInfo.cs index 10258aba..d3b951b4 100644 --- a/Mindscape.Raygun4Net.NetCore/Properties/AssemblyVersionInfo.cs +++ b/Mindscape.Raygun4Net.NetCore/Properties/AssemblyVersionInfo.cs @@ -1,6 +1,4 @@ using System.Reflection; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; // Version information for an assembly consists of the following four values: // @@ -12,5 +10,5 @@ // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("6.3.0")] -[assembly: AssemblyFileVersion("6.3.0")] +[assembly: AssemblyVersion("6.3.1")] +[assembly: AssemblyFileVersion("6.3.1")] diff --git a/Mindscape.Raygun4Net.NetCore/readme.txt b/Mindscape.Raygun4Net.NetCore/README.md similarity index 100% rename from Mindscape.Raygun4Net.NetCore/readme.txt rename to Mindscape.Raygun4Net.NetCore/README.md diff --git a/Mindscape.Raygun4Net.Signed.nuspec b/Mindscape.Raygun4Net.Signed.nuspec index ca25415f..8bf885d0 100644 --- a/Mindscape.Raygun4Net.Signed.nuspec +++ b/Mindscape.Raygun4Net.Signed.nuspec @@ -2,7 +2,7 @@ <package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd"> <metadata minClientVersion="2.5"> <id>Mindscape.Raygun4Net.Signed</id> - <version>5.10.2</version> + <version>5.11.0</version> <title /> <authors>Raygun</authors> <owners /> @@ -42,6 +42,6 @@ <file src="build\windowsphone\Mindscape.Raygun4Net.WindowsPhone.dll" target="lib\windowsphone8\Mindscape.Raygun4Net.WindowsPhone.dll" /> <file src="build\windowsphone\Mindscape.Raygun4Net.WindowsPhone.pdb" target="lib\windowsphone8\Mindscape.Raygun4Net.WindowsPhone.pdb" /> - <file src="readme.txt" /> + <file src="README.md" /> </files> </package> diff --git a/Mindscape.Raygun4Net.Tests/Mindscape.Raygun4Net.Tests.csproj b/Mindscape.Raygun4Net.Tests/Mindscape.Raygun4Net.Tests.csproj index 70272621..ed92f3d8 100644 --- a/Mindscape.Raygun4Net.Tests/Mindscape.Raygun4Net.Tests.csproj +++ b/Mindscape.Raygun4Net.Tests/Mindscape.Raygun4Net.Tests.csproj @@ -73,6 +73,7 @@ <Compile Include="RaygunRequestMessageTests.cs" /> <Compile Include="RaygunSettingsTests.cs" /> <Compile Include="SimpleJsonTests.cs" /> + <Compile Include="Storage\RaygunOfflineStorageTests.cs" /> </ItemGroup> <ItemGroup> <ProjectReference Include="..\Mindscape.Raygun4Net\Mindscape.Raygun4Net.csproj"> diff --git a/Mindscape.Raygun4Net.Tests/RaygunSettingsTests.cs b/Mindscape.Raygun4Net.Tests/RaygunSettingsTests.cs index 1d8d72e8..5ef5d72c 100644 --- a/Mindscape.Raygun4Net.Tests/RaygunSettingsTests.cs +++ b/Mindscape.Raygun4Net.Tests/RaygunSettingsTests.cs @@ -18,7 +18,7 @@ public void Apikey_EmptyByDefault() [Test] public void ApiEndPoint_DefaultValue() { - Assert.AreEqual("https://api.raygun.io/entries", RaygunSettings.Settings.ApiEndpoint.AbsoluteUri); + Assert.AreEqual("https://api.raygun.com/entries", RaygunSettings.Settings.ApiEndpoint.AbsoluteUri); } [Test] diff --git a/Mindscape.Raygun4Net.Tests/Storage/RaygunOfflineStorageTests.cs b/Mindscape.Raygun4Net.Tests/Storage/RaygunOfflineStorageTests.cs new file mode 100644 index 00000000..49d823ec --- /dev/null +++ b/Mindscape.Raygun4Net.Tests/Storage/RaygunOfflineStorageTests.cs @@ -0,0 +1,161 @@ +using System.Linq; +using Mindscape.Raygun4Net.Storage; +using NUnit.Framework; + +namespace Mindscape.Raygun4Net.Tests.Storage +{ + [TestFixture] + public class RaygunOfflineStorageTests + { + private IRaygunOfflineStorage _storageOne; + private IRaygunOfflineStorage _storageTwo; + + private const string TestApiKey = "DummyApiKey"; + + [SetUp] + public void SetUp() + { + _storageOne = new IsolatedRaygunOfflineStorage(); + _storageTwo = new IsolatedRaygunOfflineStorage(); + } + + [TearDown] + public void TearDown() + { + // Clear all files from storage. + var filesOne = _storageOne.FetchAll(TestApiKey); + foreach (var file in filesOne) + { + _storageOne.Remove(file.Name, TestApiKey); + } + + var filesTwo = _storageTwo.FetchAll(TestApiKey); + foreach (var file in filesTwo) + { + _storageTwo.Remove(file.Name, TestApiKey); + } + } + + [Test] + public void Store_UsingInvalidMessageValue_ReturnFalse() + { + Assert.IsFalse(_storageOne.Store(null, TestApiKey)); + Assert.IsFalse(_storageOne.Store("", TestApiKey)); + } + + [Test] + public void Store_UsingInvalidApiKeyValue_ReturnFalse() + { + Assert.IsFalse(_storageOne.Store("DummyData", null)); + Assert.IsFalse(_storageOne.Store("DummyData", "")); + } + + [Test] + public void FetchAll_UsingInvalidApiKeyValue_ReturnEmptyResult() + { + Assert.IsEmpty(_storageOne.FetchAll(null)); + Assert.IsEmpty(_storageOne.FetchAll("")); + } + + [Test] + public void Remove_UsingInvalidNameValue_ReturnFalse() + { + Assert.IsFalse(_storageOne.Remove(null, TestApiKey)); + Assert.IsFalse(_storageOne.Remove("", TestApiKey)); + } + + [Test] + public void Remove_UsingInvalidApiKeyValue_ReturnFalse() + { + Assert.IsFalse(_storageOne.Remove("DummyName", null)); + Assert.IsFalse(_storageOne.Remove("DummyName", "")); + } + + [Test] + public void Store_WriteASingleMessageToStorage_OneMessageIsAvailableFromStorage() + { + // Ensure there are no files in storage. + var files = _storageOne.FetchAll(TestApiKey); + Assert.That(files.Count, Is.EqualTo(0)); + + RaygunSettings.Settings.MaxCrashReportsStoredOffline = 1; + + // Save one message to storage. + _storageOne.Store("DummyData", TestApiKey); + + files = _storageOne.FetchAll(TestApiKey); + + // Ensure only one file was created. + Assert.That(files.Count, Is.EqualTo(1)); + Assert.That(files.First().Contents, Is.EqualTo("DummyData")); + } + + [Test] + public void Store_WriteMultipleMessagesToStorage_MaxReportsLimitIsRespected() + { + // Ensure there are no files in storage. + var files = _storageOne.FetchAll(TestApiKey); + Assert.That(files.Count, Is.EqualTo(0)); + + RaygunSettings.Settings.MaxCrashReportsStoredOffline = 1; + + // Save two messages to storage. + _storageOne.Store("DummyData1", TestApiKey); + _storageOne.Store("DummyData2", TestApiKey); + + files = _storageOne.FetchAll(TestApiKey); + + // Ensure only one file was created. + Assert.That(files.Count, Is.EqualTo(1)); + Assert.That(files.First().Contents, Is.EqualTo("DummyData1")); + } + + [Test] + public void Remove_TwoMessagesStoredAndOneRemoved_OnlyOneMessageRemainsInStorage() + { + // Ensure there are no files in storage. + var files = _storageOne.FetchAll(TestApiKey); + Assert.That(files.Count, Is.EqualTo(0)); + + RaygunSettings.Settings.MaxCrashReportsStoredOffline = 2; + + // Save two messages to storage. + Assert.IsTrue(_storageOne.Store("DummyData1", TestApiKey)); + Assert.IsTrue(_storageOne.Store("DummyData2", TestApiKey)); + + files = _storageOne.FetchAll(TestApiKey); + + // Ensure two files were created. + Assert.That(files.Count, Is.EqualTo(2)); + + // Remove the first file. + Assert.IsTrue(_storageOne.Remove(files.First().Name, TestApiKey)); + + // Ensure only one file remains. + files = _storageOne.FetchAll(TestApiKey); + Assert.That(files.Count, Is.EqualTo(1)); + } + + [Test] + public void Store_StoreUnderFirstKeyAndFetchWithSecondKey_NoFilesFoundForSecondKey() + { + const string apiKeyOne = "KEY_ONE"; + const string apiKeyTwo = "KEY_TWO"; + + // Ensure there are no files in storage under the first API key. + Assert.That(_storageOne.FetchAll(apiKeyOne).Count, Is.EqualTo(0)); + + // Ensure there are no files in storage under the second API key. + Assert.That(_storageTwo.FetchAll(apiKeyTwo).Count, Is.EqualTo(0)); + + RaygunSettings.Settings.MaxCrashReportsStoredOffline = 1; + + // Save one messages to storage under the first API key. + _storageOne.Store("DummyData1", apiKeyOne); + + // There should be one file under the first API key. + Assert.That(_storageOne.FetchAll(apiKeyOne).Count, Is.EqualTo(1)); + Assert.That(_storageTwo.FetchAll(apiKeyTwo).Count, Is.EqualTo(0)); + } + } +} \ No newline at end of file diff --git a/Mindscape.Raygun4Net.WebApi.Signed.nuspec b/Mindscape.Raygun4Net.WebApi.Signed.nuspec index 0d086bc9..b1752a48 100644 --- a/Mindscape.Raygun4Net.WebApi.Signed.nuspec +++ b/Mindscape.Raygun4Net.WebApi.Signed.nuspec @@ -2,7 +2,7 @@ <package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd"> <metadata minClientVersion="2.5"> <id>Mindscape.Raygun4Net.WebApi.Signed</id> - <version>5.10.3</version> + <version>5.11.0</version> <title>Raygun for ASP.NET Web API Raygun @@ -13,13 +13,13 @@ https://github.com/MindscapeHQ/raygun4net https://raw.github.com/MindscapeHQ/raygun4net/master/LICENSE - + - + diff --git a/Mindscape.Raygun4Net.WebApi.nuspec b/Mindscape.Raygun4Net.WebApi.nuspec index f6ff375a..107bb58a 100644 --- a/Mindscape.Raygun4Net.WebApi.nuspec +++ b/Mindscape.Raygun4Net.WebApi.nuspec @@ -2,7 +2,7 @@ Mindscape.Raygun4Net.WebApi - 5.10.3 + 5.11.0 Raygun for ASP.NET Web API Raygun @@ -13,13 +13,13 @@ https://github.com/MindscapeHQ/raygun4net https://raw.github.com/MindscapeHQ/raygun4net/master/LICENSE - + - + diff --git a/Mindscape.Raygun4Net.WebApi/Properties/AssemblyInfo.cs b/Mindscape.Raygun4Net.WebApi/Properties/AssemblyInfo.cs index 2ea52d64..370b1c76 100644 --- a/Mindscape.Raygun4Net.WebApi/Properties/AssemblyInfo.cs +++ b/Mindscape.Raygun4Net.WebApi/Properties/AssemblyInfo.cs @@ -1,8 +1,7 @@ using System.Reflection; -using System.Runtime.CompilerServices; using System.Runtime.InteropServices; -// General Information about an assembly is controlled through the following +// General Information about an assembly is controlled through the following // set of attributes. Change these attribute values to modify the information // associated with an assembly. [assembly: AssemblyTitle("Raygun4Net.WebApi")] @@ -14,8 +13,8 @@ [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from // COM, set the ComVisible attribute to true on that type. [assembly: ComVisible(false)] diff --git a/Mindscape.Raygun4Net.WebApi/Properties/AssemblyVersionInfo.cs b/Mindscape.Raygun4Net.WebApi/Properties/AssemblyVersionInfo.cs index 6af10067..a28f2cb5 100644 --- a/Mindscape.Raygun4Net.WebApi/Properties/AssemblyVersionInfo.cs +++ b/Mindscape.Raygun4Net.WebApi/Properties/AssemblyVersionInfo.cs @@ -10,5 +10,5 @@ // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("5.10.3.0")] -[assembly: AssemblyFileVersion("5.10.3.0")] +[assembly: AssemblyVersion("5.11.0.0")] +[assembly: AssemblyFileVersion("5.11.0.0")] diff --git a/Mindscape.Raygun4Net.WebApi/readme.txt b/Mindscape.Raygun4Net.WebApi/README.md similarity index 100% rename from Mindscape.Raygun4Net.WebApi/readme.txt rename to Mindscape.Raygun4Net.WebApi/README.md diff --git a/Mindscape.Raygun4Net.WebApi/RaygunWebApiClient.cs b/Mindscape.Raygun4Net.WebApi/RaygunWebApiClient.cs index 730d2a07..9ece8e8e 100644 --- a/Mindscape.Raygun4Net.WebApi/RaygunWebApiClient.cs +++ b/Mindscape.Raygun4Net.WebApi/RaygunWebApiClient.cs @@ -1,4 +1,5 @@ using System; +using System.Net; using System.Net.Http; using System.Reflection; using System.Threading; @@ -6,32 +7,42 @@ using System.Web.Http.Controllers; using System.Web.Http.Dispatcher; using System.Web.Http.ExceptionHandling; -using Mindscape.Raygun4Net.WebApi.Builders; +using System.Collections; using System.Collections.Generic; +using System.Linq; +using Mindscape.Raygun4Net.WebApi.Builders; using Mindscape.Raygun4Net.Messages; using Mindscape.Raygun4Net.Filters; -using System.Net; -using System.Collections; -using System.Linq; +using Mindscape.Raygun4Net.Logging; +using Mindscape.Raygun4Net.Storage; namespace Mindscape.Raygun4Net.WebApi { public class RaygunWebApiClient : RaygunClientBase { internal const string UnhandledExceptionTag = "UnhandledException"; - - protected readonly RaygunRequestMessageOptions _requestMessageOptions = new RaygunRequestMessageOptions(); - private readonly List _wrapperExceptions = new List(); + private static RaygunWebApiExceptionFilter _exceptionFilter; + private static RaygunWebApiActionFilter _actionFilter; + private static RaygunWebApiDelegatingHandler _delegatingHandler; + private static object _sendLock = new object(); + + private readonly List _wrapperExceptions = new List(); private readonly ThreadLocal _currentWebRequest = new ThreadLocal(() => null); private readonly ThreadLocal _currentRequestMessage = new ThreadLocal(() => null); - + private readonly string _apiKey; - private static RaygunWebApiExceptionFilter _exceptionFilter; - private static RaygunWebApiActionFilter _actionFilter; - private static RaygunWebApiDelegatingHandler _delegatingHandler; - + private IRaygunOfflineStorage _offlineStorage = new IsolatedRaygunOfflineStorage(); + + protected readonly RaygunRequestMessageOptions _requestMessageOptions = new RaygunRequestMessageOptions(); + + /// + /// Gets or sets the username/password credentials which are used to authenticate with the system default Proxy server, if one is set + /// and requires credentials. + /// + public ICredentials ProxyCredentials { get; set; } + /// /// Initializes a new instance of the class. /// Uses the ApiKey specified in the config file. @@ -59,7 +70,7 @@ public RaygunWebApiClient() public RaygunWebApiClient(string apiKey) { _apiKey = apiKey; - + ApplicationVersion = RaygunSettings.Settings.ApplicationVersion; if (string.IsNullOrEmpty(ApplicationVersion)) { @@ -68,10 +79,12 @@ public RaygunWebApiClient(string apiKey) // or else we will not be getting the user's library but our own Raygun4Net library. ApplicationVersion = Assembly.GetCallingAssembly()?.GetName()?.Version?.ToString(); } - + Init(); } + #region Attach/Detach Methods + /// /// Causes Raygun4Net to listen for exceptions. /// @@ -86,7 +99,7 @@ public static void Attach(HttpConfiguration config) // or else we will not be getting the user's library but our own Raygun4Net library. appVersion = Assembly.GetCallingAssembly()?.GetName()?.Version?.ToString(); } - + AttachInternal(config, null, appVersion); } @@ -105,7 +118,7 @@ public static void Attach(HttpConfiguration config, Func gen // or else we will not be getting the user's library but our own Raygun4Net library. appVersion = Assembly.GetCallingAssembly()?.GetName()?.Version?.ToString(); } - + if (generateRaygunClient != null) { AttachInternal(config, message => generateRaygunClient(), appVersion); @@ -145,7 +158,7 @@ private static void AttachInternal( string appVersion) { Detach(config); - + if (RaygunSettings.Settings.IsRawDataIgnored == false) { _delegatingHandler = new RaygunWebApiDelegatingHandler(); @@ -220,7 +233,7 @@ public static void Detach(HttpConfiguration config) _actionFilter = null; } } - + /// /// /// @@ -278,31 +291,9 @@ private void Init() UseKeyValuePairRawDataFilter = RaygunSettings.Settings.UseKeyValuePairRawDataFilter; } - /// - /// Gets or sets the username/password credentials which are used to authenticate with the system default Proxy server, if one is set - /// and requires credentials. - /// - public ICredentials ProxyCredentials { get; set; } - - protected override bool CanSend(Exception exception) - { - if (RaygunSettings.Settings.ExcludeErrorsFromLocal && _currentWebRequest.Value != null && _currentWebRequest.Value.IsLocal()) - { - return false; - } - - return base.CanSend(exception); - } - - protected bool CanSend(RaygunMessage message) - { - if (message != null && message.Details != null && message.Details.Response != null) - { - return !RaygunSettings.Settings.ExcludedStatusCodes.Contains(message.Details.Response.StatusCode); - } + #endregion // Attach/Detach Methods - return true; - } + #region Message Scrubbing Properties /// /// Adds a list of outer exceptions that will be stripped, leaving only the valuable inner exception. @@ -403,8 +394,8 @@ public void IgnoreServerVariableNames(params string[] names) } /// - /// Specifies whether or not RawData from web requests is ignored when sending reports to Raygun.io. - /// The default is false which means RawData will be sent to Raygun.io. + /// Specifies whether or not RawData from web requests is ignored when sending reports to Raygun. + /// The default is false which means RawData will be sent to Raygun. /// public bool IsRawDataIgnored { @@ -444,7 +435,7 @@ public bool UseKeyValuePairRawDataFilter /// /// Add an implementation to be used when capturing the raw data - /// of a HTTP request. This filter will be passed the request raw data and is expected to remove + /// of a HTTP request. This filter will be passed the request raw data and is expected to remove /// or replace values whose keys are found in the list supplied to the Filter method. /// /// Custom raw data filter implementation. @@ -453,8 +444,12 @@ public void AddRawDataFilter(IRaygunDataFilter filter) _requestMessageOptions.AddRawDataFilter(filter); } + #endregion // Message Scrubbing Properties + + #region Message Send Methods + /// - /// Transmits an exception to Raygun.io synchronously, using the version number of the originating assembly. + /// Transmits an exception to Raygun synchronously, using the version number of the originating assembly. /// /// The exception to deliver. public override void Send(Exception exception) @@ -463,7 +458,7 @@ public override void Send(Exception exception) } /// - /// Transmits an exception to Raygun.io synchronously specifying a list of string tags associated + /// Transmits an exception to Raygun synchronously specifying a list of string tags associated /// with the message for identification. This uses the version number of the originating assembly. /// /// The exception to deliver. @@ -474,7 +469,7 @@ public void Send(Exception exception, IList tags) } /// - /// Transmits an exception to Raygun.io synchronously specifying a list of string tags associated + /// Transmits an exception to Raygun synchronously specifying a list of string tags associated /// with the message for identification, as well as sending a key-value collection of custom data. /// This uses the version number of the originating assembly. /// @@ -488,12 +483,13 @@ public void Send(Exception exception, IList tags, IDictionary userCustom _currentRequestMessage.Value = BuildRequestMessage(); StripAndSend(exception, tags, userCustomData); + FlagAsSent(exception); } } /// - /// Asynchronously transmits a message to Raygun.io. + /// Asynchronously transmits a message to Raygun. /// /// The exception to deliver. public void SendInBackground(Exception exception) @@ -502,7 +498,7 @@ public void SendInBackground(Exception exception) } /// - /// Asynchronously transmits an exception to Raygun.io. + /// Asynchronously transmits an exception to Raygun. /// /// The exception to deliver. /// A list of strings associated with the message. @@ -512,7 +508,7 @@ public void SendInBackground(Exception exception, IList tags) } /// - /// Asynchronously transmits an exception to Raygun.io. + /// Asynchronously transmits an exception to Raygun. /// /// The exception to deliver. /// A list of strings associated with the message. @@ -531,12 +527,13 @@ public void SendInBackground(Exception exception, IList tags, IDictionar _currentRequestMessage.Value = currentRequestMessage; StripAndSend(exception, tags, userCustomData, currentTime); }); + FlagAsSent(exception); } } /// - /// Asynchronously transmits a message to Raygun.io. + /// Asynchronously transmits a message to Raygun. /// /// The RaygunMessage to send. This needs its OccurredOn property /// set to a valid DateTime and as much of the Details property as is available. @@ -545,11 +542,94 @@ public void SendInBackground(RaygunMessage raygunMessage) ThreadPool.QueueUserWorkItem(c => Send(raygunMessage)); } - internal void FlagExceptionAsSent(Exception exception) + /// + /// Posts a RaygunMessage to the Raygun API endpoint. + /// + /// The RaygunMessage to send. This needs its OccurredOn property + /// set to a valid DateTime and as much of the Details property as is available. + public override void Send(RaygunMessage raygunMessage) { - base.FlagAsSent(exception); + if (!ValidateApiKey()) + { + RaygunLogger.Instance.Warning("Failed to send error report due to invalid API key"); + return; + } + + bool canSend = OnSendingMessage(raygunMessage) && CanSend(raygunMessage); + + if (!canSend) + { + return; + } + + string message = null; + + try + { + message = SimpleJson.SerializeObject(raygunMessage); + } + catch (Exception ex) + { + RaygunLogger.Instance.Error($"Failed to serialize report due to: {ex.Message}"); + + if (RaygunSettings.Settings.ThrowOnError) + { + throw; + } + } + + if (string.IsNullOrEmpty(message)) + { + return; + } + + bool successfullySentReport = true; + + try + { + Send(message); + } + catch (Exception ex) + { + successfullySentReport = false; + + RaygunLogger.Instance.Error($"Failed to send report to Raygun due to: {ex.Message}"); + + SaveMessage(message); + + if (RaygunSettings.Settings.ThrowOnError) + { + throw; + } + } + + if (successfullySentReport) + { + SendStoredMessages(); + } + } + + private void Send(string message) + { + RaygunLogger.Instance.Verbose("Sending Payload --------------"); + RaygunLogger.Instance.Verbose(message); + RaygunLogger.Instance.Verbose("------------------------------"); + + WebClientHelper.Send(message, _apiKey, ProxyCredentials); + } + + private void StripAndSend(Exception exception, IList tags, IDictionary userCustomData, DateTime? currentTime = null) + { + foreach (Exception e in StripWrapperExceptions(exception)) + { + Send(BuildMessage(e, tags, userCustomData, currentTime)); + } } + #endregion // Message Send Methods + + #region Message Building Methods + private RaygunRequestMessage BuildRequestMessage() { var message = _currentWebRequest.Value != null ? RaygunWebApiRequestMessageBuilder.Build(_currentWebRequest.Value, _requestMessageOptions) : null; @@ -557,12 +637,6 @@ private RaygunRequestMessage BuildRequestMessage() return message; } - public RaygunWebApiClient SetCurrentHttpRequest(HttpRequestMessage request) - { - _currentWebRequest.Value = request; - return this; - } - protected RaygunMessage BuildMessage(Exception exception, IList tags, IDictionary userCustomData) { return BuildMessage(exception, tags, userCustomData, null); @@ -592,14 +666,6 @@ protected RaygunMessage BuildMessage(Exception exception, IList tags, ID return message; } - private void StripAndSend(Exception exception, IList tags, IDictionary userCustomData, DateTime? currentTime = null) - { - foreach (Exception e in StripWrapperExceptions(exception)) - { - Send(BuildMessage(e, tags, userCustomData, currentTime)); - } - } - protected IEnumerable StripWrapperExceptions(Exception exception) { if (exception != null && _wrapperExceptions.Any(wrapperException => exception.GetType() == wrapperException && (exception.InnerException != null || exception is ReflectionTypeLoadException))) @@ -654,38 +720,131 @@ protected IEnumerable StripWrapperExceptions(Exception exception) } } - /// - /// Posts a RaygunMessage to the Raygun.io api endpoint. - /// - /// The RaygunMessage to send. This needs its OccurredOn property - /// set to a valid DateTime and as much of the Details property as is available. - public override void Send(RaygunMessage raygunMessage) + #endregion // Message Building Methods + + #region Message Offline Storage + + private void SaveMessage(string message) { - try + if (!RaygunSettings.Settings.CrashReportingOfflineStorageEnabled) { - bool canSend = OnSendingMessage(raygunMessage) && CanSend(raygunMessage); - if (canSend) - { - var message = SimpleJson.SerializeObject(raygunMessage); - WebClientHelper.Send(message, _apiKey, ProxyCredentials); - } + RaygunLogger.Instance.Warning("Offline storage is disabled, skipping saving report."); + return; } - catch (Exception ex) + + if (!ValidateApiKey()) + { + RaygunLogger.Instance.Warning("Failed to save report due to invalid API key."); + return; + } + + // Avoid writing and reading from disk at the same time with `SendStoredMessages`. + lock (_sendLock) { try { - System.Diagnostics.Trace.WriteLine(string.Format("Error Logging Exception to Raygun.io {0}", ex.Message)); + if (!_offlineStorage.Store(message, _apiKey)) + { + RaygunLogger.Instance.Warning("Failed to save report to offline storage"); + } } - catch + catch (Exception ex) { - // ignored + RaygunLogger.Instance.Error($"Failed to save report to offline storage due to: {ex.Message}"); } + } + } - if (RaygunSettings.Settings.ThrowOnError) + private void SendStoredMessages() + { + if (!RaygunSettings.Settings.CrashReportingOfflineStorageEnabled) + { + RaygunLogger.Instance.Warning("Offline storage is disabled, skipping sending stored reports."); + return; + } + + if (!ValidateApiKey()) + { + RaygunLogger.Instance.Warning("Failed to send offline reports due to invalid API key."); + return; + } + + lock (_sendLock) + { + try { - throw; + var files = _offlineStorage.FetchAll(_apiKey); + + foreach (var file in files) + { + try + { + // Send the stored report. + Send(file.Contents); + + // Remove the stored report from local storage. + if (_offlineStorage.Remove(file.Name, _apiKey)) + { + RaygunLogger.Instance.Info("Successfully removed report from offline storage."); + } + else + { + RaygunLogger.Instance.Warning("Failed to remove report from offline storage."); + } + } + catch (Exception ex) + { + RaygunLogger.Instance.Error($"Failed to send stored report to Raygun due to: {ex.Message}"); + + // If just one message fails to send, then don't delete the message, + // and don't attempt sending anymore until later. + return; + } + } } + catch (Exception ex) + { + RaygunLogger.Instance.Error($"Failed to send stored report to Raygun due to: {ex.Message}"); + } + } + } + + #endregion // Message Offline Storage + + protected bool ValidateApiKey() + { + return !string.IsNullOrEmpty(_apiKey); + } + + protected override bool CanSend(Exception exception) + { + if (RaygunSettings.Settings.ExcludeErrorsFromLocal && _currentWebRequest.Value != null && _currentWebRequest.Value.IsLocal()) + { + return false; + } + + return base.CanSend(exception); + } + + protected bool CanSend(RaygunMessage message) + { + if (message?.Details?.Response != null) + { + return !RaygunSettings.Settings.ExcludedStatusCodes.Contains(message.Details.Response.StatusCode); } + + return true; + } + + internal void FlagExceptionAsSent(Exception exception) + { + base.FlagAsSent(exception); + } + + public RaygunWebApiClient SetCurrentHttpRequest(HttpRequestMessage request) + { + _currentWebRequest.Value = request; + return this; } } } \ No newline at end of file diff --git a/Mindscape.Raygun4Net.nuspec b/Mindscape.Raygun4Net.nuspec index 0e197ea8..038b5cd9 100644 --- a/Mindscape.Raygun4Net.nuspec +++ b/Mindscape.Raygun4Net.nuspec @@ -2,7 +2,7 @@ Mindscape.Raygun4Net - 5.10.2 + 5.11.0 <authors>Raygun</authors> <owners /> @@ -43,6 +43,6 @@ <file src="build\windowsphone\Mindscape.Raygun4Net.WindowsPhone.dll" target="lib\windowsphone8\Mindscape.Raygun4Net.WindowsPhone.dll" /> <file src="build\windowsphone\Mindscape.Raygun4Net.WindowsPhone.pdb" target="lib\windowsphone8\Mindscape.Raygun4Net.WindowsPhone.pdb" /> - <file src="readme.txt" /> + <file src="README.md" /> </files> </package> diff --git a/Mindscape.Raygun4Net/Builders/RaygunEnvironmentMessageBuilder.cs b/Mindscape.Raygun4Net/Builders/RaygunEnvironmentMessageBuilder.cs index 742b9174..137b7708 100644 --- a/Mindscape.Raygun4Net/Builders/RaygunEnvironmentMessageBuilder.cs +++ b/Mindscape.Raygun4Net/Builders/RaygunEnvironmentMessageBuilder.cs @@ -9,6 +9,7 @@ using System.Text; using System.Windows.Forms; using Microsoft.VisualBasic.Devices; +using Mindscape.Raygun4Net.Logging; using Mindscape.Raygun4Net.Messages; namespace Mindscape.Raygun4Net.Builders @@ -16,24 +17,24 @@ namespace Mindscape.Raygun4Net.Builders public class RaygunEnvironmentMessageBuilder { private static RaygunEnvironmentMessage _message; - + public static RaygunEnvironmentMessage Build() { bool mediumTrust = RaygunSettings.Settings.MediumTrust || !HasUnrestrictedFeatureSet; - + // // Gather the environment information that only needs to be collected once // - + if (_message == null) { _message = new RaygunEnvironmentMessage(); - + // Different environments can fail to load the environment details. // For now if they fail to load for whatever reason then just // swallow the exception. A good addition would be to handle // these cases and load them correctly depending on where its running. - // see http://raygun.io/forums/thread/3655 + // see http://raygun.com/forums/thread/3655 try { @@ -42,9 +43,9 @@ public static RaygunEnvironmentMessage Build() } catch (Exception ex) { - System.Diagnostics.Trace.WriteLine("Error retrieving window dimensions: {0}", ex.Message); + RaygunLogger.Instance.Error($"Error retrieving window dimensions: {ex.Message}"); } - + try { DateTime now = DateTime.Now; @@ -53,9 +54,9 @@ public static RaygunEnvironmentMessage Build() } catch (Exception ex) { - System.Diagnostics.Trace.WriteLine("Error retrieving time and locale: {0}", ex.Message); + RaygunLogger.Instance.Error($"Error retrieving time and locale: {ex.Message}"); } - + try { if (!mediumTrust) @@ -64,50 +65,50 @@ public static RaygunEnvironmentMessage Build() // moved to net40 minimum we can move this out of here _message.ProcessorCount = Environment.ProcessorCount; _message.Architecture = Environment.GetEnvironmentVariable("PROCESSOR_ARCHITECTURE"); - + ComputerInfo info = new ComputerInfo(); _message.TotalPhysicalMemory = info.TotalPhysicalMemory / 0x100000; // in MB _message.TotalVirtualMemory = info.TotalVirtualMemory / 0x100000; // in MB - + _message.Cpu = GetCpu(); _message.OSVersion = GetOSVersion(); } } catch (SecurityException) { - System.Diagnostics.Trace.WriteLine("RaygunClient error: couldn't access environment variables. If you are running in Medium Trust, in web.config in RaygunSettings set mediumtrust=\"true\""); + RaygunLogger.Instance.Error("RaygunClient error: couldn't access environment variables. If you are running in Medium Trust, in web.config in RaygunSettings set mediumtrust=\"true\""); } catch (Exception ex) { - System.Diagnostics.Trace.WriteLine("Error retrieving environment info: {0}", ex.Message); + RaygunLogger.Instance.Error($"Error retrieving environment info: {ex.Message}"); } } // // Gather the environment info that must be collected at the time of a report being generated. // - + try { if (!mediumTrust) { ComputerInfo info = new ComputerInfo(); - + _message.AvailablePhysicalMemory = info.AvailablePhysicalMemory / 0x100000; // in MB _message.AvailableVirtualMemory = info.AvailableVirtualMemory / 0x100000; // in MB - + _message.DiskSpaceFree = GetDiskSpace(); } } catch (SecurityException) { - System.Diagnostics.Trace.WriteLine("RaygunClient error: couldn't access environment variables. If you are running in Medium Trust, in web.config in RaygunSettings set mediumtrust=\"true\""); + RaygunLogger.Instance.Error("RaygunClient error: couldn't access environment variables. If you are running in Medium Trust, in web.config in RaygunSettings set mediumtrust=\"true\""); } catch (Exception ex) { - System.Diagnostics.Trace.WriteLine("Error retrieving environment info: {0}", ex.Message); + RaygunLogger.Instance.Error($"Error retrieving environment info: {ex.Message}"); } - + return _message; } @@ -125,7 +126,7 @@ private static string GetCpu() } catch (ManagementException ex) { - System.Diagnostics.Trace.WriteLine("Error retrieving CPU {0}", ex.Message); + RaygunLogger.Instance.Error($"Error retrieving CPU {ex.Message}"); } } return Environment.GetEnvironmentVariable("PROCESSOR_IDENTIFIER"); @@ -145,7 +146,7 @@ private static string GetOSVersion() } catch (ManagementException ex) { - System.Diagnostics.Trace.WriteLine("Error retrieving OSVersion {0}", ex.Message); + RaygunLogger.Instance.Error($"Error retrieving OSVersion {ex.Message}"); } } return Environment.OSVersion.Version.ToString(3); diff --git a/Mindscape.Raygun4Net/Builders/RaygunRequestMessageBuilder.cs b/Mindscape.Raygun4Net/Builders/RaygunRequestMessageBuilder.cs index 4508300b..61ddde1c 100644 --- a/Mindscape.Raygun4Net/Builders/RaygunRequestMessageBuilder.cs +++ b/Mindscape.Raygun4Net/Builders/RaygunRequestMessageBuilder.cs @@ -10,6 +10,7 @@ using System.Web; using Mindscape.Raygun4Net.Messages; using Mindscape.Raygun4Net.Filters; +using Mindscape.Raygun4Net.Logging; namespace Mindscape.Raygun4Net.Builders { @@ -44,7 +45,7 @@ public static RaygunRequestMessage Build(HttpRequest request, RaygunRequestMessa } catch (Exception e) { - System.Diagnostics.Trace.WriteLine("Failed to get basic request info: {0}", e.Message); + RaygunLogger.Instance.Error($"Failed to get basic request info: { e.Message}"); } return message; @@ -92,7 +93,7 @@ private static string GetIpAddress(HttpRequest request) } catch (Exception ex) { - System.Diagnostics.Trace.WriteLine("Failed to get IP address: {0}", ex.Message); + RaygunLogger.Instance.Error($"Failed to get IP address: {ex.Message}"); } return strIp; diff --git a/Mindscape.Raygun4Net/Logging/IRaygunLogger.cs b/Mindscape.Raygun4Net/Logging/IRaygunLogger.cs new file mode 100644 index 00000000..e3f110ab --- /dev/null +++ b/Mindscape.Raygun4Net/Logging/IRaygunLogger.cs @@ -0,0 +1,12 @@ + +namespace Mindscape.Raygun4Net.Logging +{ + public interface IRaygunLogger + { + void Error(string message); + void Warning(string message); + void Info(string message); + void Debug(string message); + void Verbose(string message); + } +} \ No newline at end of file diff --git a/Mindscape.Raygun4Net/Logging/RaygunLogLevel.cs b/Mindscape.Raygun4Net/Logging/RaygunLogLevel.cs new file mode 100644 index 00000000..b4bdc481 --- /dev/null +++ b/Mindscape.Raygun4Net/Logging/RaygunLogLevel.cs @@ -0,0 +1,12 @@ +namespace Mindscape.Raygun4Net.Logging +{ + public enum RaygunLogLevel + { + None = 0, + Error, + Warning, + Info, + Debug, + Verbose + } +} \ No newline at end of file diff --git a/Mindscape.Raygun4Net/Logging/RaygunLogger.cs b/Mindscape.Raygun4Net/Logging/RaygunLogger.cs new file mode 100644 index 00000000..3d06067a --- /dev/null +++ b/Mindscape.Raygun4Net/Logging/RaygunLogger.cs @@ -0,0 +1,56 @@ +using System; +using System.Diagnostics; +using Mindscape.Raygun4Net.Utils; + +namespace Mindscape.Raygun4Net.Logging +{ + public class RaygunLogger : Singleton<RaygunLogger>, IRaygunLogger + { + private const string RaygunPrefix = "Raygun: "; + + public void Error(string message) + { + Log(RaygunLogLevel.Error, message); + } + + public void Warning(string message) + { + Log(RaygunLogLevel.Warning, message); + } + + public void Info(string message) + { + Log(RaygunLogLevel.Info, message); + } + + public void Debug(string message) + { + Log(RaygunLogLevel.Debug, message); + } + + public void Verbose(string message) + { + Log(RaygunLogLevel.Verbose, message); + } + + private void Log(RaygunLogLevel level, string message) + { + if (RaygunSettings.Settings.LogLevel == RaygunLogLevel.None) + { + return; + } + + if (level <= RaygunSettings.Settings.LogLevel) + { + try + { + Trace.WriteLine($"{RaygunPrefix}{message}"); + } + catch + { + // ignored + } + } + } + } +} \ No newline at end of file diff --git a/Mindscape.Raygun4Net/Mindscape.Raygun4Net.csproj b/Mindscape.Raygun4Net/Mindscape.Raygun4Net.csproj index 35d37489..300159d8 100644 --- a/Mindscape.Raygun4Net/Mindscape.Raygun4Net.csproj +++ b/Mindscape.Raygun4Net/Mindscape.Raygun4Net.csproj @@ -77,6 +77,9 @@ <Compile Include="Builders\RaygunRequestMessageBuilder.cs" /> <Compile Include="IRaygunApplication.cs" /> <Compile Include="IRaygunMessageBuilder.cs" /> + <Compile Include="Logging\IRaygunLogger.cs" /> + <Compile Include="Logging\RaygunLogger.cs" /> + <Compile Include="Logging\RaygunLogLevel.cs" /> <Compile Include="Messages\RaygunClientMessage.cs" /> <Compile Include="Messages\RaygunEnvironmentMessage.cs" /> <Compile Include="Messages\RaygunErrorStackTraceLineMessage.cs" /> @@ -115,6 +118,11 @@ <Compile Include="Filters\IRaygunDataFilter.cs" /> <Compile Include="Filters\RaygunKeyValuePairDataFilter.cs" /> <Compile Include="Filters\RaygunXmlDataFilter.cs" /> + <Compile Include="Storage\IRaygunFile.cs" /> + <Compile Include="Storage\IRaygunOfflineStorage.cs" /> + <Compile Include="Storage\RaygunFile.cs" /> + <Compile Include="Storage\IsolatedRaygunOfflineStorage.cs" /> + <Compile Include="Utils\Singleton.cs" /> </ItemGroup> <ItemGroup> <None Include="app.config" /> diff --git a/Mindscape.Raygun4Net/ProfilingSupport/HttpApplicationInitializer.cs b/Mindscape.Raygun4Net/ProfilingSupport/HttpApplicationInitializer.cs index 648239b1..f59bcfd4 100644 --- a/Mindscape.Raygun4Net/ProfilingSupport/HttpApplicationInitializer.cs +++ b/Mindscape.Raygun4Net/ProfilingSupport/HttpApplicationInitializer.cs @@ -2,6 +2,7 @@ using System.IO; using System.Threading; using System.Web; +using Mindscape.Raygun4Net.Logging; using Mindscape.Raygun4Net.ProfilingSupport; namespace Mindscape.Raygun4Net @@ -42,7 +43,7 @@ private void BeginRequest(object sender, EventArgs e) } catch (Exception ex) { - System.Diagnostics.Trace.WriteLine($"Error during begin request of APM sampling: {ex.Message}"); + RaygunLogger.Instance.Error($"Error during begin request of APM sampling: {ex.Message}"); } } @@ -57,7 +58,7 @@ private void EndRequest(object sender, EventArgs e) } catch (Exception ex) { - System.Diagnostics.Trace.WriteLine($"Error during end request of APM sampling: {ex.Message}"); + RaygunLogger.Instance.Error($"Error during end request of APM sampling: {ex.Message}"); } } @@ -71,7 +72,7 @@ private bool InitProfilingSupport() { if (APM.ProfilerAttached) { - System.Diagnostics.Trace.WriteLine("Detected Raygun APM profiler is attached, initializing profiler support."); + RaygunLogger.Instance.Info("Detected Raygun APM profiler is attached, initializing profiler support."); _samplingManager = new SamplingManager(); _refreshTimer = new Timer(RefreshAgentSettings, null, TimeSpan.Zero, AgentPollingDelay); @@ -87,7 +88,7 @@ private bool InitProfilingSupport() } catch (Exception ex) { - System.Diagnostics.Trace.WriteLine($"Error initialising APM profiler support: {ex.Message}"); + RaygunLogger.Instance.Error($"Error initialising APM profiler support: {ex.Message}"); } return false; @@ -116,17 +117,17 @@ private void RefreshAgentSettings(object state) } else { - System.Diagnostics.Trace.WriteLine($"Could not locate sampling settings for site {_appIdentifier}"); + RaygunLogger.Instance.Warning($"Could not locate sampling settings for site {_appIdentifier}"); } } else { - System.Diagnostics.Trace.WriteLine($"Could not locate Raygun APM configuration file {SettingsFilePath}"); + RaygunLogger.Instance.Warning($"Could not locate Raygun APM configuration file {SettingsFilePath}"); } } catch (Exception ex) { - System.Diagnostics.Trace.WriteLine($"Error refreshing agent settings: {ex.Message}"); + RaygunLogger.Instance.Error($"Error refreshing agent settings: {ex.Message}"); } } } diff --git a/Mindscape.Raygun4Net/Properties/AssemblyInfo.cs b/Mindscape.Raygun4Net/Properties/AssemblyInfo.cs index 4c93dc56..5d6b0337 100644 --- a/Mindscape.Raygun4Net/Properties/AssemblyInfo.cs +++ b/Mindscape.Raygun4Net/Properties/AssemblyInfo.cs @@ -1,8 +1,7 @@ using System.Reflection; -using System.Runtime.CompilerServices; using System.Runtime.InteropServices; -// General Information about an assembly is controlled through the following +// General Information about an assembly is controlled through the following // set of attributes. Change these attribute values to modify the information // associated with an assembly. [assembly: AssemblyTitle("Raygun4Net")] @@ -10,12 +9,12 @@ [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("Raygun")] [assembly: AssemblyProduct("Raygun4Net")] -[assembly: AssemblyCopyright("Copyright © Raygun 2012-2019")] +[assembly: AssemblyCopyright("Copyright © Raygun 2012-2020")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from // COM, set the ComVisible attribute to true on that type. [assembly: ComVisible(false)] diff --git a/Mindscape.Raygun4Net/RaygunClient.cs b/Mindscape.Raygun4Net/RaygunClient.cs index 9f0a22c9..c574656d 100644 --- a/Mindscape.Raygun4Net/RaygunClient.cs +++ b/Mindscape.Raygun4Net/RaygunClient.cs @@ -3,15 +3,13 @@ using System.Collections.Generic; using System.Linq; using System.Net; -using Mindscape.Raygun4Net.Messages; - using System.Web; using System.Threading; using System.Reflection; +using Mindscape.Raygun4Net.Messages; using Mindscape.Raygun4Net.Builders; -using System.IO; -using System.IO.IsolatedStorage; -using System.Text; +using Mindscape.Raygun4Net.Logging; +using Mindscape.Raygun4Net.Storage; namespace Mindscape.Raygun4Net { @@ -19,12 +17,35 @@ public class RaygunClient : RaygunClientBase { internal const string UnhandledExceptionTag = "UnhandledException"; + [ThreadStatic] + private static RaygunRequestMessage _currentRequestMessage; + private static object _sendLock = new object(); + private readonly string _apiKey; private readonly RaygunRequestMessageOptions _requestMessageOptions = new RaygunRequestMessageOptions(); private readonly List<Type> _wrapperExceptions = new List<Type>(); - [ThreadStatic] - private static RaygunRequestMessage _currentRequestMessage; + private IRaygunOfflineStorage _offlineStorage = new IsolatedRaygunOfflineStorage(); + + /// <summary> + /// Gets or sets the username/password credentials which are used to authenticate with the system default Proxy server, if one is set + /// and requires credentials. + /// </summary> + public ICredentials ProxyCredentials { get; set; } + + /// <summary> + /// Gets or sets an IWebProxy instance which can be used to override the default system proxy server settings + /// </summary> + public IWebProxy WebProxy { get; set; } + + /// <summary> + /// Initializes a new instance of the <see cref="RaygunClient" /> class. + /// Uses the ApiKey specified in the config file. + /// </summary> + public RaygunClient() + : this(RaygunSettings.Settings.ApiKey) + { + } /// <summary> /// Initializes a new instance of the <see cref="RaygunClient" /> class. @@ -42,56 +63,30 @@ public RaygunClient(string apiKey) var ignoredNames = RaygunSettings.Settings.IgnoreFormFieldNames.Split(','); IgnoreFormFieldNames(ignoredNames); } + if (!string.IsNullOrEmpty(RaygunSettings.Settings.IgnoreHeaderNames)) { var ignoredNames = RaygunSettings.Settings.IgnoreHeaderNames.Split(','); IgnoreHeaderNames(ignoredNames); } + if (!string.IsNullOrEmpty(RaygunSettings.Settings.IgnoreCookieNames)) { var ignoredNames = RaygunSettings.Settings.IgnoreCookieNames.Split(','); IgnoreCookieNames(ignoredNames); } + if (!string.IsNullOrEmpty(RaygunSettings.Settings.IgnoreServerVariableNames)) { var ignoredNames = RaygunSettings.Settings.IgnoreServerVariableNames.Split(','); IgnoreServerVariableNames(ignoredNames); } + IsRawDataIgnored = RaygunSettings.Settings.IsRawDataIgnored; ThreadPool.QueueUserWorkItem(state => { SendStoredMessages(); }); } - /// <summary> - /// Initializes a new instance of the <see cref="RaygunClient" /> class. - /// Uses the ApiKey specified in the config file. - /// </summary> - public RaygunClient() - : this(RaygunSettings.Settings.ApiKey) - { - } - - protected bool ValidateApiKey() - { - if (string.IsNullOrEmpty(_apiKey)) - { - System.Diagnostics.Debug.WriteLine("ApiKey has not been provided, exception will not be logged"); - return false; - } - return true; - } - - /// <summary> - /// Gets or sets the username/password credentials which are used to authenticate with the system default Proxy server, if one is set - /// and requires credentials. - /// </summary> - public ICredentials ProxyCredentials { get; set; } - - /// <summary> - /// Gets or sets an IWebProxy instance which can be used to override the default system proxy server settings - /// </summary> - public IWebProxy WebProxy { get; set; } - /// <summary> /// Adds a list of outer exceptions that will be stripped, leaving only the valuable inner exception. /// This can be used when a wrapper exception, e.g. TargetInvocationException or HttpUnhandledException, @@ -124,6 +119,8 @@ public void RemoveWrapperExceptions(params Type[] wrapperExceptions) } } + #region Message Scrubbing Properties + /// <summary> /// Adds a list of keys to ignore when attaching the Form data of an HTTP POST request. This allows /// you to remove sensitive data from the transmitted copy of the Form on the HttpRequest by specifying the keys you want removed. @@ -169,8 +166,8 @@ public void IgnoreServerVariableNames(params string[] names) } /// <summary> - /// Specifies whether or not RawData from web requests is ignored when sending reports to Raygun.io. - /// The default is false which means RawData will be sent to Raygun.io. + /// Specifies whether or not RawData from web requests is ignored when sending reports to Raygun. + /// The default is false which means RawData will be sent to Raygun. /// </summary> public bool IsRawDataIgnored { @@ -181,30 +178,12 @@ public bool IsRawDataIgnored } } - protected override bool CanSend(Exception exception) - { - if (RaygunSettings.Settings.ExcludeErrorsFromLocal && HttpContext.Current != null) - { - try - { - if (HttpContext.Current.Request.IsLocal) - { - return false; - } - } - catch - { - if (RaygunSettings.Settings.ThrowOnError) - { - throw; - } - } - } - return base.CanSend(exception); - } + #endregion // Message Scrubbing Properties + + #region Message Send Methods /// <summary> - /// Transmits an exception to Raygun.io synchronously, using the version number of the originating assembly. + /// Transmits an exception to Raygun synchronously, using the version number of the originating assembly. /// </summary> /// <param name="exception">The exception to deliver.</param> public override void Send(Exception exception) @@ -213,7 +192,7 @@ public override void Send(Exception exception) } /// <summary> - /// Transmits an exception to Raygun.io synchronously specifying a list of string tags associated + /// Transmits an exception to Raygun synchronously specifying a list of string tags associated /// with the message for identification. This uses the version number of the originating assembly. /// </summary> /// <param name="exception">The exception to deliver.</param> @@ -224,7 +203,7 @@ public void Send(Exception exception, IList<string> tags) } /// <summary> - /// Transmits an exception to Raygun.io synchronously specifying a list of string tags associated + /// Transmits an exception to Raygun synchronously specifying a list of string tags associated /// with the message for identification, as well as sending a key-value collection of custom data. /// This uses the version number of the originating assembly. /// </summary> @@ -237,7 +216,7 @@ public void Send(Exception exception, IList<string> tags, IDictionary userCustom } /// <summary> - /// Transmits an exception to Raygun.io synchronously specifying a list of string tags associated + /// Transmits an exception to Raygun synchronously specifying a list of string tags associated /// with the message for identification, as well as sending a key-value collection of custom data. /// This uses the version number of the originating assembly. /// </summary> @@ -252,12 +231,13 @@ public void Send(Exception exception, IList<string> tags, IDictionary userCustom _currentRequestMessage = BuildRequestMessage(); Send(BuildMessage(exception, tags, userCustomData, userInfo, null)); + FlagAsSent(exception); } } /// <summary> - /// Asynchronously transmits a message to Raygun.io. + /// Asynchronously transmits a message to Raygun. /// </summary> /// <param name="exception">The exception to deliver.</param> public void SendInBackground(Exception exception) @@ -266,7 +246,7 @@ public void SendInBackground(Exception exception) } /// <summary> - /// Asynchronously transmits an exception to Raygun.io. + /// Asynchronously transmits an exception to Raygun. /// </summary> /// <param name="exception">The exception to deliver.</param> /// <param name="tags">A list of strings associated with the message.</param> @@ -276,7 +256,7 @@ public void SendInBackground(Exception exception, IList<string> tags) } /// <summary> - /// Asynchronously transmits an exception to Raygun.io. + /// Asynchronously transmits an exception to Raygun. /// </summary> /// <param name="exception">The exception to deliver.</param> /// <param name="tags">A list of strings associated with the message.</param> @@ -287,7 +267,7 @@ public void SendInBackground(Exception exception, IList<string> tags, IDictionar } /// <summary> - /// Asynchronously transmits an exception to Raygun.io. + /// Asynchronously transmits an exception to Raygun. /// </summary> /// <param name="exception">The exception to deliver.</param> /// <param name="tags">A list of strings associated with the message.</param> @@ -301,7 +281,7 @@ public void SendInBackground(Exception exception, IList<string> tags, IDictionar // otherwise it will be disposed while we are using it on the other thread. RaygunRequestMessage currentRequestMessage = BuildRequestMessage(); DateTime currentTime = DateTime.UtcNow; - + ThreadPool.QueueUserWorkItem(c => { try @@ -324,7 +304,7 @@ public void SendInBackground(Exception exception, IList<string> tags, IDictionar } /// <summary> - /// Asynchronously transmits a message to Raygun.io. + /// Asynchronously transmits a message to Raygun. /// </summary> /// <param name="raygunMessage">The RaygunMessage to send. This needs its OccurredOn property /// set to a valid DateTime and as much of the Details property as is available.</param> @@ -333,10 +313,94 @@ public void SendInBackground(RaygunMessage raygunMessage) ThreadPool.QueueUserWorkItem(c => Send(raygunMessage)); } + /// <summary> + /// Posts a RaygunMessage to the Raygun API endpoint. + /// </summary> + /// <param name="raygunMessage">The RaygunMessage to send. This needs its OccurredOn property + /// set to a valid DateTime and as much of the Details property as is available.</param> + public override void Send(RaygunMessage raygunMessage) + { + if (!ValidateApiKey()) + { + RaygunLogger.Instance.Warning("Failed to send error report due to invalid API key"); + return; + } + + bool canSend = OnSendingMessage(raygunMessage); + + if (!canSend) + { + return; + } + + string message = null; + + try + { + message = SimpleJson.SerializeObject(raygunMessage); + } + catch (Exception ex) + { + RaygunLogger.Instance.Error($"Failed to serialize report due to: {ex.Message}"); + + if (RaygunSettings.Settings.ThrowOnError) + { + throw; + } + } + + if (string.IsNullOrEmpty(message)) + { + return; + } + + bool successfullySentReport = true; + + try + { + Send(message); + } + catch (Exception ex) + { + successfullySentReport = false; + + RaygunLogger.Instance.Error($"Failed to send report to Raygun due to: {ex.Message}"); + + SaveMessage(message); + + if (RaygunSettings.Settings.ThrowOnError) + { + throw; + } + } + + if (successfullySentReport) + { + SendStoredMessages(); + } + } + + private void Send(string message) + { + RaygunLogger.Instance.Verbose("Sending Payload --------------"); + RaygunLogger.Instance.Verbose(message); + RaygunLogger.Instance.Verbose("------------------------------"); + + using (var client = CreateWebClient()) + { + client.UploadString(RaygunSettings.Settings.ApiEndpoint, message); + } + } + + #endregion // Message Send Methods + + #region Message Building Methods + private RaygunRequestMessage BuildRequestMessage() { RaygunRequestMessage requestMessage = null; HttpContext context = HttpContext.Current; + if (context != null) { HttpRequest request = null; @@ -346,7 +410,7 @@ private RaygunRequestMessage BuildRequestMessage() } catch (HttpException ex) { - System.Diagnostics.Trace.WriteLine("Error retrieving HttpRequest {0}", ex.Message); + RaygunLogger.Instance.Error($"Failed to retrieve the HttpRequest due to: {ex.Message}"); } if (request != null) @@ -404,62 +468,122 @@ private Exception StripWrapperExceptions(Exception exception) return exception; } - /// <summary> - /// Posts a RaygunMessage to the Raygun.io api endpoint. - /// </summary> - /// <param name="raygunMessage">The RaygunMessage to send. This needs its OccurredOn property - /// set to a valid DateTime and as much of the Details property as is available.</param> - public override void Send(RaygunMessage raygunMessage) + #endregion // Message Building Methods + + #region Message Offline Storage + + private void SaveMessage(string message) { - bool canSend = OnSendingMessage(raygunMessage); - if (canSend) + if (!RaygunSettings.Settings.CrashReportingOfflineStorageEnabled) + { + RaygunLogger.Instance.Warning("Offline storage is disabled, skipping saving report."); + return; + } + + if (!ValidateApiKey()) + { + RaygunLogger.Instance.Warning("Failed to save report due to invalid API key."); + return; + } + + // Avoid writing and reading from disk at the same time with `SendStoredMessages`. + lock (_sendLock) { - string message = null; try { - message = SimpleJson.SerializeObject(raygunMessage); + if (!_offlineStorage.Store(message, _apiKey)) + { + RaygunLogger.Instance.Warning("Failed to save report to offline storage"); + } } catch (Exception ex) { - System.Diagnostics.Trace.WriteLine(string.Format("Error serializing exception {0}", ex.Message)); - - if (RaygunSettings.Settings.ThrowOnError) - { - throw; - } + RaygunLogger.Instance.Error($"Failed to save report to offline storage due to: {ex.Message}"); } + } + } - if (message != null) + private void SendStoredMessages() + { + if (!RaygunSettings.Settings.CrashReportingOfflineStorageEnabled) + { + RaygunLogger.Instance.Warning("Offline storage is disabled, skipping sending stored reports."); + return; + } + + if (!ValidateApiKey()) + { + RaygunLogger.Instance.Warning("Failed to send offline reports due to invalid API key."); + return; + } + + lock (_sendLock) + { + try { - try - { - Send(message); - } - catch (Exception ex) + var files = _offlineStorage.FetchAll(_apiKey); + + foreach (var file in files) { - SaveMessage(message); - System.Diagnostics.Trace.WriteLine(string.Format("Error Logging Exception to Raygun.io {0}", ex.Message)); + try + { + // Send the stored report. + Send(file.Contents); - if (RaygunSettings.Settings.ThrowOnError) + // Remove the stored report from local storage. + if (_offlineStorage.Remove(file.Name, _apiKey)) + { + RaygunLogger.Instance.Info("Successfully removed report from offline storage."); + } + else + { + RaygunLogger.Instance.Warning("Failed to remove report from offline storage."); + } + } + catch (Exception ex) { - throw; + RaygunLogger.Instance.Error($"Failed to send stored report to Raygun due to: {ex.Message}"); + + // If just one message fails to send, then don't delete the message, + // and don't attempt sending anymore until later. + return; } } - - SendStoredMessages(); + } + catch (Exception ex) + { + RaygunLogger.Instance.Error($"Failed to send stored report to Raygun due to: {ex.Message}"); } } } - private void Send(string message) + #endregion // Message Offline Storage + + protected bool ValidateApiKey() { - if (ValidateApiKey()) + return !string.IsNullOrEmpty(_apiKey); + } + + protected override bool CanSend(Exception exception) + { + if (RaygunSettings.Settings.ExcludeErrorsFromLocal && HttpContext.Current != null) { - using (var client = CreateWebClient()) + try { - client.UploadString(RaygunSettings.Settings.ApiEndpoint, message); + if (HttpContext.Current.Request.IsLocal) + { + return false; + } + } + catch + { + if (RaygunSettings.Settings.ThrowOnError) + { + throw; + } } } + return base.CanSend(exception); } protected WebClient CreateWebClient() @@ -495,135 +619,5 @@ protected WebClient CreateWebClient() } return client; } - - private void SaveMessage(string message) - { - try - { - using (IsolatedStorageFile isolatedStorage = GetIsolatedStorageScope()) - { - string directoryName = "RaygunOfflineStorage"; - string[] directories = isolatedStorage.GetDirectoryNames("*"); - if (!FileExists(directories, directoryName)) - { - isolatedStorage.CreateDirectory(directoryName); - } - - int number = 1; - string[] files = isolatedStorage.GetFileNames(directoryName + "\\*.txt"); - while (true) - { - bool exists = FileExists(files, "RaygunErrorMessage" + number + ".txt"); - if (!exists) - { - string nextFileName = "RaygunErrorMessage" + (number + 1) + ".txt"; - exists = FileExists(files, nextFileName); - if (exists) - { - isolatedStorage.DeleteFile(directoryName + "\\" + nextFileName); - } - break; - } - number++; - } - - if (number == 11) - { - string firstFileName = "RaygunErrorMessage1.txt"; - if (FileExists(files, firstFileName)) - { - isolatedStorage.DeleteFile(directoryName + "\\" + firstFileName); - } - } - using (IsolatedStorageFileStream isoStream = new IsolatedStorageFileStream(directoryName + "\\RaygunErrorMessage" + number + ".txt", FileMode.OpenOrCreate, FileAccess.Write, isolatedStorage)) - { - using (StreamWriter writer = new StreamWriter(isoStream, Encoding.Unicode)) - { - writer.Write(message); - writer.Flush(); - writer.Close(); - } - } - System.Diagnostics.Trace.WriteLine("Saved message: " + "RaygunErrorMessage" + number + ".txt"); - } - } - catch (Exception ex) - { - System.Diagnostics.Trace.WriteLine(string.Format("Error saving message to isolated storage {0}", ex.Message)); - } - } - - private bool FileExists(string[] files, string fileName) - { - foreach (string str in files) - { - if (fileName.Equals(str)) - { - return true; - } - } - return false; - } - - private static object _sendLock = new object(); - - private void SendStoredMessages() - { - lock (_sendLock) - { - try - { - using (IsolatedStorageFile isolatedStorage = GetIsolatedStorageScope()) - { - string directoryName = "RaygunOfflineStorage"; - string[] directories = isolatedStorage.GetDirectoryNames("*"); - if (FileExists(directories, directoryName)) - { - string[] fileNames = isolatedStorage.GetFileNames(directoryName + "\\*.txt"); - foreach (string name in fileNames) - { - IsolatedStorageFileStream isoFileStream = new IsolatedStorageFileStream(directoryName + "\\" + name, FileMode.Open, isolatedStorage); - using (StreamReader reader = new StreamReader(isoFileStream)) - { - string text = reader.ReadToEnd(); - try - { - Send(text); - } - catch - { - // If just one message fails to send, then don't delete the message, and don't attempt sending anymore until later. - return; - } - System.Diagnostics.Debug.WriteLine("Sent " + name); - } - isolatedStorage.DeleteFile(directoryName + "\\" + name); - } - if (isolatedStorage.GetFileNames(directoryName + "\\*.txt").Length == 0) - { - System.Diagnostics.Debug.WriteLine("Successfully sent all pending messages"); - isolatedStorage.DeleteDirectory(directoryName); - } - } - } - } - catch (Exception ex) - { - System.Diagnostics.Debug.WriteLine(string.Format("Error sending stored messages to Raygun.io {0}", ex.Message)); - } - } - } - - private IsolatedStorageFile GetIsolatedStorageScope() - { - if (AppDomain.CurrentDomain != null && AppDomain.CurrentDomain.ActivationContext != null) - { - return IsolatedStorageFile.GetUserStoreForApplication(); - } - else - { - return IsolatedStorageFile.GetUserStoreForAssembly(); - } - } } } diff --git a/Mindscape.Raygun4Net/RaygunClientBase.cs b/Mindscape.Raygun4Net/RaygunClientBase.cs index 4bf0633c..54598f11 100644 --- a/Mindscape.Raygun4Net/RaygunClientBase.cs +++ b/Mindscape.Raygun4Net/RaygunClientBase.cs @@ -1,13 +1,27 @@ using System; using System.Net; +using Mindscape.Raygun4Net.Logging; using Mindscape.Raygun4Net.Messages; namespace Mindscape.Raygun4Net { public abstract class RaygunClientBase { + private bool _handlingRecursiveErrorSending; + private bool _handlingRecursiveGrouping; + protected internal const string SentKey = "AlreadySentByRaygun"; + /// <summary> + /// Raised just before a message is sent. This can be used to make final adjustments to the <see cref="RaygunMessage"/>, or to cancel the send. + /// </summary> + public event EventHandler<RaygunSendingMessageEventArgs> SendingMessage; + + /// <summary> + /// Raised before a message is sent. This can be used to add a custom grouping key to a RaygunMessage before sending it to the Raygun service. + /// </summary> + public event EventHandler<RaygunCustomGroupingKeyEventArgs> CustomGroupingKey; + /// <summary> /// Gets or sets the user identity string. /// </summary> @@ -28,6 +42,19 @@ public abstract class RaygunClientBase /// </summary> public string ApplicationVersion { get; set; } + /// <summary> + /// Transmits an exception to Raygun synchronously. + /// </summary> + /// <param name="exception">The exception to deliver.</param> + public abstract void Send(Exception exception); + + /// <summary> + /// Posts a RaygunMessage to the Raygun API endpoint. + /// </summary> + /// <param name="raygunMessage">The RaygunMessage to send. This needs its OccurredOn property + /// set to a valid DateTime and as much of the Details property as is available.</param> + public abstract void Send(RaygunMessage raygunMessage); + protected virtual bool CanSend(Exception exception) { return exception == null || exception.Data == null || !exception.Data.Contains(SentKey) || false.Equals(exception.Data[SentKey]); @@ -47,18 +74,11 @@ protected void FlagAsSent(Exception exception) } catch (Exception ex) { - System.Diagnostics.Debug.WriteLine(String.Format("Failed to flag exception as sent: {0}", ex.Message)); + RaygunLogger.Instance.Debug($"Failed to flag exception as sent: {ex.Message}"); } } } - /// <summary> - /// Raised just before a message is sent. This can be used to make final adjustments to the <see cref="RaygunMessage"/>, or to cancel the send. - /// </summary> - public event EventHandler<RaygunSendingMessageEventArgs> SendingMessage; - - private bool _handlingRecursiveErrorSending; - // Returns true if the message can be sent, false if the sending is canceled. protected bool OnSendingMessage(RaygunMessage raygunMessage) { @@ -89,12 +109,6 @@ protected bool OnSendingMessage(RaygunMessage raygunMessage) return result; } - /// <summary> - /// Raised before a message is sent. This can be used to add a custom grouping key to a RaygunMessage before sending it to the Raygun service. - /// </summary> - public event EventHandler<RaygunCustomGroupingKeyEventArgs> CustomGroupingKey; - - private bool _handlingRecursiveGrouping; protected string OnCustomGroupingKey(Exception exception, RaygunMessage message) { string result = null; @@ -119,18 +133,5 @@ protected string OnCustomGroupingKey(Exception exception, RaygunMessage message) } return result; } - - /// <summary> - /// Transmits an exception to Raygun.io synchronously. - /// </summary> - /// <param name="exception">The exception to deliver.</param> - public abstract void Send(Exception exception); - - /// <summary> - /// Posts a RaygunMessage to the Raygun.io api endpoint. - /// </summary> - /// <param name="raygunMessage">The RaygunMessage to send. This needs its OccurredOn property - /// set to a valid DateTime and as much of the Details property as is available.</param> - public abstract void Send(RaygunMessage raygunMessage); } } diff --git a/Mindscape.Raygun4Net/RaygunMessageBuilder.cs b/Mindscape.Raygun4Net/RaygunMessageBuilder.cs index b892265f..35b0f235 100644 --- a/Mindscape.Raygun4Net/RaygunMessageBuilder.cs +++ b/Mindscape.Raygun4Net/RaygunMessageBuilder.cs @@ -7,6 +7,7 @@ using System.Reflection; using System.Web; using Mindscape.Raygun4Net.Builders; +using Mindscape.Raygun4Net.Logging; using Mindscape.Raygun4Net.Messages; namespace Mindscape.Raygun4Net @@ -92,7 +93,7 @@ public IRaygunMessageBuilder SetExceptionDetails(Exception exception) } catch (Exception ex) { - System.Diagnostics.Trace.WriteLine("Error retrieving response info {0}", ex.Message); + RaygunLogger.Instance.Error($"Error retrieving response info {ex.Message}"); } return this; diff --git a/Mindscape.Raygun4Net/RaygunSettings.cs b/Mindscape.Raygun4Net/RaygunSettings.cs index e4474466..0f9c52d8 100644 --- a/Mindscape.Raygun4Net/RaygunSettings.cs +++ b/Mindscape.Raygun4Net/RaygunSettings.cs @@ -2,6 +2,7 @@ using System.Configuration; using System.Linq; using Mindscape.Raygun4Net.Breadcrumbs; +using Mindscape.Raygun4Net.Logging; namespace Mindscape.Raygun4Net { @@ -11,6 +12,8 @@ public class RaygunSettings : ConfigurationSection private const string DefaultApiEndPoint = "https://api.raygun.com/entries"; + public const int MaxCrashReportsStoredOfflineHardLimit = 64; + public static RaygunSettings Settings { get { return settings; } @@ -170,6 +173,42 @@ public string ApplicationIdentifier set { this["applicationIdentifier"] = value; } } + /// <summary> + /// Gets or sets the max crash reports stored on the device. + /// There is a hard upper limit of 64 reports. + /// </summary> + /// <value>The max crash reports stored on device.</value> + [ConfigurationProperty("maxCrashReportsStoredOffline", IsRequired = false, DefaultValue = MaxCrashReportsStoredOfflineHardLimit)] + public int MaxCrashReportsStoredOffline + { + get { return (int)this["maxCrashReportsStoredOffline"]; } + set { this["maxCrashReportsStoredOffline"] = value; } + } + + /// <summary> + /// Allows for crash reports to be stored to local storage when there is no available network connection. + /// </summary> + /// <value><c>true</c> if allowing crash reports to be stored offline; otherwise, <c>false</c>.</value> + [ConfigurationProperty("crashReportingOfflineStorageEnabled", IsRequired = false, DefaultValue = true)] + public bool CrashReportingOfflineStorageEnabled + { + get { return (bool)this["crashReportingOfflineStorageEnabled"]; } + set { this["crashReportingOfflineStorageEnabled"] = value; } + } + + /// <summary> + /// Gets or sets the log level controlling the amount of information printed to system consoles. + /// Setting the level to <see cref="RaygunLogLevel.Verbose"/> will print the raw Crash Reporting being + /// posted to the API endpoints. + /// </summary> + /// <value>The log level.</value> + [ConfigurationProperty("logLevel", IsRequired = false, DefaultValue = RaygunLogLevel.Warning)] + public RaygunLogLevel LogLevel + { + get { return (RaygunLogLevel)this["logLevel"]; } + set { this["logLevel"] = value; } + } + /// <summary> /// Return false. /// </summary> diff --git a/Mindscape.Raygun4Net/Storage/IRaygunFile.cs b/Mindscape.Raygun4Net/Storage/IRaygunFile.cs new file mode 100644 index 00000000..8b2a1105 --- /dev/null +++ b/Mindscape.Raygun4Net/Storage/IRaygunFile.cs @@ -0,0 +1,15 @@ +namespace Mindscape.Raygun4Net.Storage +{ + public interface IRaygunFile + { + /// <summary> + /// The filename as seen in local storage. + /// </summary> + string Name { get; } + + /// <summary> + /// The raw data stored locally. + /// </summary> + string Contents { get; } + } +} \ No newline at end of file diff --git a/Mindscape.Raygun4Net/Storage/IRaygunOfflineStorage.cs b/Mindscape.Raygun4Net/Storage/IRaygunOfflineStorage.cs new file mode 100644 index 00000000..51c9f5f6 --- /dev/null +++ b/Mindscape.Raygun4Net/Storage/IRaygunOfflineStorage.cs @@ -0,0 +1,32 @@ +using System.Collections.Generic; + +namespace Mindscape.Raygun4Net.Storage +{ + public interface IRaygunOfflineStorage + { + /// <summary> + /// Persist the <paramref name="message"/>> to local storage. + /// Messages must be saved to a location unique per its API key. + /// This is to ensure messages are not sent using the wrong API key. + /// </summary> + /// <param name="message">The serialized error report to store locally.</param> + /// <param name="apiKey">The key for which these file are associated with.</param> + /// <returns></returns> + bool Store(string message, string apiKey); + + /// <summary> + /// Retrieve all files from local storage. + /// </summary> + /// <param name="apiKey">The key for which these file are associated with.</param> + /// <returns>A container of files that are currently stored locally.</returns> + IList<IRaygunFile> FetchAll(string apiKey); + + /// <summary> + /// Delete a file from local storage that has the following <paramref name="name"/>. + /// </summary> + /// <param name="name">The filename of the local file.</param> + /// <param name="apiKey">The key for which these file are associated with.</param> + /// <returns></returns> + bool Remove(string name, string apiKey); + } +} \ No newline at end of file diff --git a/Mindscape.Raygun4Net/Storage/IsolatedRaygunOfflineStorage.cs b/Mindscape.Raygun4Net/Storage/IsolatedRaygunOfflineStorage.cs new file mode 100644 index 00000000..f426e4ef --- /dev/null +++ b/Mindscape.Raygun4Net/Storage/IsolatedRaygunOfflineStorage.cs @@ -0,0 +1,212 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.IO.IsolatedStorage; +using System.Security.Cryptography; +using System.Text; + +namespace Mindscape.Raygun4Net.Storage +{ + public class IsolatedRaygunOfflineStorage : IRaygunOfflineStorage + { + private string _folderNameHash = null; + private const string RaygunBaseDirectory = "Raygun"; + private const string RaygunFileFormat = ".json"; + + private int _currentFileCounter = 0; + + public bool Store(string message, string apiKey) + { + // Do not store invalid messages. + if (string.IsNullOrEmpty(message) || string.IsNullOrEmpty(apiKey)) + { + return false; + } + + using (var storage = GetIsolatedStorageScope()) + { + // Get the directory within isolated storage to hold our data. + var localDirectory = GetLocalDirectory(apiKey); + + if (string.IsNullOrEmpty(localDirectory)) + { + return false; + } + + // Create the destination if it's not there. + if (!EnsureDirectoryExists(storage, localDirectory)) + { + return false; + } + + var searchPattern = Path.Combine(localDirectory, $"*{RaygunFileFormat}"); + var maxReports = Math.Min(RaygunSettings.Settings.MaxCrashReportsStoredOffline, RaygunSettings.MaxCrashReportsStoredOfflineHardLimit); + + // We can only save the report if we have not reached the report count limit. + if (storage.GetFileNames(searchPattern).Length >= maxReports) + { + return false; + } + + // Build up our file information. + var filename = GetUniqueAscendingJsonName(); + var localFilePath = Path.Combine(localDirectory, filename); + + // Write the contents to storage. + using (var stream = new IsolatedStorageFileStream(localFilePath, FileMode.OpenOrCreate, FileAccess.Write, storage)) + { + using (var writer = new StreamWriter(stream, Encoding.Unicode)) + { + writer.Write(message); + writer.Flush(); + writer.Close(); + } + } + } + + return true; + } + + private bool EnsureDirectoryExists(IsolatedStorageFile storage, string localDirectory) + { + bool success = true; + + try + { + storage.CreateDirectory(localDirectory); + } + catch + { + success = false; + } + + return success; + } + + public IList<IRaygunFile> FetchAll(string apiKey) + { + var files = new List<IRaygunFile>(); + + if (string.IsNullOrEmpty(apiKey)) + { + return files; + } + + using (var storage = GetIsolatedStorageScope()) + { + // Get the directory within isolated storage to hold our data. + var localDirectory = GetLocalDirectory(apiKey); + + if (string.IsNullOrEmpty(localDirectory)) + { + return files; + } + + // We must ensure the local directory exists before we look for files.. + if (!EnsureDirectoryExists(storage, localDirectory)) + { + return files; + } + + // Look for all the files within our working local directory. + var fileNames = storage.GetFileNames(Path.Combine(localDirectory, $"*{RaygunFileFormat}")); + + // Take action on each file. + foreach (var name in fileNames) + { + var stream = new IsolatedStorageFileStream(Path.Combine(localDirectory, name), FileMode.Open, storage); + + // Read the contents and put it into our own structure. + using (var reader = new StreamReader(stream)) + { + string contents = reader.ReadToEnd(); + + files.Add(new RaygunFile() + { + Name = name, + Contents = contents + }); + } + } + } + + return files; + } + + public bool Remove(string name, string apiKey) + { + // We cannot remove based on invalid params. + if (string.IsNullOrEmpty(name) || string.IsNullOrEmpty(apiKey)) + { + return false; + } + + using (var storage = GetIsolatedStorageScope()) + { + // Get a list of the current files in storage. + var localDirectory = GetLocalDirectory(apiKey); + + if (string.IsNullOrEmpty(localDirectory)) + { + return false; + } + + var localFilePath = Path.Combine(localDirectory, name); + + // We must ensure the local file exists before delete it. + if (storage.GetFileNames(localFilePath)?.Length == 0) + { + return false; + } + + storage.DeleteFile(localFilePath); + + return true; + } + } + + private IsolatedStorageFile GetIsolatedStorageScope() + { + return AppDomain.CurrentDomain?.ActivationContext != null ? + IsolatedStorageFile.GetUserStoreForApplication() : + IsolatedStorageFile.GetUserStoreForAssembly(); + } + + private string GetLocalDirectory(string apiKey) + { + // Attempt to perform the hash operation once. + if (string.IsNullOrEmpty(_folderNameHash)) + { + _folderNameHash = PerformHash(apiKey); + } + + // If successful return the correct path. + return (_folderNameHash == null) ? null : Path.Combine(RaygunBaseDirectory, _folderNameHash); + } + + private string PerformHash(string input) + { + // Use input string to calculate MD5 hash + using (var hasher = MD5.Create()) + { + var inputBytes = Encoding.ASCII.GetBytes(input); + var hashBytes = hasher.ComputeHash(inputBytes); + + // Convert the byte array to hexadecimal string + var builder = new StringBuilder(); + + foreach (var hashByte in hashBytes) + { + builder.Append(hashByte.ToString("X2")); + } + + return builder.ToString(); + } + } + + private string GetUniqueAscendingJsonName() + { + return $"{DateTime.UtcNow.Ticks}-{_currentFileCounter++}-{Guid.NewGuid().ToString()}{RaygunFileFormat}"; + } + } +} \ No newline at end of file diff --git a/Mindscape.Raygun4Net/Storage/RaygunFile.cs b/Mindscape.Raygun4Net/Storage/RaygunFile.cs new file mode 100644 index 00000000..8adc537d --- /dev/null +++ b/Mindscape.Raygun4Net/Storage/RaygunFile.cs @@ -0,0 +1,8 @@ +namespace Mindscape.Raygun4Net.Storage +{ + public class RaygunFile : IRaygunFile + { + public string Name { get; set; } + public string Contents { get; set; } + } +} \ No newline at end of file diff --git a/Mindscape.Raygun4Net/Utils/Singleton.cs b/Mindscape.Raygun4Net/Utils/Singleton.cs new file mode 100644 index 00000000..1c51a90a --- /dev/null +++ b/Mindscape.Raygun4Net/Utils/Singleton.cs @@ -0,0 +1,28 @@ +using System; + +namespace Mindscape.Raygun4Net.Utils +{ + public abstract class Singleton<T> where T : class + { + private static T _instance; + private static object _lock = new object(); + + protected Singleton() { } + + public static T Instance + { + get + { + lock (_lock) + { + if (_instance == null) + { + _instance = Activator.CreateInstance<T>(); + } + + return _instance; + } + } + } + } +} \ No newline at end of file diff --git a/Mindscape.Raygun4Net2.Tests/RaygunSettingsTests.cs b/Mindscape.Raygun4Net2.Tests/RaygunSettingsTests.cs index ece22783..357c7a00 100644 --- a/Mindscape.Raygun4Net2.Tests/RaygunSettingsTests.cs +++ b/Mindscape.Raygun4Net2.Tests/RaygunSettingsTests.cs @@ -18,7 +18,7 @@ public void Apikey_EmptyByDefault() [Test] public void ApiEndPoint_DefaultValue() { - Assert.AreEqual("https://api.raygun.io/entries", RaygunSettings.Settings.ApiEndpoint.AbsoluteUri); + Assert.AreEqual("https://api.raygun.com/entries", RaygunSettings.Settings.ApiEndpoint.AbsoluteUri); } [Test] diff --git a/Mindscape.Raygun4Net2/Builders/RaygunEnvironmentMessageBuilder.cs b/Mindscape.Raygun4Net2/Builders/RaygunEnvironmentMessageBuilder.cs index a2e1daa7..21341459 100644 --- a/Mindscape.Raygun4Net2/Builders/RaygunEnvironmentMessageBuilder.cs +++ b/Mindscape.Raygun4Net2/Builders/RaygunEnvironmentMessageBuilder.cs @@ -19,7 +19,7 @@ public static RaygunEnvironmentMessage Build() // For now if they fail to load for whatever reason then just // swallow the exception. A good addition would be to handle // these cases and load them correctly depending on where its running. - // see http://raygun.io/forums/thread/3655 + // see http://raygun.com/forums/thread/3655 try { diff --git a/Mindscape.Raygun4Net2/Mindscape.Raygun4Net2.csproj b/Mindscape.Raygun4Net2/Mindscape.Raygun4Net2.csproj index d1f5c5c8..5dabe592 100644 --- a/Mindscape.Raygun4Net2/Mindscape.Raygun4Net2.csproj +++ b/Mindscape.Raygun4Net2/Mindscape.Raygun4Net2.csproj @@ -69,6 +69,15 @@ <Compile Include="..\Mindscape.Raygun4Net\IRaygunMessageBuilder.cs"> <Link>IRaygunMessageBuilder.cs</Link> </Compile> + <Compile Include="..\Mindscape.Raygun4Net\Logging\IRaygunLogger.cs"> + <Link>Logging\IRaygunLogger.cs</Link> + </Compile> + <Compile Include="..\Mindscape.Raygun4Net\Logging\RaygunLogger.cs"> + <Link>Logging\RaygunLogger.cs</Link> + </Compile> + <Compile Include="..\Mindscape.Raygun4Net\Logging\RaygunLogLevel.cs"> + <Link>Logging\RaygunLogLevel.cs</Link> + </Compile> <Compile Include="..\Mindscape.Raygun4Net\Messages\RaygunClientMessage.cs"> <Link>Messages\RaygunClientMessage.cs</Link> </Compile> @@ -156,6 +165,21 @@ <Compile Include="..\Mindscape.Raygun4Net\SimpleJson.cs"> <Link>SimpleJson.cs</Link> </Compile> + <Compile Include="..\Mindscape.Raygun4Net\Storage\IRaygunFile.cs"> + <Link>Storage\IRaygunFile.cs</Link> + </Compile> + <Compile Include="..\Mindscape.Raygun4Net\Storage\IRaygunOfflineStorage.cs"> + <Link>Storage\IRaygunOfflineStorage.cs</Link> + </Compile> + <Compile Include="..\Mindscape.Raygun4Net\Storage\RaygunFile.cs"> + <Link>Storage\RaygunFile.cs</Link> + </Compile> + <Compile Include="..\Mindscape.Raygun4Net\Storage\IsolatedRaygunOfflineStorage.cs"> + <Link>Storage\IsolatedRaygunOfflineStorage.cs</Link> + </Compile> + <Compile Include="..\Mindscape.Raygun4Net\Utils\Singleton.cs"> + <Link>Utils\Singleton.cs</Link> + </Compile> <Compile Include="Builders\RaygunEnvironmentMessageBuilder.cs" /> <Compile Include="Builders\RaygunErrorMessageBuilder.cs" /> <Compile Include="Builders\RaygunRequestMessageBuilder.cs" /> diff --git a/Mindscape.Raygun4Net2/Properties/AssemblyInfo.cs b/Mindscape.Raygun4Net2/Properties/AssemblyInfo.cs index 1501c9dd..a4d5d7d4 100644 --- a/Mindscape.Raygun4Net2/Properties/AssemblyInfo.cs +++ b/Mindscape.Raygun4Net2/Properties/AssemblyInfo.cs @@ -1,8 +1,7 @@ using System.Reflection; -using System.Runtime.CompilerServices; using System.Runtime.InteropServices; -// General Information about an assembly is controlled through the following +// General Information about an assembly is controlled through the following // set of attributes. Change these attribute values to modify the information // associated with an assembly. [assembly: AssemblyTitle("Raygun4Net2.0")] @@ -10,12 +9,12 @@ [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("Raygun")] [assembly: AssemblyProduct("Raygun4Net")] -[assembly: AssemblyCopyright("Copyright © Raygun 2014-2019")] +[assembly: AssemblyCopyright("Copyright © Raygun 2014-2020")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from // COM, set the ComVisible attribute to true on that type. [assembly: ComVisible(false)] diff --git a/Mindscape.Raygun4Net2/RaygunClient.cs b/Mindscape.Raygun4Net2/RaygunClient.cs index 2f7bed78..c65a3e1a 100644 --- a/Mindscape.Raygun4Net2/RaygunClient.cs +++ b/Mindscape.Raygun4Net2/RaygunClient.cs @@ -3,22 +3,35 @@ using System.Collections.Generic; using System.Net; using System.Reflection; -using System.Text; using System.Threading; using System.Web; using Mindscape.Raygun4Net.Builders; +using Mindscape.Raygun4Net.Logging; using Mindscape.Raygun4Net.Messages; +using Mindscape.Raygun4Net.Storage; namespace Mindscape.Raygun4Net { public class RaygunClient : RaygunClientBase { + [ThreadStatic] + private static RaygunRequestMessage _currentRequestMessage; + private static object _sendLock = new object(); + private readonly string _apiKey; private readonly RaygunRequestMessageOptions _requestMessageOptions = new RaygunRequestMessageOptions(); private readonly List<Type> _wrapperExceptions = new List<Type>(); - [ThreadStatic] - private static RaygunRequestMessage _currentRequestMessage; + private IRaygunOfflineStorage _offlineStorage = new IsolatedRaygunOfflineStorage(); + + /// <summary> + /// Initializes a new instance of the <see cref="RaygunClient" /> class. + /// Uses the ApiKey specified in the config file. + /// </summary> + public RaygunClient() + : this(RaygunSettings.Settings.ApiKey) + { + } /// <summary> /// Initializes a new instance of the <see cref="RaygunClient" /> class. @@ -36,41 +49,28 @@ public RaygunClient(string apiKey) var ignoredNames = RaygunSettings.Settings.IgnoreFormFieldNames.Split(','); IgnoreFormFieldNames(ignoredNames); } + if (!string.IsNullOrEmpty(RaygunSettings.Settings.IgnoreHeaderNames)) { var ignoredNames = RaygunSettings.Settings.IgnoreHeaderNames.Split(','); IgnoreHeaderNames(ignoredNames); } + if (!string.IsNullOrEmpty(RaygunSettings.Settings.IgnoreCookieNames)) { var ignoredNames = RaygunSettings.Settings.IgnoreCookieNames.Split(','); IgnoreCookieNames(ignoredNames); } + if (!string.IsNullOrEmpty(RaygunSettings.Settings.IgnoreServerVariableNames)) { var ignoredNames = RaygunSettings.Settings.IgnoreServerVariableNames.Split(','); IgnoreServerVariableNames(ignoredNames); } - IsRawDataIgnored = RaygunSettings.Settings.IsRawDataIgnored; - } - /// <summary> - /// Initializes a new instance of the <see cref="RaygunClient" /> class. - /// Uses the ApiKey specified in the config file. - /// </summary> - public RaygunClient() - : this(RaygunSettings.Settings.ApiKey) - { - } + IsRawDataIgnored = RaygunSettings.Settings.IsRawDataIgnored; - protected bool ValidateApiKey() - { - if (string.IsNullOrEmpty(_apiKey)) - { - System.Diagnostics.Debug.WriteLine("ApiKey has not been provided, exception will not be logged"); - return false; - } - return true; + ThreadPool.QueueUserWorkItem(state => { SendStoredMessages(); }); } /// <summary> @@ -104,6 +104,8 @@ public void RemoveWrapperExceptions(params Type[] wrapperExceptions) } } + #region Message Scrubbing Properties + /// <summary> /// Adds a list of keys to ignore when attaching the Form data of an HTTP POST request. This allows /// you to remove sensitive data from the transmitted copy of the Form on the HttpRequest by specifying the keys you want removed. @@ -149,8 +151,8 @@ public void IgnoreServerVariableNames(params string[] names) } /// <summary> - /// Specifies whether or not RawData from web requests is ignored when sending reports to Raygun.io. - /// The default is false which means RawData will be sent to Raygun.io. + /// Specifies whether or not RawData from web requests is ignored when sending reports to Raygun. + /// The default is false which means RawData will be sent to Raygun. /// </summary> public bool IsRawDataIgnored { @@ -161,17 +163,12 @@ public bool IsRawDataIgnored } } - protected override bool CanSend(Exception exception) - { - if (RaygunSettings.Settings.ExcludeErrorsFromLocal && HttpContext.Current != null && HttpContext.Current.Request.IsLocal) - { - return false; - } - return base.CanSend(exception); - } + #endregion // Message Scrubbing Properties + + #region Message Send Methods /// <summary> - /// Transmits an exception to Raygun.io synchronously. + /// Transmits an exception to Raygun synchronously. /// </summary> /// <param name="exception">The exception to deliver.</param> public override void Send(Exception exception) @@ -180,7 +177,7 @@ public override void Send(Exception exception) } /// <summary> - /// Transmits an exception to Raygun.io synchronously specifying a list of string tags associated + /// Transmits an exception to Raygun synchronously specifying a list of string tags associated /// with the message for identification. /// </summary> /// <param name="exception">The exception to deliver.</param> @@ -191,7 +188,7 @@ public void Send(Exception exception, IList<string> tags) } /// <summary> - /// Transmits an exception to Raygun.io synchronously specifying a list of string tags associated + /// Transmits an exception to Raygun synchronously specifying a list of string tags associated /// with the message for identification, as well as sending a key-value collection of custom data. /// </summary> /// <param name="exception">The exception to deliver.</param> @@ -205,7 +202,7 @@ public void Send(Exception exception, IList<string> tags, IDictionary userCustom } /// <summary> - /// Asynchronously transmits a message to Raygun.io. + /// Asynchronously transmits a message to Raygun. /// </summary> /// <param name="exception">The exception to deliver.</param> public void SendInBackground(Exception exception) @@ -214,7 +211,7 @@ public void SendInBackground(Exception exception) } /// <summary> - /// Asynchronously transmits an exception to Raygun.io. + /// Asynchronously transmits an exception to Raygun. /// </summary> /// <param name="exception">The exception to deliver.</param> /// <param name="tags">A list of strings associated with the message.</param> @@ -224,7 +221,7 @@ public void SendInBackground(Exception exception, IList<string> tags) } /// <summary> - /// Asynchronously transmits an exception to Raygun.io. + /// Asynchronously transmits an exception to Raygun. /// </summary> /// <param name="exception">The exception to deliver.</param> /// <param name="tags">A list of strings associated with the message.</param> @@ -243,7 +240,7 @@ public void SendInBackground(Exception exception, IList<string> tags, IDictionar } /// <summary> - /// Asynchronously transmits a message to Raygun.io. + /// Asynchronously transmits a message to Raygun. /// </summary> /// <param name="raygunMessage">The RaygunMessage to send. This needs its OccurredOn property /// set to a valid DateTime and as much of the Details property as is available.</param> @@ -252,6 +249,93 @@ public void SendInBackground(RaygunMessage raygunMessage) ThreadPool.QueueUserWorkItem(c => Send(raygunMessage)); } + /// <summary> + /// Posts a RaygunMessage to the Raygun API endpoint. + /// </summary> + /// <param name="raygunMessage">The RaygunMessage to send. This needs its OccurredOn property + /// set to a valid DateTime and as much of the Details property as is available.</param> + public override void Send(RaygunMessage raygunMessage) + { + if (!ValidateApiKey()) + { + RaygunLogger.Instance.Warning("Failed to send error report due to invalid API key"); + return; + } + + bool canSend = OnSendingMessage(raygunMessage); + + if (!canSend) + { + return; + } + + string message = null; + + try + { + message = SimpleJson.SerializeObject(raygunMessage); + } + catch (Exception ex) + { + RaygunLogger.Instance.Error($"Failed to serialize report due to: {ex.Message}"); + + if (RaygunSettings.Settings.ThrowOnError) + { + throw; + } + } + + if (string.IsNullOrEmpty(message)) + { + return; + } + + bool successfullySentReport = true; + + try + { + Send(message); + } + catch (Exception ex) + { + successfullySentReport = false; + + RaygunLogger.Instance.Error($"Failed to send report to Raygun due to: {ex.Message}"); + + SaveMessage(message); + + if (RaygunSettings.Settings.ThrowOnError) + { + throw; + } + } + + if (successfullySentReport) + { + SendStoredMessages(); + } + } + + private void Send(string message) + { + RaygunLogger.Instance.Verbose("Sending Payload --------------"); + RaygunLogger.Instance.Verbose(message); + RaygunLogger.Instance.Verbose("------------------------------"); + + using (var client = new WebClient()) + { + client.Headers.Add("X-ApiKey", _apiKey); + client.Headers.Add("content-type", "application/json; charset=utf-8"); + client.Encoding = System.Text.Encoding.UTF8; + + client.UploadString(RaygunSettings.Settings.ApiEndpoint, message); + } + } + + #endregion // Message Send Methods + + #region Message Building Methods + private RaygunRequestMessage BuildRequestMessage() { RaygunRequestMessage requestMessage = null; @@ -315,56 +399,122 @@ private Exception StripWrapperExceptions(Exception exception) return exception; } - /// <summary> - /// Posts a RaygunMessage to the Raygun.io api endpoint. - /// </summary> - /// <param name="raygunMessage">The RaygunMessage to send. This needs its OccurredOn property - /// set to a valid DateTime and as much of the Details property as is available.</param> - public override void Send(RaygunMessage raygunMessage) + #endregion // Message Building Methods + + #region Message Offline Storage + + private void SaveMessage(string message) { - if (ValidateApiKey()) + if (!RaygunSettings.Settings.CrashReportingOfflineStorageEnabled) { - bool canSend = OnSendingMessage(raygunMessage); - if (canSend) - { - string message = null; + RaygunLogger.Instance.Warning("Offline storage is disabled, skipping saving report."); + return; + } - try - { - message = SimpleJson.SerializeObject(raygunMessage); - } - catch (Exception ex) - { - System.Diagnostics.Debug.WriteLine(string.Format("Error serializing raygun message: {0}", ex.Message)); - } + if (!ValidateApiKey()) + { + RaygunLogger.Instance.Warning("Failed to save report due to invalid API key."); + return; + } - if (message != null) + // Avoid writing and reading from disk at the same time with `SendStoredMessages`. + lock (_sendLock) + { + try + { + if (!_offlineStorage.Store(message, _apiKey)) { - SendMessage(message); + RaygunLogger.Instance.Warning("Failed to save report to offline storage"); } } + catch (Exception ex) + { + RaygunLogger.Instance.Error($"Failed to save report to offline storage due to: {ex.Message}"); + } } } - private bool SendMessage(string message) + private void SendStoredMessages() { - using (var client = new WebClient()) + if (!RaygunSettings.Settings.CrashReportingOfflineStorageEnabled) { - client.Headers.Add("X-ApiKey", _apiKey); - client.Headers.Add("content-type", "application/json; charset=utf-8"); - client.Encoding = System.Text.Encoding.UTF8; + RaygunLogger.Instance.Warning("Offline storage is disabled, skipping sending stored reports."); + return; + } + + if (!ValidateApiKey()) + { + RaygunLogger.Instance.Warning("Failed to send offline reports due to invalid API key."); + return; + } + lock (_sendLock) + { try { - client.UploadString(RaygunSettings.Settings.ApiEndpoint, message); + var files = _offlineStorage.FetchAll(_apiKey); + + foreach (var file in files) + { + try + { + // Send the stored report. + Send(file.Contents); + + // Remove the stored report from local storage. + if (_offlineStorage.Remove(file.Name, _apiKey)) + { + RaygunLogger.Instance.Info("Successfully removed report from offline storage."); + } + else + { + RaygunLogger.Instance.Warning("Failed to remove report from offline storage."); + } + } + catch (Exception ex) + { + RaygunLogger.Instance.Error($"Failed to send stored report to Raygun due to: {ex.Message}"); + + // If just one message fails to send, then don't delete the message, + // and don't attempt sending anymore until later. + return; + } + } } catch (Exception ex) { - System.Diagnostics.Debug.WriteLine(string.Format("Error Logging Exception to Raygun.io {0}", ex.Message)); - return false; + RaygunLogger.Instance.Error($"Failed to send stored report to Raygun due to: {ex.Message}"); + } + } + } + + #endregion // Message Offline Storage + + protected override bool CanSend(Exception exception) + { + if (RaygunSettings.Settings.ExcludeErrorsFromLocal && HttpContext.Current != null) + { + try + { + if (HttpContext.Current.Request.IsLocal) + { + return false; + } + } + catch + { + if (RaygunSettings.Settings.ThrowOnError) + { + throw; + } } } - return true; + return base.CanSend(exception); + } + + protected bool ValidateApiKey() + { + return !string.IsNullOrEmpty(_apiKey); } } } diff --git a/Mindscape.Raygun4Net2/RaygunSettings.cs b/Mindscape.Raygun4Net2/RaygunSettings.cs index f83622d5..61040825 100644 --- a/Mindscape.Raygun4Net2/RaygunSettings.cs +++ b/Mindscape.Raygun4Net2/RaygunSettings.cs @@ -1,5 +1,6 @@ using System; using System.Configuration; +using Mindscape.Raygun4Net.Logging; namespace Mindscape.Raygun4Net { @@ -7,7 +8,9 @@ public class RaygunSettings : ConfigurationSection { private static readonly RaygunSettings settings = ConfigurationManager.GetSection("RaygunSettings") as RaygunSettings ?? new RaygunSettings(); - private const string DefaultApiEndPoint = "https://api.raygun.io/entries"; + private const string DefaultApiEndPoint = "https://api.raygun.com/entries"; + + public const int MaxCrashReportsStoredOfflineHardLimit = 64; public static RaygunSettings Settings { @@ -99,5 +102,41 @@ public string ApplicationIdentifier get { return (string)this["applicationIdentifier"]; } set { this["applicationIdentifier"] = value; } } + + /// <summary> + /// Gets or sets the max crash reports stored on the device. + /// There is a hard upper limit of 64 reports. + /// </summary> + /// <value>The max crash reports stored on device.</value> + [ConfigurationProperty("maxCrashReportsStoredOffline", IsRequired = false, DefaultValue = MaxCrashReportsStoredOfflineHardLimit)] + public int MaxCrashReportsStoredOffline + { + get { return (int)this["maxCrashReportsStoredOffline"]; } + set { this["maxCrashReportsStoredOffline"] = value; } + } + + /// <summary> + /// Allows for crash reports to be stored to local storage when there is no available network connection. + /// </summary> + /// <value><c>true</c> if allowing crash reports to be stored offline; otherwise, <c>false</c>.</value> + [ConfigurationProperty("crashReportingOfflineStorageEnabled", IsRequired = false, DefaultValue = true)] + public bool CrashReportingOfflineStorageEnabled + { + get { return (bool)this["crashReportingOfflineStorageEnabled"]; } + set { this["crashReportingOfflineStorageEnabled"] = value; } + } + + /// <summary> + /// Gets or sets the log level controlling the amount of information printed to system consoles. + /// Setting the level to <see cref="RaygunLogLevel.Verbose"/> will print the raw Crash Reporting being + /// posted to the API endpoints. + /// </summary> + /// <value>The log level.</value> + [ConfigurationProperty("logLevel", IsRequired = false, DefaultValue = RaygunLogLevel.Warning)] + public RaygunLogLevel LogLevel + { + get { return (RaygunLogLevel)this["logLevel"]; } + set { this["logLevel"] = value; } + } } } diff --git a/Mindscape.Raygun4Net4.ClientProfile/Mindscape.Raygun4Net4.ClientProfile.csproj b/Mindscape.Raygun4Net4.ClientProfile/Mindscape.Raygun4Net4.ClientProfile.csproj index cb6eb7a2..342f95cb 100644 --- a/Mindscape.Raygun4Net4.ClientProfile/Mindscape.Raygun4Net4.ClientProfile.csproj +++ b/Mindscape.Raygun4Net4.ClientProfile/Mindscape.Raygun4Net4.ClientProfile.csproj @@ -91,6 +91,15 @@ <Compile Include="..\Mindscape.Raygun4Net\IRaygunMessageBuilder.cs"> <Link>IRaygunMessageBuilder.cs</Link> </Compile> + <Compile Include="..\Mindscape.Raygun4Net\Logging\IRaygunLogger.cs"> + <Link>Logging\IRaygunLogger.cs</Link> + </Compile> + <Compile Include="..\Mindscape.Raygun4Net\Logging\RaygunLogger.cs"> + <Link>Logging\RaygunLogger.cs</Link> + </Compile> + <Compile Include="..\Mindscape.Raygun4Net\Logging\RaygunLogLevel.cs"> + <Link>Logging\RaygunLogLevel.cs</Link> + </Compile> <Compile Include="..\Mindscape.Raygun4Net\Messages\RaygunClientMessage.cs"> <Link>Messages\RaygunClientMessage.cs</Link> </Compile> @@ -118,6 +127,21 @@ <Compile Include="..\Mindscape.Raygun4Net\SimpleJson.cs"> <Link>SimpleJson.cs</Link> </Compile> + <Compile Include="..\Mindscape.Raygun4Net\Storage\IRaygunFile.cs"> + <Link>Storage\IRaygunFile.cs</Link> + </Compile> + <Compile Include="..\Mindscape.Raygun4Net\Storage\IRaygunOfflineStorage.cs"> + <Link>Storage\IRaygunOfflineStorage.cs</Link> + </Compile> + <Compile Include="..\Mindscape.Raygun4Net\Storage\RaygunFile.cs"> + <Link>Storage\RaygunFile.cs</Link> + </Compile> + <Compile Include="..\Mindscape.Raygun4Net\Storage\IsolatedRaygunOfflineStorage.cs"> + <Link>Storage\IsolatedRaygunOfflineStorage.cs</Link> + </Compile> + <Compile Include="..\Mindscape.Raygun4Net\Utils\Singleton.cs"> + <Link>Utils\Singleton.cs</Link> + </Compile> <Compile Include="Properties\AssemblyInfo.cs" /> <Compile Include="RaygunClient.cs" /> </ItemGroup> diff --git a/Mindscape.Raygun4Net4.ClientProfile/Properties/AssemblyInfo.cs b/Mindscape.Raygun4Net4.ClientProfile/Properties/AssemblyInfo.cs index 7a6f228d..133f707d 100644 --- a/Mindscape.Raygun4Net4.ClientProfile/Properties/AssemblyInfo.cs +++ b/Mindscape.Raygun4Net4.ClientProfile/Properties/AssemblyInfo.cs @@ -1,8 +1,7 @@ using System.Reflection; -using System.Runtime.CompilerServices; using System.Runtime.InteropServices; -// General Information about an assembly is controlled through the following +// General Information about an assembly is controlled through the following // set of attributes. Change these attribute values to modify the information // associated with an assembly. [assembly: AssemblyTitle("Raygun4Net4.ClientProfile")] @@ -10,12 +9,12 @@ [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("Raygun")] [assembly: AssemblyProduct("Raygun4Net")] -[assembly: AssemblyCopyright("Copyright © Raygun 2015-2019")] +[assembly: AssemblyCopyright("Copyright © Raygun 2015-2020")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from // COM, set the ComVisible attribute to true on that type. [assembly: ComVisible(false)] diff --git a/Mindscape.Raygun4Net4.ClientProfile/RaygunClient.cs b/Mindscape.Raygun4Net4.ClientProfile/RaygunClient.cs index b708d266..68b1013b 100644 --- a/Mindscape.Raygun4Net4.ClientProfile/RaygunClient.cs +++ b/Mindscape.Raygun4Net4.ClientProfile/RaygunClient.cs @@ -3,34 +3,33 @@ using System.Collections.Generic; using System.Linq; using System.Net; -using Mindscape.Raygun4Net.Messages; - using System.Threading; using System.Reflection; -using Mindscape.Raygun4Net.Builders; -using System.IO; -using System.IO.IsolatedStorage; -using System.Text; +using Mindscape.Raygun4Net.Messages; +using Mindscape.Raygun4Net.Logging; +using Mindscape.Raygun4Net.Storage; namespace Mindscape.Raygun4Net { public class RaygunClient : RaygunClientBase { + private static object _sendLock = new object(); + private readonly string _apiKey; private readonly List<Type> _wrapperExceptions = new List<Type>(); + private IRaygunOfflineStorage _offlineStorage = new IsolatedRaygunOfflineStorage(); + /// <summary> - /// Initializes a new instance of the <see cref="RaygunClient" /> class. + /// Gets or sets the username/password credentials which are used to authenticate with the system default Proxy server, if one is set + /// and requires credentials. /// </summary> - /// <param name="apiKey">The API key.</param> - public RaygunClient(string apiKey) - { - _apiKey = apiKey; - - _wrapperExceptions.Add(typeof(TargetInvocationException)); + public ICredentials ProxyCredentials { get; set; } - ThreadPool.QueueUserWorkItem(state => { SendStoredMessages(); }); - } + /// <summary> + /// Gets or sets an IWebProxy instance which can be used to override the default system proxy server settings + /// </summary> + public IWebProxy WebProxy { get; set; } /// <summary> /// Initializes a new instance of the <see cref="RaygunClient" /> class. @@ -41,26 +40,18 @@ public RaygunClient() { } - protected bool ValidateApiKey() - { - if (string.IsNullOrEmpty(_apiKey)) - { - System.Diagnostics.Debug.WriteLine("ApiKey has not been provided, exception will not be logged"); - return false; - } - return true; - } - /// <summary> - /// Gets or sets the username/password credentials which are used to authenticate with the system default Proxy server, if one is set - /// and requires credentials. + /// Initializes a new instance of the <see cref="RaygunClient" /> class. /// </summary> - public ICredentials ProxyCredentials { get; set; } + /// <param name="apiKey">The API key.</param> + public RaygunClient(string apiKey) + { + _apiKey = apiKey; - /// <summary> - /// Gets or sets an IWebProxy instance which can be used to override the default system proxy server settings - /// </summary> - public IWebProxy WebProxy { get; set; } + _wrapperExceptions.Add(typeof(TargetInvocationException)); + + ThreadPool.QueueUserWorkItem(state => { SendStoredMessages(); }); + } /// <summary> /// Adds a list of outer exceptions that will be stripped, leaving only the valuable inner exception. @@ -94,8 +85,10 @@ public void RemoveWrapperExceptions(params Type[] wrapperExceptions) } } + #region Message Send Methods + /// <summary> - /// Transmits an exception to Raygun.io synchronously, using the version number of the originating assembly. + /// Transmits an exception to Raygun synchronously, using the version number of the originating assembly. /// </summary> /// <param name="exception">The exception to deliver.</param> public override void Send(Exception exception) @@ -104,7 +97,7 @@ public override void Send(Exception exception) } /// <summary> - /// Transmits an exception to Raygun.io synchronously specifying a list of string tags associated + /// Transmits an exception to Raygun synchronously specifying a list of string tags associated /// with the message for identification. This uses the version number of the originating assembly. /// </summary> /// <param name="exception">The exception to deliver.</param> @@ -115,7 +108,7 @@ public void Send(Exception exception, IList<string> tags) } /// <summary> - /// Transmits an exception to Raygun.io synchronously specifying a list of string tags associated + /// Transmits an exception to Raygun synchronously specifying a list of string tags associated /// with the message for identification, as well as sending a key-value collection of custom data. /// This uses the version number of the originating assembly. /// </summary> @@ -128,7 +121,7 @@ public void Send(Exception exception, IList<string> tags, IDictionary userCustom } /// <summary> - /// Transmits an exception to Raygun.io synchronously specifying a list of string tags associated + /// Transmits an exception to Raygun synchronously specifying a list of string tags associated /// with the message for identification, as well as sending a key-value collection of custom data. /// This uses the version number of the originating assembly. /// </summary> @@ -146,7 +139,7 @@ public void Send(Exception exception, IList<string> tags, IDictionary userCustom } /// <summary> - /// Asynchronously transmits a message to Raygun.io. + /// Asynchronously transmits a message to Raygun. /// </summary> /// <param name="exception">The exception to deliver.</param> public void SendInBackground(Exception exception) @@ -155,7 +148,7 @@ public void SendInBackground(Exception exception) } /// <summary> - /// Asynchronously transmits an exception to Raygun.io. + /// Asynchronously transmits an exception to Raygun. /// </summary> /// <param name="exception">The exception to deliver.</param> /// <param name="tags">A list of strings associated with the message.</param> @@ -165,7 +158,7 @@ public void SendInBackground(Exception exception, IList<string> tags) } /// <summary> - /// Asynchronously transmits an exception to Raygun.io. + /// Asynchronously transmits an exception to Raygun. /// </summary> /// <param name="exception">The exception to deliver.</param> /// <param name="tags">A list of strings associated with the message.</param> @@ -176,7 +169,7 @@ public void SendInBackground(Exception exception, IList<string> tags, IDictionar } /// <summary> - /// Asynchronously transmits an exception to Raygun.io. + /// Asynchronously transmits an exception to Raygun. /// </summary> /// <param name="exception">The exception to deliver.</param> /// <param name="tags">A list of strings associated with the message.</param> @@ -208,7 +201,7 @@ public void SendInBackground(Exception exception, IList<string> tags, IDictionar } /// <summary> - /// Asynchronously transmits a message to Raygun.io. + /// Asynchronously transmits a message to Raygun. /// </summary> /// <param name="raygunMessage">The RaygunMessage to send. This needs its OccurredOn property /// set to a valid DateTime and as much of the Details property as is available.</param> @@ -217,6 +210,97 @@ public void SendInBackground(RaygunMessage raygunMessage) ThreadPool.QueueUserWorkItem(c => Send(raygunMessage)); } + private void StripAndSend(Exception exception, IList<string> tags, IDictionary userCustomData, RaygunIdentifierMessage userInfo, DateTime? currentTime) + { + foreach (Exception e in StripWrapperExceptions(exception)) + { + Send(BuildMessage(e, tags, userCustomData, userInfo, currentTime)); + } + } + + /// <summary> + /// Posts a RaygunMessage to the Raygun API endpoint. + /// </summary> + /// <param name="raygunMessage">The RaygunMessage to send. This needs its OccurredOn property + /// set to a valid DateTime and as much of the Details property as is available.</param> + public override void Send(RaygunMessage raygunMessage) + { + if (!ValidateApiKey()) + { + RaygunLogger.Instance.Warning("Failed to send error report due to invalid API key."); + return; + } + + bool canSend = OnSendingMessage(raygunMessage); + + if (!canSend) + { + return; + } + + string message = null; + + try + { + message = SimpleJson.SerializeObject(raygunMessage); + } + catch (Exception ex) + { + RaygunLogger.Instance.Error($"Failed to serialize report due to: {ex.Message}"); + + if (RaygunSettings.Settings.ThrowOnError) + { + throw; + } + } + + if (string.IsNullOrEmpty(message)) + { + return; + } + + bool successfullySentReport = true; + + try + { + Send(message); + } + catch (Exception ex) + { + successfullySentReport = false; + + RaygunLogger.Instance.Error($"Failed to send report to Raygun due to: {ex.Message}"); + + SaveMessage(message); + + if (RaygunSettings.Settings.ThrowOnError) + { + throw; + } + } + + if (successfullySentReport) + { + SendStoredMessages(); + } + } + + private void Send(string message) + { + RaygunLogger.Instance.Verbose("Sending Payload --------------"); + RaygunLogger.Instance.Verbose(message); + RaygunLogger.Instance.Verbose("------------------------------"); + + using (var client = CreateWebClient()) + { + client.UploadString(RaygunSettings.Settings.ApiEndpoint, message); + } + } + + #endregion // Message Send Methods + + #region Message Building Methods + protected RaygunMessage BuildMessage(Exception exception, IList<string> tags, IDictionary userCustomData) { return BuildMessage(exception, tags, userCustomData, null, null); @@ -243,14 +327,6 @@ protected RaygunMessage BuildMessage(Exception exception, IList<string> tags, ID return message; } - private void StripAndSend(Exception exception, IList<string> tags, IDictionary userCustomData, RaygunIdentifierMessage userInfo, DateTime? currentTime) - { - foreach (Exception e in StripWrapperExceptions(exception)) - { - Send(BuildMessage(e, tags, userCustomData, userInfo, currentTime)); - } - } - protected IEnumerable<Exception> StripWrapperExceptions(Exception exception) { if (exception != null && _wrapperExceptions.Any(wrapperException => exception.GetType() == wrapperException && exception.InnerException != null)) @@ -280,62 +356,100 @@ protected IEnumerable<Exception> StripWrapperExceptions(Exception exception) } } - /// <summary> - /// Posts a RaygunMessage to the Raygun.io api endpoint. - /// </summary> - /// <param name="raygunMessage">The RaygunMessage to send. This needs its OccurredOn property - /// set to a valid DateTime and as much of the Details property as is available.</param> - public override void Send(RaygunMessage raygunMessage) + #endregion // Message Building Methods + + #region Message Offline Storage + + private void SaveMessage(string message) { - bool canSend = OnSendingMessage(raygunMessage); - if (canSend) + if (!RaygunSettings.Settings.CrashReportingOfflineStorageEnabled) + { + RaygunLogger.Instance.Warning("Offline storage is disabled, skipping saving report."); + return; + } + + if (!ValidateApiKey()) + { + RaygunLogger.Instance.Warning("Failed to save report due to invalid API key."); + return; + } + + // Avoid writing and reading from disk at the same time with `SendStoredMessages`. + lock (_sendLock) { - string message = null; try { - message = SimpleJson.SerializeObject(raygunMessage); + if (!_offlineStorage.Store(message, _apiKey)) + { + RaygunLogger.Instance.Warning("Failed to save report to offline storage."); + } } catch (Exception ex) { - System.Diagnostics.Debug.WriteLine(string.Format("Error serializing exception {0}", ex.Message)); - - if (RaygunSettings.Settings.ThrowOnError) - { - throw; - } + RaygunLogger.Instance.Error($"Failed to save report to offline storage due to: {ex.Message}"); } + } + } - if (message != null) + private void SendStoredMessages() + { + if (!RaygunSettings.Settings.CrashReportingOfflineStorageEnabled) + { + RaygunLogger.Instance.Warning("Offline storage is disabled, skipping sending stored reports."); + return; + } + + if (!ValidateApiKey()) + { + RaygunLogger.Instance.Warning("Failed to send offline reports due to invalid API key."); + return; + } + + lock (_sendLock) + { + try { - try - { - Send(message); - } - catch (Exception ex) + var files = _offlineStorage.FetchAll(_apiKey); + + foreach (var file in files) { - SaveMessage(message); - System.Diagnostics.Debug.WriteLine(string.Format("Error Logging Exception to Raygun.io {0}", ex.Message)); + try + { + // Send the stored report. + Send(file.Contents); - if (RaygunSettings.Settings.ThrowOnError) + // Remove the stored report from local storage. + if (_offlineStorage.Remove(file.Name, _apiKey)) + { + RaygunLogger.Instance.Info("Successfully removed report from offline storage."); + } + else + { + RaygunLogger.Instance.Warning("Failed to remove report from offline storage."); + } + } + catch (Exception ex) { - throw; + RaygunLogger.Instance.Error($"Failed to send stored report to Raygun due to: {ex.Message}"); + + // If just one message fails to send, then don't delete the message, + // and don't attempt sending anymore until later. + return; } } - - SendStoredMessages(); + } + catch (Exception ex) + { + RaygunLogger.Instance.Error($"Failed to send stored report to Raygun due to: {ex.Message}"); } } } - private void Send(string message) + #endregion // Message Offline Storage + + protected bool ValidateApiKey() { - if (ValidateApiKey()) - { - using (var client = CreateWebClient()) - { - client.UploadString(RaygunSettings.Settings.ApiEndpoint, message); - } - } + return !string.IsNullOrEmpty(_apiKey); } protected WebClient CreateWebClient() @@ -371,120 +485,5 @@ protected WebClient CreateWebClient() } return client; } - - private void SaveMessage(string message) - { - try - { - using (IsolatedStorageFile isolatedStorage = GetIsolatedStorageScope()) - { - string directoryName = "RaygunOfflineStorage"; - if (!isolatedStorage.DirectoryExists(directoryName)) - { - isolatedStorage.CreateDirectory(directoryName); - } - - int number = 1; - while (true) - { - bool exists = isolatedStorage.FileExists(directoryName + "\\RaygunErrorMessage" + number + ".txt"); - if (!exists) - { - string nextFileName = directoryName + "\\RaygunErrorMessage" + (number + 1) + ".txt"; - exists = isolatedStorage.FileExists(nextFileName); - if (exists) - { - isolatedStorage.DeleteFile(nextFileName); - } - break; - } - number++; - } - - if (number == 11) - { - string firstFileName = directoryName + "\\RaygunErrorMessage1.txt"; - if (isolatedStorage.FileExists(firstFileName)) - { - isolatedStorage.DeleteFile(firstFileName); - } - } - using (IsolatedStorageFileStream isoStream = new IsolatedStorageFileStream(directoryName + "\\RaygunErrorMessage" + number + ".txt", FileMode.OpenOrCreate, FileAccess.Write, isolatedStorage)) - { - using (StreamWriter writer = new StreamWriter(isoStream, Encoding.Unicode)) - { - writer.Write(message); - writer.Flush(); - writer.Close(); - } - } - System.Diagnostics.Trace.WriteLine("Saved message: " + "RaygunErrorMessage" + number + ".txt"); - } - } - catch (Exception ex) - { - System.Diagnostics.Trace.WriteLine(string.Format("Error saving message to isolated storage {0}", ex.Message)); - } - } - - private static object _sendLock = new object(); - - private void SendStoredMessages() - { - lock (_sendLock) - { - try - { - using (IsolatedStorageFile isolatedStorage = GetIsolatedStorageScope()) - { - string directoryName = "RaygunOfflineStorage"; - if (isolatedStorage.DirectoryExists(directoryName)) - { - string[] fileNames = isolatedStorage.GetFileNames(directoryName + "\\*.txt"); - foreach (string name in fileNames) - { - IsolatedStorageFileStream isoFileStream = isolatedStorage.OpenFile(directoryName + "\\" + name, FileMode.Open); - using (StreamReader reader = new StreamReader(isoFileStream)) - { - string text = reader.ReadToEnd(); - try - { - Send(text); - } - catch - { - // If just one message fails to send, then don't delete the message, and don't attempt sending anymore until later. - return; - } - System.Diagnostics.Debug.WriteLine("Sent " + name); - } - isolatedStorage.DeleteFile(directoryName + "\\" + name); - } - if (isolatedStorage.GetFileNames(directoryName + "\\*.txt").Length == 0) - { - System.Diagnostics.Debug.WriteLine("Successfully sent all pending messages"); - isolatedStorage.DeleteDirectory(directoryName); - } - } - } - } - catch (Exception ex) - { - System.Diagnostics.Debug.WriteLine(string.Format("Error sending stored messages to Raygun.io {0}", ex.Message)); - } - } - } - - private IsolatedStorageFile GetIsolatedStorageScope() - { - if (AppDomain.CurrentDomain != null && AppDomain.CurrentDomain.ActivationContext != null) - { - return IsolatedStorageFile.GetUserStoreForApplication(); - } - else - { - return IsolatedStorageFile.GetUserStoreForAssembly(); - } - } } } diff --git a/Mindscape.Raygun4Net4/Properties/AssemblyInfo.cs b/Mindscape.Raygun4Net4/Properties/AssemblyInfo.cs index 3f7ca51b..98c2acae 100644 --- a/Mindscape.Raygun4Net4/Properties/AssemblyInfo.cs +++ b/Mindscape.Raygun4Net4/Properties/AssemblyInfo.cs @@ -1,8 +1,7 @@ using System.Reflection; -using System.Runtime.CompilerServices; using System.Runtime.InteropServices; -// General Information about an assembly is controlled through the following +// General Information about an assembly is controlled through the following // set of attributes. Change these attribute values to modify the information // associated with an assembly. [assembly: AssemblyTitle("Raygun4Net4.0")] @@ -10,12 +9,12 @@ [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("Raygun")] [assembly: AssemblyProduct("Raygun4Net")] -[assembly: AssemblyCopyright("Copyright © Raygun 2014-2019")] +[assembly: AssemblyCopyright("Copyright © Raygun 2014-2020")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from // COM, set the ComVisible attribute to true on that type. [assembly: ComVisible(false)] diff --git a/Mindscape.Raygun4Net4/RaygunClient.cs b/Mindscape.Raygun4Net4/RaygunClient.cs index 27c699cc..42ed4a0d 100644 --- a/Mindscape.Raygun4Net4/RaygunClient.cs +++ b/Mindscape.Raygun4Net4/RaygunClient.cs @@ -4,16 +4,14 @@ using System.Linq; using System.Net; using Mindscape.Raygun4Net.Messages; - using System.Web; using System.Threading; using System.Reflection; using Mindscape.Raygun4Net.Builders; -using System.IO; -using System.IO.IsolatedStorage; -using System.Text; using Mindscape.Raygun4Net.Breadcrumbs; using Mindscape.Raygun4Net.Filters; +using Mindscape.Raygun4Net.Logging; +using Mindscape.Raygun4Net.Storage; namespace Mindscape.Raygun4Net { @@ -21,14 +19,37 @@ public class RaygunClient : RaygunClientBase { internal const string UnhandledExceptionTag = "UnhandledException"; + [ThreadStatic] private static RaygunRequestMessage _currentRequestMessage; + [ThreadStatic] private static List<RaygunBreadcrumb> _currentBreadcrumbs; + + private static readonly RaygunBreadcrumbs _breadcrumbs = new RaygunBreadcrumbs(new DefaultBreadcrumbStorage()); + private static object _sendLock = new object(); + private readonly string _apiKey; private readonly RaygunRequestMessageOptions _requestMessageOptions = new RaygunRequestMessageOptions(); private readonly List<Type> _wrapperExceptions = new List<Type>(); - [ThreadStatic] private static RaygunRequestMessage _currentRequestMessage; - [ThreadStatic] private static List<RaygunBreadcrumb> _currentBreadcrumbs; + private IRaygunOfflineStorage _offlineStorage = new IsolatedRaygunOfflineStorage(); - private static readonly RaygunBreadcrumbs _breadcrumbs = new RaygunBreadcrumbs(new DefaultBreadcrumbStorage()); + /// <summary> + /// Gets or sets the username/password credentials which are used to authenticate with the system default Proxy server, if one is set + /// and requires credentials. + /// </summary> + public ICredentials ProxyCredentials { get; set; } + + /// <summary> + /// Gets or sets an IWebProxy instance which can be used to override the default system proxy server settings + /// </summary> + public IWebProxy WebProxy { get; set; } + + /// <summary> + /// Initializes a new instance of the <see cref="RaygunClient" /> class. + /// Uses the ApiKey specified in the config file. + /// </summary> + public RaygunClient() + : this(RaygunSettings.Settings.ApiKey) + { + } /// <summary> /// Initializes a new instance of the <see cref="RaygunClient" /> class. @@ -86,37 +107,6 @@ public RaygunClient(string apiKey) ThreadPool.QueueUserWorkItem(state => { SendStoredMessages(); }); } - /// <summary> - /// Initializes a new instance of the <see cref="RaygunClient" /> class. - /// Uses the ApiKey specified in the config file. - /// </summary> - public RaygunClient() - : this(RaygunSettings.Settings.ApiKey) - { - } - - protected bool ValidateApiKey() - { - if (string.IsNullOrEmpty(_apiKey)) - { - System.Diagnostics.Debug.WriteLine("ApiKey has not been provided, exception will not be logged"); - return false; - } - - return true; - } - - /// <summary> - /// Gets or sets the username/password credentials which are used to authenticate with the system default Proxy server, if one is set - /// and requires credentials. - /// </summary> - public ICredentials ProxyCredentials { get; set; } - - /// <summary> - /// Gets or sets an IWebProxy instance which can be used to override the default system proxy server settings - /// </summary> - public IWebProxy WebProxy { get; set; } - /// <summary> /// Adds a list of outer exceptions that will be stripped, leaving only the valuable inner exception. /// This can be used when a wrapper exception, e.g. TargetInvocationException or HttpUnhandledException, @@ -149,6 +139,8 @@ public void RemoveWrapperExceptions(params Type[] wrapperExceptions) } } + #region Message Scrubbing Properties + /// <summary> /// Adds a list of keys to remove from the following sections of the <see cref="RaygunRequestMessage" /> /// <see cref="RaygunRequestMessage.Headers" /> @@ -218,8 +210,8 @@ public void IgnoreServerVariableNames(params string[] names) } /// <summary> - /// Specifies whether or not RawData from web requests is ignored when sending reports to Raygun.io. - /// The default is false which means RawData will be sent to Raygun.io. + /// Specifies whether or not RawData from web requests is ignored when sending reports to Raygun. + /// The default is false which means RawData will be sent to Raygun. /// </summary> public bool IsRawDataIgnored { @@ -259,7 +251,7 @@ public bool UseKeyValuePairRawDataFilter /// <summary> /// Add an <see cref="IRaygunDataFilter"/> implementation to be used when capturing the raw data - /// of a HTTP request. This filter will be passed the request raw data and is expected to remove + /// of a HTTP request. This filter will be passed the request raw data and is expected to remove /// or replace values whose keys are found in the list supplied to the Filter method. /// </summary> /// <param name="filter">Custom raw data filter implementation.</param> @@ -268,28 +260,9 @@ public void AddRawDataFilter(IRaygunDataFilter filter) _requestMessageOptions.AddRawDataFilter(filter); } - protected override bool CanSend(Exception exception) - { - if (RaygunSettings.Settings.ExcludeErrorsFromLocal && HttpContext.Current != null) - { - try - { - if (HttpContext.Current.Request.IsLocal) - { - return false; - } - } - catch - { - if (RaygunSettings.Settings.ThrowOnError) - { - throw; - } - } - } + #endregion // Message Scrubbing Properties - return base.CanSend(exception); - } + #region Breadcrumbs public static void RecordBreadcrumb(string message) { @@ -306,8 +279,12 @@ public static void ClearBreadcrumbs() _breadcrumbs.Clear(); } + #endregion // Breadcrumbs + + #region Message Send Methods + /// <summary> - /// Transmits an exception to Raygun.io synchronously, using the version number of the originating assembly. + /// Transmits an exception to Raygun synchronously, using the version number of the originating assembly. /// </summary> /// <param name="exception">The exception to deliver.</param> public override void Send(Exception exception) @@ -316,7 +293,7 @@ public override void Send(Exception exception) } /// <summary> - /// Transmits an exception to Raygun.io synchronously specifying a list of string tags associated + /// Transmits an exception to Raygun synchronously specifying a list of string tags associated /// with the message for identification. This uses the version number of the originating assembly. /// </summary> /// <param name="exception">The exception to deliver.</param> @@ -327,7 +304,7 @@ public void Send(Exception exception, IList<string> tags) } /// <summary> - /// Transmits an exception to Raygun.io synchronously specifying a list of string tags associated + /// Transmits an exception to Raygun synchronously specifying a list of string tags associated /// with the message for identification, as well as sending a key-value collection of custom data. /// This uses the version number of the originating assembly. /// </summary> @@ -340,7 +317,7 @@ public void Send(Exception exception, IList<string> tags, IDictionary userCustom } /// <summary> - /// Transmits an exception to Raygun.io synchronously specifying a list of string tags associated + /// Transmits an exception to Raygun synchronously specifying a list of string tags associated /// with the message for identification, as well as sending a key-value collection of custom data. /// This uses the version number of the originating assembly. /// </summary> @@ -361,7 +338,7 @@ public void Send(Exception exception, IList<string> tags, IDictionary userCustom } /// <summary> - /// Asynchronously transmits a message to Raygun.io. + /// Asynchronously transmits a message to Raygun. /// </summary> /// <param name="exception">The exception to deliver.</param> public void SendInBackground(Exception exception) @@ -370,7 +347,7 @@ public void SendInBackground(Exception exception) } /// <summary> - /// Asynchronously transmits an exception to Raygun.io. + /// Asynchronously transmits an exception to Raygun. /// </summary> /// <param name="exception">The exception to deliver.</param> /// <param name="tags">A list of strings associated with the message.</param> @@ -380,7 +357,7 @@ public void SendInBackground(Exception exception, IList<string> tags) } /// <summary> - /// Asynchronously transmits an exception to Raygun.io. + /// Asynchronously transmits an exception to Raygun. /// </summary> /// <param name="exception">The exception to deliver.</param> /// <param name="tags">A list of strings associated with the message.</param> @@ -391,7 +368,7 @@ public void SendInBackground(Exception exception, IList<string> tags, IDictionar } /// <summary> - /// Asynchronously transmits an exception to Raygun.io. + /// Asynchronously transmits an exception to Raygun. /// </summary> /// <param name="exception">The exception to deliver.</param> /// <param name="tags">A list of strings associated with the message.</param> @@ -446,7 +423,7 @@ public void SendInBackground(Exception exception, IList<string> tags, IDictionar } /// <summary> - /// Asynchronously transmits a message to Raygun.io. + /// Asynchronously transmits a message to Raygun. /// </summary> /// <param name="raygunMessage">The RaygunMessage to send. This needs its OccurredOn property /// set to a valid DateTime and as much of the Details property as is available.</param> @@ -455,6 +432,99 @@ public void SendInBackground(RaygunMessage raygunMessage) ThreadPool.QueueUserWorkItem(c => Send(raygunMessage)); } + private void StripAndSend(Exception exception, IList<string> tags, IDictionary userCustomData, RaygunIdentifierMessage userInfo, DateTime? currentTime) + { + foreach (Exception e in StripWrapperExceptions(exception)) + { + Send(BuildMessage(e, tags, userCustomData, userInfo, currentTime)); + } + } + + /// <summary> + /// Posts a RaygunMessage to the Raygun API endpoint. + /// </summary> + /// <param name="raygunMessage">The RaygunMessage to send. This needs its OccurredOn property + /// set to a valid DateTime and as much of the Details property as is available.</param> + public override void Send(RaygunMessage raygunMessage) + { + if (!ValidateApiKey()) + { + RaygunLogger.Instance.Warning("Failed to send error report due to invalid API key"); + return; + } + + bool canSend = OnSendingMessage(raygunMessage); + + if (!canSend) + { + return; + } + + string message = null; + + try + { + message = SimpleJson.SerializeObject(raygunMessage); + } + catch (Exception ex) + { + RaygunLogger.Instance.Error($"Failed to serialize report due to: {ex.Message}"); + + if (RaygunSettings.Settings.ThrowOnError) + { + throw; + } + } + + if (string.IsNullOrEmpty(message)) + { + return; + } + + bool successfullySentReport = true; + + try + { + Send(message); + } + catch (Exception ex) + { + successfullySentReport = false; + + RaygunLogger.Instance.Error($"Failed to send report to Raygun due to: {ex.Message}"); + + SaveMessage(message); + + if (RaygunSettings.Settings.ThrowOnError) + { + throw; + } + } + + if (successfullySentReport) + { + SendStoredMessages(); + } + } + + private void Send(string message) + { + RaygunLogger.Instance.Verbose("Sending Payload --------------"); + RaygunLogger.Instance.Verbose(message); + RaygunLogger.Instance.Verbose("------------------------------"); + + if (WebProxy != null) + { + WebClientHelper.WebProxy = WebProxy; + } + + WebClientHelper.Send(message, _apiKey, ProxyCredentials); + } + + #endregion // Message Send Methods + + #region Message Building Methods + private RaygunRequestMessage BuildRequestMessage() { RaygunRequestMessage requestMessage = null; @@ -468,7 +538,7 @@ private RaygunRequestMessage BuildRequestMessage() } catch (HttpException ex) { - System.Diagnostics.Trace.WriteLine("Error retrieving HttpRequest {0}", ex.Message); + RaygunLogger.Instance.Error($"Error retrieving HttpRequest {ex.Message}"); } if (request != null) @@ -518,14 +588,6 @@ protected RaygunMessage BuildMessage(Exception exception, IList<string> tags, ID return message; } - private void StripAndSend(Exception exception, IList<string> tags, IDictionary userCustomData, RaygunIdentifierMessage userInfo, DateTime? currentTime) - { - foreach (Exception e in StripWrapperExceptions(exception)) - { - Send(BuildMessage(e, tags, userCustomData, userInfo, currentTime)); - } - } - protected IEnumerable<Exception> StripWrapperExceptions(Exception exception) { if (exception != null && _wrapperExceptions.Any(wrapperException => exception.GetType() == wrapperException && (exception.InnerException != null || exception is ReflectionTypeLoadException))) @@ -580,201 +642,123 @@ protected IEnumerable<Exception> StripWrapperExceptions(Exception exception) } } - /// <summary> - /// Posts a RaygunMessage to the Raygun.io api endpoint. - /// </summary> - /// <param name="raygunMessage">The RaygunMessage to send. This needs its OccurredOn property - /// set to a valid DateTime and as much of the Details property as is available.</param> - public override void Send(RaygunMessage raygunMessage) + #endregion // Message Building Methods + + #region Message Offline Storage + + private void SaveMessage(string message) { - bool canSend = OnSendingMessage(raygunMessage); - if (canSend) + if (!RaygunSettings.Settings.CrashReportingOfflineStorageEnabled) + { + RaygunLogger.Instance.Warning("Offline storage is disabled, skipping saving report."); + return; + } + + if (!ValidateApiKey()) + { + RaygunLogger.Instance.Warning("Failed to save report due to invalid API key."); + return; + } + + // Avoid writing and reading from disk at the same time with `SendStoredMessages`. + lock (_sendLock) { - string message = null; try { - message = SimpleJson.SerializeObject(raygunMessage); - } - catch (Exception serializeException) - { - System.Diagnostics.Trace.WriteLine($"Error serializing Raygun message due to: {serializeException}"); - - if (RaygunSettings.Settings.ThrowOnError) + if (!_offlineStorage.Store(message, _apiKey)) { - throw; + RaygunLogger.Instance.Warning("Failed to save report to offline storage"); } } - - if (message != null) + catch (Exception ex) { - try - { - if (WebProxy != null) - { - WebClientHelper.WebProxy = WebProxy; - } - - WebClientHelper.Send(message, _apiKey, ProxyCredentials); - } - catch (Exception sendMessageException) - { - try - { - System.Diagnostics.Trace.WriteLine($"Error sending exception to Raygun.io due to: {sendMessageException}"); - - // Attempt to store the message in isolated storage. - SaveMessage(message); - } - catch (Exception saveMessageException) - { - // Ignored - System.Diagnostics.Trace.WriteLine($"Error saving Raygun message due to: {saveMessageException}"); - } - - if (RaygunSettings.Settings.ThrowOnError) - { - throw; - } - } - - SendStoredMessages(); + RaygunLogger.Instance.Error($"Failed to save report to offline storage due to: {ex.Message}"); } } } - private void SaveMessage(string message) + private void SendStoredMessages() { - try + if (!RaygunSettings.Settings.CrashReportingOfflineStorageEnabled) + { + RaygunLogger.Instance.Warning("Offline storage is disabled, skipping sending stored reports."); + return; + } + + if (!ValidateApiKey()) { - using (IsolatedStorageFile isolatedStorage = GetIsolatedStorageScope()) + RaygunLogger.Instance.Warning("Failed to send offline reports due to invalid API key."); + return; + } + + lock (_sendLock) + { + try { - string directoryName = "RaygunOfflineStorage"; - if (!isolatedStorage.DirectoryExists(directoryName)) - { - isolatedStorage.CreateDirectory(directoryName); - } + var files = _offlineStorage.FetchAll(_apiKey); - int number = 1; - while (true) + foreach (var file in files) { - bool exists = isolatedStorage.FileExists(directoryName + "\\RaygunErrorMessage" + number + ".txt"); - if (!exists) + try { - string nextFileName = directoryName + "\\RaygunErrorMessage" + (number + 1) + ".txt"; - exists = isolatedStorage.FileExists(nextFileName); - if (exists) + // Send the stored report. + Send(file.Contents); + + // Remove the stored report from local storage. + if (_offlineStorage.Remove(file.Name, _apiKey)) { - isolatedStorage.DeleteFile(nextFileName); + RaygunLogger.Instance.Info("Successfully removed report from offline storage."); + } + else + { + RaygunLogger.Instance.Warning("Failed to remove report from offline storage."); } - - break; } - - number++; - } - - if (number == 11) - { - string firstFileName = directoryName + "\\RaygunErrorMessage1.txt"; - if (isolatedStorage.FileExists(firstFileName)) + catch (Exception ex) { - isolatedStorage.DeleteFile(firstFileName); - } - } - - string reportFilePath = directoryName + "\\RaygunErrorMessage" + number + ".txt"; + RaygunLogger.Instance.Error($"Failed to send stored report to Raygun due to: {ex.Message}"); - using (IsolatedStorageFileStream isoStream = new IsolatedStorageFileStream(reportFilePath, FileMode.OpenOrCreate, FileAccess.Write, isolatedStorage)) - { - using (StreamWriter writer = new StreamWriter(isoStream, Encoding.Unicode)) - { - writer.Write(message); - writer.Flush(); - writer.Close(); + // If just one message fails to send, then don't delete the message, + // and don't attempt sending anymore until later. + return; } } - - System.Diagnostics.Trace.WriteLine("Saved Raygun message to: " + reportFilePath); } - } - catch (Exception ex) - { - System.Diagnostics.Trace.WriteLine($"Error saving message to isolated storage {ex}"); + catch (Exception ex) + { + RaygunLogger.Instance.Error($"Failed to send stored report to Raygun due to: {ex.Message}"); + } } } - private static object _sendLock = new object(); + #endregion // Message Offline Storage - private void SendStoredMessages() + protected bool ValidateApiKey() { - lock (_sendLock) + return !string.IsNullOrEmpty(_apiKey); + } + + protected override bool CanSend(Exception exception) + { + if (RaygunSettings.Settings.ExcludeErrorsFromLocal && HttpContext.Current != null) { - if (!ValidateApiKey()) - { - System.Diagnostics.Debug.WriteLine("ApiKey has not been provided, skipping sending stored Raygun messages"); - return; - } - try { - using (IsolatedStorageFile isolatedStorage = GetIsolatedStorageScope()) + if (HttpContext.Current.Request.IsLocal) { - string directoryName = "RaygunOfflineStorage"; - if (isolatedStorage.DirectoryExists(directoryName)) - { - string[] fileNames = isolatedStorage.GetFileNames(directoryName + "\\*.txt"); - foreach (string name in fileNames) - { - IsolatedStorageFileStream isoFileStream = isolatedStorage.OpenFile(directoryName + "\\" + name, FileMode.Open); - using (StreamReader reader = new StreamReader(isoFileStream)) - { - string text = reader.ReadToEnd(); - try - { - if (WebProxy != null) - { - WebClientHelper.WebProxy = WebProxy; - } - - WebClientHelper.Send(text, _apiKey, ProxyCredentials); - } - catch - { - // If just one message fails to send, then don't delete the message, and don't attempt sending anymore until later. - return; - } - - System.Diagnostics.Debug.WriteLine("Sent " + name); - } - - isolatedStorage.DeleteFile(directoryName + "\\" + name); - } - - if (isolatedStorage.GetFileNames(directoryName + "\\*.txt").Length == 0) - { - System.Diagnostics.Debug.WriteLine("Successfully sent all pending messages"); - isolatedStorage.DeleteDirectory(directoryName); - } - } + return false; } } - catch (Exception ex) + catch { - System.Diagnostics.Debug.WriteLine($"Error sending stored messages to Raygun.io due to: {ex}"); + if (RaygunSettings.Settings.ThrowOnError) + { + throw; + } } } - } - private IsolatedStorageFile GetIsolatedStorageScope() - { - if (AppDomain.CurrentDomain != null && AppDomain.CurrentDomain.ActivationContext != null) - { - return IsolatedStorageFile.GetUserStoreForApplication(); - } - else - { - return IsolatedStorageFile.GetUserStoreForAssembly(); - } + return base.CanSend(exception); } } } \ No newline at end of file diff --git a/Mindscape.Raygun4Net4/WebClientHelper.cs b/Mindscape.Raygun4Net4/WebClientHelper.cs index e087a576..2930c21e 100644 --- a/Mindscape.Raygun4Net4/WebClientHelper.cs +++ b/Mindscape.Raygun4Net4/WebClientHelper.cs @@ -1,5 +1,6 @@ using System; using System.Net; +using Mindscape.Raygun4Net.Logging; namespace Mindscape.Raygun4Net { @@ -25,7 +26,7 @@ public static void Send(string message, string apiKey, ICredentials proxyCredent { if (string.IsNullOrEmpty(apiKey)) { - System.Diagnostics.Trace.WriteLine("ApiKey has not been provided, the Raygun message will not be sent"); + RaygunLogger.Instance.Warning("ApiKey has not been provided, the Raygun message will not be sent"); return; }