From a25db7cd060afd61a8a67508238583d5967c8cc8 Mon Sep 17 00:00:00 2001 From: Sean Date: Tue, 14 May 2024 15:00:35 +1200 Subject: [PATCH 01/21] Add support for offline storage of errors to Raygun4Net Core This is an early implementation to allow for early feedback and suggestions --- .../RaygunClientBase.cs | 101 ++++++++++++++---- .../Storage/BackgroundOfflineErrorSender.cs | 88 +++++++++++++++ .../Storage/IOfflineErrorStore.cs | 13 +++ .../Storage/OfflineErrorRecord.cs | 21 ++++ .../Storage/FileSystemErrorStore.cs | 98 +++++++++++++++++ .../Storage/LocalApplicationDataStorage.cs | 40 +++++++ 6 files changed, 338 insertions(+), 23 deletions(-) create mode 100644 Mindscape.Raygun4Net.NetCore.Common/Storage/BackgroundOfflineErrorSender.cs create mode 100644 Mindscape.Raygun4Net.NetCore.Common/Storage/IOfflineErrorStore.cs create mode 100644 Mindscape.Raygun4Net.NetCore.Common/Storage/OfflineErrorRecord.cs create mode 100644 Mindscape.Raygun4Net.NetCore/Storage/FileSystemErrorStore.cs create mode 100644 Mindscape.Raygun4Net.NetCore/Storage/LocalApplicationDataStorage.cs diff --git a/Mindscape.Raygun4Net.NetCore.Common/RaygunClientBase.cs b/Mindscape.Raygun4Net.NetCore.Common/RaygunClientBase.cs index ffa7c45b..a6699b13 100644 --- a/Mindscape.Raygun4Net.NetCore.Common/RaygunClientBase.cs +++ b/Mindscape.Raygun4Net.NetCore.Common/RaygunClientBase.cs @@ -9,6 +9,7 @@ using System.Threading; using System.Threading.Tasks; using Mindscape.Raygun4Net.Breadcrumbs; +using Mindscape.Raygun4Net.Storage; namespace Mindscape.Raygun4Net { @@ -19,7 +20,7 @@ public abstract class RaygunClientBase /// /// If no HttpClient is provided to the constructor, this will be used. /// - private static readonly HttpClient DefaultClient = new () + private static readonly HttpClient DefaultClient = new() { // The default timeout is 100 seconds for the HttpClient, Timeout = TimeSpan.FromSeconds(30) @@ -39,13 +40,16 @@ public abstract class RaygunClientBase private readonly ThrottledBackgroundMessageProcessor _backgroundMessageProcessor; private readonly IRaygunUserProvider _userProvider; protected internal const string SentKey = "AlreadySentByRaygun"; - + /// /// Store a strong reference to the OnApplicationUnhandledException delegate so it does not get garbage collected while /// the client is still alive /// + // ReSharper disable once PrivateFieldCanBeConvertedToLocalVariable private readonly UnhandledExceptionBridge.UnhandledExceptionHandler _onUnhandledExceptionDelegate; + private readonly IOfflineErrorStore _offlineErrorStore; + /// /// Raised just before a message is sent. This can be used to make final adjustments to the , or to cancel the send. @@ -115,7 +119,13 @@ protected RaygunClientBase(RaygunSettingsBase settings, IRaygunUserProvider user { } + // ReSharper disable once IntroduceOptionalParameters.Global protected RaygunClientBase(RaygunSettingsBase settings, HttpClient client, IRaygunUserProvider userProvider) + : this(settings, client, userProvider, null) + { + } + + protected RaygunClientBase(RaygunSettingsBase settings, HttpClient client, IRaygunUserProvider userProvider, IOfflineErrorStore offlineErrorStore) { _client = client ?? DefaultClient; _settings = settings; @@ -123,10 +133,16 @@ protected RaygunClientBase(RaygunSettingsBase settings, HttpClient client, IRayg _userProvider = userProvider; _wrapperExceptions.Add(typeof(TargetInvocationException)); - + _onUnhandledExceptionDelegate = OnApplicationUnhandledException; - + UnhandledExceptionBridge.OnUnhandledException(_onUnhandledExceptionDelegate); + + if (offlineErrorStore != null) + { + _offlineErrorStore = offlineErrorStore; + BackgroundOfflineErrorReporter.SetSendCallback(Send); + } } /// @@ -160,6 +176,7 @@ public void RemoveWrapperExceptions(params Type[] wrapperExceptions) _wrapperExceptions.Remove(wrapper); } } + protected virtual bool CanSend(Exception exception) { return exception?.Data == null || !exception.Data.Contains(SentKey) || @@ -260,7 +277,12 @@ protected async Task OnCustomGroupingKey(Exception exception, RaygunMess protected bool ValidateApiKey() { - if (string.IsNullOrEmpty(_settings.ApiKey)) + return ValidateApiKey(_settings.ApiKey); + } + + private bool ValidateApiKey(string apiKey) + { + if (string.IsNullOrEmpty(apiKey)) { Debug.WriteLine("ApiKey has not been provided, exception will not be logged"); return false; @@ -363,10 +385,10 @@ public virtual Task SendInBackground(Exception exception, IList tags = n Debug.WriteLine("Could not add message to background queue. Dropping exception: {0}", ex); } } - + FlagAsSent(exception); } - + return Task.CompletedTask; } @@ -381,17 +403,16 @@ public Task SendInBackground(RaygunMessage raygunMessage) { Debug.WriteLine("Could not add message to background queue. Dropping message: {0}", raygunMessage); } - + return Task.CompletedTask; } - protected async Task BuildMessage(Exception exception, - IList tags, - IDictionary userCustomData = null, - RaygunIdentifierMessage userInfo = null, - Action customiseMessage = null) + protected async Task BuildMessage(Exception exception, + IList tags, + IDictionary userCustomData = null, + RaygunIdentifierMessage userInfo = null, + Action customiseMessage = null) { - var message = RaygunMessageBuilder.New(_settings) .SetEnvironmentDetails() .SetMachineName(Environment.MachineName) @@ -416,7 +437,7 @@ protected async Task BuildMessage(Exception exception, } protected virtual async Task StripAndSend(Exception exception, IList tags, IDictionary userCustomData, - RaygunIdentifierMessage userInfo) + RaygunIdentifierMessage userInfo) { foreach (var e in StripWrapperExceptions(exception)) { @@ -427,7 +448,7 @@ protected virtual async Task StripAndSend(Exception exception, IList tag protected virtual IEnumerable StripWrapperExceptions(Exception exception) { if (exception != null && _wrapperExceptions.Any(wrapperException => - exception.GetType() == wrapperException && exception.InnerException != null)) + exception.GetType() == wrapperException && exception.InnerException != null)) { var aggregate = exception as AggregateException; @@ -471,9 +492,14 @@ public Task Send(RaygunMessage 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. /// - public async Task Send(RaygunMessage raygunMessage, CancellationToken cancellationToken) + public Task Send(RaygunMessage raygunMessage, CancellationToken cancellationToken) { - if (!ValidateApiKey()) + return Send(raygunMessage, _settings.ApiKey, cancellationToken); + } + + internal async Task Send(RaygunMessage raygunMessage, string apiKey, CancellationToken cancellationToken) + { + if (!ValidateApiKey(apiKey)) { return; } @@ -485,19 +511,25 @@ public async Task Send(RaygunMessage raygunMessage, CancellationToken cancellati return; } + var hasMessageBeenStored = false; var requestMessage = new HttpRequestMessage(HttpMethod.Post, _settings.ApiEndpoint); - requestMessage.Headers.Add("X-ApiKey", _settings.ApiKey); + requestMessage.Headers.Add("X-ApiKey", apiKey); try { var message = SimpleJson.SerializeObject(raygunMessage); requestMessage.Content = new StringContent(message, Encoding.UTF8, "application/json"); + var response = await _client.SendAsync(requestMessage, cancellationToken).ConfigureAwait(false); - var result = await _client.SendAsync(requestMessage, cancellationToken).ConfigureAwait(false); - - if (!result.IsSuccessStatusCode) + if (!response.IsSuccessStatusCode) { - Debug.WriteLine($"Error Logging Exception to Raygun {result.ReasonPhrase}"); + Debug.WriteLine($"Error Logging Exception to Raygun {response.ReasonPhrase}"); + + if ((int)response.StatusCode >= 500) + { + // If we got a server error then add it to offline storage to send later + hasMessageBeenStored = await SaveMessageToOfflineStore(raygunMessage, apiKey, cancellationToken); + } if (_settings.ThrowOnError) { @@ -509,11 +541,34 @@ public async Task Send(RaygunMessage raygunMessage, CancellationToken cancellati { Debug.WriteLine($"Error Logging Exception to Raygun {ex.Message}"); + if (!hasMessageBeenStored) + { + // Do our best to save it in the offline storage if it hasn't been saved yet + await SaveMessageToOfflineStore(raygunMessage, apiKey, cancellationToken); + } + if (_settings.ThrowOnError) { throw; } } } + + + private async Task SaveMessageToOfflineStore(RaygunMessage message, string apiKey, CancellationToken cancellationToken) + { + // Can't store it anywhere + if (_offlineErrorStore is null) + { + return false; + } + + return await _offlineErrorStore.Save(new OfflineErrorRecord + { + Id = Guid.NewGuid(), + Message = message, + ApiKey = apiKey + }, cancellationToken); + } } } \ No newline at end of file diff --git a/Mindscape.Raygun4Net.NetCore.Common/Storage/BackgroundOfflineErrorSender.cs b/Mindscape.Raygun4Net.NetCore.Common/Storage/BackgroundOfflineErrorSender.cs new file mode 100644 index 00000000..685ef2bb --- /dev/null +++ b/Mindscape.Raygun4Net.NetCore.Common/Storage/BackgroundOfflineErrorSender.cs @@ -0,0 +1,88 @@ +using System; +using System.Diagnostics; +using System.Threading; +using System.Threading.Tasks; + +namespace Mindscape.Raygun4Net.Storage; + +public static class BackgroundOfflineErrorReporter +{ + internal delegate Task SendHandler(RaygunMessage message, string apiKey, CancellationToken cancellationToken); + + private static readonly Timer BackgroundTimer = new Timer(SendOfflineErrors); + private static volatile bool _isRunning; + private static TimeSpan _interval = TimeSpan.FromSeconds(30); + private static SendHandler _sendHandler; + private static Func _offlineErrorStore; + + public static TimeSpan Interval + { + get { return _interval; } + set + { + _interval = value; + + // Set the new interval on the timer + BackgroundTimer.Change(TimeSpan.Zero, Interval); + } + } + + public static bool IsRunning => _isRunning; + + static BackgroundOfflineErrorReporter() + { + Start(); + } + + internal static void SetErrorStore(Func offlineStoreFunc) + { + _offlineErrorStore = offlineStoreFunc; + } + + internal static void SetSendCallback(SendHandler sendHandler) + { + _sendHandler = sendHandler; + } + + private static async void SendOfflineErrors(object state) + { + var store = _offlineErrorStore?.Invoke(); + + // We don't have a store set, or a send handler - so we can't actually do anything + if (store is null || _sendHandler is null) + return; + + try + { + var errors = await store.GetAll(CancellationToken.None); + foreach (var error in errors) + { + await _sendHandler(error.Message, error.ApiKey, CancellationToken.None); + await store.Remove(error.Id, CancellationToken.None); + } + } + catch (Exception ex) + { + Debug.WriteLine($"Exception sending offline errors: {ex}"); + } + } + + /// + /// Start the internal timer. This will enable the sending of any offline stored errors. + /// This requires SetSendCallback to be + /// + public static void Start() + { + BackgroundTimer.Change(TimeSpan.Zero, Interval); + _isRunning = true; + } + + /// + /// Stop the internal timer - and prevents any offline errors from being sent in the background + /// + public static void Stop() + { + BackgroundTimer.Change(Timeout.Infinite, 0); + _isRunning = false; + } +} \ No newline at end of file diff --git a/Mindscape.Raygun4Net.NetCore.Common/Storage/IOfflineErrorStore.cs b/Mindscape.Raygun4Net.NetCore.Common/Storage/IOfflineErrorStore.cs new file mode 100644 index 00000000..ca2e37c8 --- /dev/null +++ b/Mindscape.Raygun4Net.NetCore.Common/Storage/IOfflineErrorStore.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace Mindscape.Raygun4Net.Storage; + +public interface IOfflineErrorStore +{ + public Task> GetAll(CancellationToken cancellationToken); + public Task Save(OfflineErrorRecord errorRecord, CancellationToken cancellationToken); + public Task Remove(Guid errorId, CancellationToken cancellationToken); +} \ No newline at end of file diff --git a/Mindscape.Raygun4Net.NetCore.Common/Storage/OfflineErrorRecord.cs b/Mindscape.Raygun4Net.NetCore.Common/Storage/OfflineErrorRecord.cs new file mode 100644 index 00000000..ab12b9ea --- /dev/null +++ b/Mindscape.Raygun4Net.NetCore.Common/Storage/OfflineErrorRecord.cs @@ -0,0 +1,21 @@ +using System; + +namespace Mindscape.Raygun4Net.Storage; + +public sealed class OfflineErrorRecord +{ + /// + /// Unique ID for the record + /// + public Guid Id { get; set; } = Guid.NewGuid(); + + /// + /// The application api key that the payload was intended for + /// + public string ApiKey { get; set; } + + /// + /// The JSON serialized payload of the error + /// + public RaygunMessage Message { get; set; } +} \ No newline at end of file diff --git a/Mindscape.Raygun4Net.NetCore/Storage/FileSystemErrorStore.cs b/Mindscape.Raygun4Net.NetCore/Storage/FileSystemErrorStore.cs new file mode 100644 index 00000000..56d28781 --- /dev/null +++ b/Mindscape.Raygun4Net.NetCore/Storage/FileSystemErrorStore.cs @@ -0,0 +1,98 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.IO.Compression; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace Mindscape.Raygun4Net.Storage; + +public class FileSystemErrorStore : IOfflineErrorStore +{ + private readonly string _storageDirectory; + + public FileSystemErrorStore(string storageDirectory) + { + _storageDirectory = storageDirectory; + } + + public virtual async Task> GetAll(CancellationToken cancellationToken) + { + var crashFiles = Directory.GetFiles(_storageDirectory, "*.crash"); + var errorRecords = new List(); + + foreach (var crashFile in crashFiles) + { + try + { + using var fileStream = new FileStream(crashFile, FileMode.Open); + using var gzipStream = new GZipStream(fileStream, CompressionMode.Decompress); + using var reader = new StreamReader(gzipStream, Encoding.UTF8); + + Trace.WriteLine($"Attempting to load offline crash at {crashFile}"); + var jsonString = await reader.ReadToEndAsync(); + var errorRecord = SimpleJson.DeserializeObject(jsonString); + + errorRecords.Add(errorRecord); + } + catch (Exception ex) + { + Debug.WriteLine("Error deserializing offline crash: {0}", ex.ToString()); + } + } + + return errorRecords; + } + + public virtual async Task Save(OfflineErrorRecord errorRecord, CancellationToken cancellationToken) + { + try + { + Directory.CreateDirectory(_storageDirectory); + + var filePath = GetFilePath(errorRecord.Id); + var jsonContent = SimpleJson.SerializeObject(errorRecord); + + using var fileStream = new FileStream(filePath, FileMode.Create); + using var gzipStream = new GZipStream(fileStream, CompressionLevel.Optimal); + using var writer = new StreamWriter(gzipStream, Encoding.UTF8); + + Trace.WriteLine($"Saving crash {errorRecord.Id} to {filePath}"); + await writer.WriteAsync(jsonContent); + await writer.FlushAsync(); + + return true; + } + catch (Exception ex) + { + Debug.WriteLine($"Error adding crash [{errorRecord.Id}] to store: {ex}"); + return false; + } + } + + public virtual Task Remove(Guid errorId, CancellationToken cancellationToken) + { + try + { + var filePath = GetFilePath(errorId); + if (File.Exists(filePath)) + { + File.Delete(filePath); + return Task.FromResult(true); + } + } + catch (Exception ex) + { + Debug.WriteLine($"Error remove crash [{errorId}] from store: {ex}"); + } + + return Task.FromResult(false); + } + + private string GetFilePath(Guid errorId) + { + return Path.Combine(_storageDirectory, $"raygun-cr-{errorId}.crash"); + } +} \ No newline at end of file diff --git a/Mindscape.Raygun4Net.NetCore/Storage/LocalApplicationDataStorage.cs b/Mindscape.Raygun4Net.NetCore/Storage/LocalApplicationDataStorage.cs new file mode 100644 index 00000000..629cdb96 --- /dev/null +++ b/Mindscape.Raygun4Net.NetCore/Storage/LocalApplicationDataStorage.cs @@ -0,0 +1,40 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Reflection; +using System.Security.Cryptography; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace Mindscape.Raygun4Net.Storage; + +public sealed class LocalApplicationDataStorage : IOfflineErrorStore +{ + private readonly FileSystemErrorStore _fileSystemErrorStorage; + + public LocalApplicationDataStorage(string uniqueApplicationId = null) + { + var uniqueId = uniqueApplicationId ?? Assembly.GetEntryAssembly()?.Location ?? throw new ApplicationException("Cannot determine unique id"); + var uniqueIdHash = SHA1.Create().ComputeHash(Encoding.UTF8.GetBytes(uniqueId)); + var uniqueIdDirectory = Encoding.UTF8.GetString(uniqueIdHash); + + var localAppDirectory = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), uniqueIdDirectory); + _fileSystemErrorStorage = new FileSystemErrorStore(localAppDirectory); + } + + public Task> GetAll(CancellationToken cancellationToken) + { + return _fileSystemErrorStorage.GetAll(cancellationToken); + } + + public Task Save(OfflineErrorRecord errorRecord, CancellationToken cancellationToken) + { + return _fileSystemErrorStorage.Save(errorRecord, cancellationToken); + } + + public Task Remove(Guid errorId, CancellationToken cancellationToken) + { + return _fileSystemErrorStorage.Remove(errorId, cancellationToken); + } +} \ No newline at end of file From 1fddfd9a62e307104cad0ec1873873804c0a3d13 Mon Sep 17 00:00:00 2001 From: Sean Date: Tue, 14 May 2024 15:03:13 +1200 Subject: [PATCH 02/21] Add overload to ctor --- Mindscape.Raygun4Net.NetCore/RaygunClient.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Mindscape.Raygun4Net.NetCore/RaygunClient.cs b/Mindscape.Raygun4Net.NetCore/RaygunClient.cs index 7c31704b..f150051e 100644 --- a/Mindscape.Raygun4Net.NetCore/RaygunClient.cs +++ b/Mindscape.Raygun4Net.NetCore/RaygunClient.cs @@ -1,5 +1,6 @@ using System; using System.Net.Http; +using Mindscape.Raygun4Net.Storage; namespace Mindscape.Raygun4Net; @@ -33,6 +34,11 @@ public RaygunClient(RaygunSettings settings, IRaygunUserProvider userProvider) : public RaygunClient(RaygunSettings settings, HttpClient httpClient, IRaygunUserProvider userProvider) : base(settings, httpClient, userProvider) { } + + public RaygunClient(RaygunSettings settings, HttpClient httpClient, IRaygunUserProvider userProvider, IOfflineErrorStore offlineErrorStore) : base(settings, httpClient, userProvider, offlineErrorStore) + { + } + // ReSharper restore MemberCanBeProtected.Global // ReSharper restore SuggestBaseTypeForParameterInConstructor // ReSharper restore UnusedMember.Global From 61d25e87c78cf667dbb2e812cdd5bd2a7e168b48 Mon Sep 17 00:00:00 2001 From: Sean Date: Wed, 15 May 2024 10:20:04 +1200 Subject: [PATCH 03/21] Fix the serialization issues and restructure the sending of payloads --- .../RaygunClientBase.cs | 81 ++++++++++++------- .../Storage/BackgroundOfflineErrorSender.cs | 6 +- .../Storage/OfflineErrorRecord.cs | 2 +- Mindscape.Raygun4Net.NetCore/RaygunClient.cs | 8 ++ .../Storage/FileSystemErrorStore.cs | 2 +- .../Storage/LocalApplicationDataStorage.cs | 2 +- 6 files changed, 68 insertions(+), 33 deletions(-) diff --git a/Mindscape.Raygun4Net.NetCore.Common/RaygunClientBase.cs b/Mindscape.Raygun4Net.NetCore.Common/RaygunClientBase.cs index a6699b13..7069f789 100644 --- a/Mindscape.Raygun4Net.NetCore.Common/RaygunClientBase.cs +++ b/Mindscape.Raygun4Net.NetCore.Common/RaygunClientBase.cs @@ -106,16 +106,24 @@ private void OnApplicationUnhandledException(Exception exception, bool isTermina Send(exception, UnhandledExceptionTags); } - protected RaygunClientBase(RaygunSettingsBase settings) : this(settings, DefaultClient, null) + protected RaygunClientBase(RaygunSettingsBase settings) + : this(settings, DefaultClient, null, null) { } // ReSharper disable once IntroduceOptionalParameters.Global - protected RaygunClientBase(RaygunSettingsBase settings, HttpClient client) : this(settings, client, null) + protected RaygunClientBase(RaygunSettingsBase settings, HttpClient client) + : this(settings, client, null, null) { } - protected RaygunClientBase(RaygunSettingsBase settings, IRaygunUserProvider userProvider) : this(settings, DefaultClient, userProvider) + protected RaygunClientBase(RaygunSettingsBase settings, IRaygunUserProvider userProvider) + : this(settings, DefaultClient, userProvider) + { + } + + protected RaygunClientBase(RaygunSettingsBase settings, IOfflineErrorStore offlineErrorStore) + : this(settings, DefaultClient, offlineErrorStore) { } @@ -125,6 +133,11 @@ protected RaygunClientBase(RaygunSettingsBase settings, HttpClient client, IRayg { } + protected RaygunClientBase(RaygunSettingsBase settings, HttpClient client, IOfflineErrorStore offlineErrorStore) + : this(settings, client, null, offlineErrorStore) + { + } + protected RaygunClientBase(RaygunSettingsBase settings, HttpClient client, IRaygunUserProvider userProvider, IOfflineErrorStore offlineErrorStore) { _client = client ?? DefaultClient; @@ -141,7 +154,7 @@ protected RaygunClientBase(RaygunSettingsBase settings, HttpClient client, IRayg if (offlineErrorStore != null) { _offlineErrorStore = offlineErrorStore; - BackgroundOfflineErrorReporter.SetSendCallback(Send); + BackgroundOfflineErrorReporter.SetSendCallback(SendPayloadAsync); } } @@ -339,6 +352,15 @@ public void Send(Exception exception, IList tags, IDictionary userCustom } } + /// + /// Transmits an exception to Raygun asynchronously. + /// + /// The exception to deliver. + public Task SendAsync(Exception exception) + { + return SendAsync(exception, null, null); + } + /// /// Transmits an exception to Raygun asynchronously. /// @@ -492,14 +514,9 @@ public Task Send(RaygunMessage 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. /// - public Task Send(RaygunMessage raygunMessage, CancellationToken cancellationToken) + public async Task Send(RaygunMessage raygunMessage, CancellationToken cancellationToken) { - return Send(raygunMessage, _settings.ApiKey, cancellationToken); - } - - internal async Task Send(RaygunMessage raygunMessage, string apiKey, CancellationToken cancellationToken) - { - if (!ValidateApiKey(apiKey)) + if (!ValidateApiKey(_settings.ApiKey)) { return; } @@ -511,14 +528,29 @@ internal async Task Send(RaygunMessage raygunMessage, string apiKey, Cancellatio return; } + try + { + var messagePayload = SimpleJson.SerializeObject(raygunMessage); + await SendPayloadAsync(messagePayload, _settings.ApiKey, cancellationToken); + } + catch (Exception ex) + { + if (_settings.ThrowOnError) + { + throw; + } + } + } + + internal async Task SendPayloadAsync(string payload, string apiKey, CancellationToken cancellationToken) + { var hasMessageBeenStored = false; var requestMessage = new HttpRequestMessage(HttpMethod.Post, _settings.ApiEndpoint); requestMessage.Headers.Add("X-ApiKey", apiKey); + requestMessage.Content = new StringContent(payload, Encoding.UTF8, "application/json"); try { - var message = SimpleJson.SerializeObject(raygunMessage); - requestMessage.Content = new StringContent(message, Encoding.UTF8, "application/json"); var response = await _client.SendAsync(requestMessage, cancellationToken).ConfigureAwait(false); if (!response.IsSuccessStatusCode) @@ -528,13 +560,11 @@ internal async Task Send(RaygunMessage raygunMessage, string apiKey, Cancellatio if ((int)response.StatusCode >= 500) { // If we got a server error then add it to offline storage to send later - hasMessageBeenStored = await SaveMessageToOfflineStore(raygunMessage, apiKey, cancellationToken); + hasMessageBeenStored = await SaveMessageToOfflineStore(payload, apiKey, cancellationToken); } - if (_settings.ThrowOnError) - { - throw new Exception("Could not log to Raygun"); - } + // Cause an exception to be bubbled up the stack + response.EnsureSuccessStatusCode(); } } catch (Exception ex) @@ -544,18 +574,15 @@ internal async Task Send(RaygunMessage raygunMessage, string apiKey, Cancellatio if (!hasMessageBeenStored) { // Do our best to save it in the offline storage if it hasn't been saved yet - await SaveMessageToOfflineStore(raygunMessage, apiKey, cancellationToken); - } - - if (_settings.ThrowOnError) - { - throw; + await SaveMessageToOfflineStore(payload, apiKey, cancellationToken); } + + throw; } } - - private async Task SaveMessageToOfflineStore(RaygunMessage message, string apiKey, CancellationToken cancellationToken) + + private async Task SaveMessageToOfflineStore(string messagePayload, string apiKey, CancellationToken cancellationToken) { // Can't store it anywhere if (_offlineErrorStore is null) @@ -566,7 +593,7 @@ private async Task SaveMessageToOfflineStore(RaygunMessage message, string return await _offlineErrorStore.Save(new OfflineErrorRecord { Id = Guid.NewGuid(), - Message = message, + MessagePayload = messagePayload, ApiKey = apiKey }, cancellationToken); } diff --git a/Mindscape.Raygun4Net.NetCore.Common/Storage/BackgroundOfflineErrorSender.cs b/Mindscape.Raygun4Net.NetCore.Common/Storage/BackgroundOfflineErrorSender.cs index 685ef2bb..dff81e3d 100644 --- a/Mindscape.Raygun4Net.NetCore.Common/Storage/BackgroundOfflineErrorSender.cs +++ b/Mindscape.Raygun4Net.NetCore.Common/Storage/BackgroundOfflineErrorSender.cs @@ -7,7 +7,7 @@ namespace Mindscape.Raygun4Net.Storage; public static class BackgroundOfflineErrorReporter { - internal delegate Task SendHandler(RaygunMessage message, string apiKey, CancellationToken cancellationToken); + internal delegate Task SendHandler(string messagePayload, string apiKey, CancellationToken cancellationToken); private static readonly Timer BackgroundTimer = new Timer(SendOfflineErrors); private static volatile bool _isRunning; @@ -34,7 +34,7 @@ static BackgroundOfflineErrorReporter() Start(); } - internal static void SetErrorStore(Func offlineStoreFunc) + public static void SetErrorStore(Func offlineStoreFunc) { _offlineErrorStore = offlineStoreFunc; } @@ -57,7 +57,7 @@ private static async void SendOfflineErrors(object state) var errors = await store.GetAll(CancellationToken.None); foreach (var error in errors) { - await _sendHandler(error.Message, error.ApiKey, CancellationToken.None); + await _sendHandler(error.MessagePayload, error.ApiKey, CancellationToken.None); await store.Remove(error.Id, CancellationToken.None); } } diff --git a/Mindscape.Raygun4Net.NetCore.Common/Storage/OfflineErrorRecord.cs b/Mindscape.Raygun4Net.NetCore.Common/Storage/OfflineErrorRecord.cs index ab12b9ea..f2e26606 100644 --- a/Mindscape.Raygun4Net.NetCore.Common/Storage/OfflineErrorRecord.cs +++ b/Mindscape.Raygun4Net.NetCore.Common/Storage/OfflineErrorRecord.cs @@ -17,5 +17,5 @@ public sealed class OfflineErrorRecord /// /// The JSON serialized payload of the error /// - public RaygunMessage Message { get; set; } + public string MessagePayload { get; set; } } \ No newline at end of file diff --git a/Mindscape.Raygun4Net.NetCore/RaygunClient.cs b/Mindscape.Raygun4Net.NetCore/RaygunClient.cs index f150051e..905766ac 100644 --- a/Mindscape.Raygun4Net.NetCore/RaygunClient.cs +++ b/Mindscape.Raygun4Net.NetCore/RaygunClient.cs @@ -30,11 +30,19 @@ public RaygunClient(RaygunSettings settings, HttpClient httpClient) : base(setti public RaygunClient(RaygunSettings settings, IRaygunUserProvider userProvider) : base(settings, userProvider) { } + + public RaygunClient(RaygunSettings settings, IOfflineErrorStore offlineErrorStore) : base(settings, offlineErrorStore) + { + } public RaygunClient(RaygunSettings settings, HttpClient httpClient, IRaygunUserProvider userProvider) : base(settings, httpClient, userProvider) { } + public RaygunClient(RaygunSettings settings, HttpClient httpClient, IOfflineErrorStore offlineErrorStore) : base(settings, httpClient, null, offlineErrorStore) + { + } + public RaygunClient(RaygunSettings settings, HttpClient httpClient, IRaygunUserProvider userProvider, IOfflineErrorStore offlineErrorStore) : base(settings, httpClient, userProvider, offlineErrorStore) { } diff --git a/Mindscape.Raygun4Net.NetCore/Storage/FileSystemErrorStore.cs b/Mindscape.Raygun4Net.NetCore/Storage/FileSystemErrorStore.cs index 56d28781..1c504adb 100644 --- a/Mindscape.Raygun4Net.NetCore/Storage/FileSystemErrorStore.cs +++ b/Mindscape.Raygun4Net.NetCore/Storage/FileSystemErrorStore.cs @@ -27,7 +27,7 @@ public virtual async Task> GetAll(CancellationToken can { try { - using var fileStream = new FileStream(crashFile, FileMode.Open); + using var fileStream = new FileStream(crashFile, FileMode.Open, FileAccess.Read); using var gzipStream = new GZipStream(fileStream, CompressionMode.Decompress); using var reader = new StreamReader(gzipStream, Encoding.UTF8); diff --git a/Mindscape.Raygun4Net.NetCore/Storage/LocalApplicationDataStorage.cs b/Mindscape.Raygun4Net.NetCore/Storage/LocalApplicationDataStorage.cs index 629cdb96..24a32d8f 100644 --- a/Mindscape.Raygun4Net.NetCore/Storage/LocalApplicationDataStorage.cs +++ b/Mindscape.Raygun4Net.NetCore/Storage/LocalApplicationDataStorage.cs @@ -17,7 +17,7 @@ public LocalApplicationDataStorage(string uniqueApplicationId = null) { var uniqueId = uniqueApplicationId ?? Assembly.GetEntryAssembly()?.Location ?? throw new ApplicationException("Cannot determine unique id"); var uniqueIdHash = SHA1.Create().ComputeHash(Encoding.UTF8.GetBytes(uniqueId)); - var uniqueIdDirectory = Encoding.UTF8.GetString(uniqueIdHash); + var uniqueIdDirectory = BitConverter.ToString(uniqueIdHash).Replace("-", "").ToLowerInvariant(); var localAppDirectory = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), uniqueIdDirectory); _fileSystemErrorStorage = new FileSystemErrorStore(localAppDirectory); From 9f09a712b121d1776da0261e627717aebe55f3ca Mon Sep 17 00:00:00 2001 From: Sean Date: Wed, 15 May 2024 13:46:13 +1200 Subject: [PATCH 04/21] Bit of a rename and removal of sent reports working with location This needs a little more thinking --- .../RaygunClientBase.cs | 39 +++---- ...orSender.cs => CachedCrashReportSender.cs} | 45 +++++-- .../Storage/CrashReportCacheEntry.cs | 43 +++++++ .../Storage/ICrashReportCache.cs | 13 +++ .../Storage/IOfflineErrorStore.cs | 13 --- .../Storage/OfflineErrorRecord.cs | 21 ---- Mindscape.Raygun4Net.NetCore/RaygunClient.cs | 6 +- .../Storage/FileSystemCrashReportCache.cs | 110 ++++++++++++++++++ .../Storage/FileSystemErrorStore.cs | 98 ---------------- .../LocalApplicationDataCrashReportCache.cs | 49 ++++++++ .../Storage/LocalApplicationDataStorage.cs | 40 ------- 11 files changed, 268 insertions(+), 209 deletions(-) rename Mindscape.Raygun4Net.NetCore.Common/Storage/{BackgroundOfflineErrorSender.cs => CachedCrashReportSender.cs} (58%) create mode 100644 Mindscape.Raygun4Net.NetCore.Common/Storage/CrashReportCacheEntry.cs create mode 100644 Mindscape.Raygun4Net.NetCore.Common/Storage/ICrashReportCache.cs delete mode 100644 Mindscape.Raygun4Net.NetCore.Common/Storage/IOfflineErrorStore.cs delete mode 100644 Mindscape.Raygun4Net.NetCore.Common/Storage/OfflineErrorRecord.cs create mode 100644 Mindscape.Raygun4Net.NetCore/Storage/FileSystemCrashReportCache.cs delete mode 100644 Mindscape.Raygun4Net.NetCore/Storage/FileSystemErrorStore.cs create mode 100644 Mindscape.Raygun4Net.NetCore/Storage/LocalApplicationDataCrashReportCache.cs delete mode 100644 Mindscape.Raygun4Net.NetCore/Storage/LocalApplicationDataStorage.cs diff --git a/Mindscape.Raygun4Net.NetCore.Common/RaygunClientBase.cs b/Mindscape.Raygun4Net.NetCore.Common/RaygunClientBase.cs index 7069f789..f9edb8ca 100644 --- a/Mindscape.Raygun4Net.NetCore.Common/RaygunClientBase.cs +++ b/Mindscape.Raygun4Net.NetCore.Common/RaygunClientBase.cs @@ -48,7 +48,7 @@ public abstract class RaygunClientBase // ReSharper disable once PrivateFieldCanBeConvertedToLocalVariable private readonly UnhandledExceptionBridge.UnhandledExceptionHandler _onUnhandledExceptionDelegate; - private readonly IOfflineErrorStore _offlineErrorStore; + private readonly ICrashReportCache _crashReportCache; /// @@ -122,8 +122,8 @@ protected RaygunClientBase(RaygunSettingsBase settings, IRaygunUserProvider user { } - protected RaygunClientBase(RaygunSettingsBase settings, IOfflineErrorStore offlineErrorStore) - : this(settings, DefaultClient, offlineErrorStore) + protected RaygunClientBase(RaygunSettingsBase settings, ICrashReportCache crashReportCache) + : this(settings, DefaultClient, crashReportCache) { } @@ -133,12 +133,12 @@ protected RaygunClientBase(RaygunSettingsBase settings, HttpClient client, IRayg { } - protected RaygunClientBase(RaygunSettingsBase settings, HttpClient client, IOfflineErrorStore offlineErrorStore) - : this(settings, client, null, offlineErrorStore) + protected RaygunClientBase(RaygunSettingsBase settings, HttpClient client, ICrashReportCache crashReportCache) + : this(settings, client, null, crashReportCache) { } - protected RaygunClientBase(RaygunSettingsBase settings, HttpClient client, IRaygunUserProvider userProvider, IOfflineErrorStore offlineErrorStore) + protected RaygunClientBase(RaygunSettingsBase settings, HttpClient client, IRaygunUserProvider userProvider, ICrashReportCache crashReportCache) { _client = client ?? DefaultClient; _settings = settings; @@ -151,10 +151,10 @@ protected RaygunClientBase(RaygunSettingsBase settings, HttpClient client, IRayg UnhandledExceptionBridge.OnUnhandledException(_onUnhandledExceptionDelegate); - if (offlineErrorStore != null) + if (crashReportCache != null) { - _offlineErrorStore = offlineErrorStore; - BackgroundOfflineErrorReporter.SetSendCallback(SendPayloadAsync); + _crashReportCache = crashReportCache; + CachedCrashReportSender.SetSendCallback(SendPayloadAsync); } } @@ -458,8 +458,7 @@ protected async Task BuildMessage(Exception exception, return message; } - protected virtual async Task StripAndSend(Exception exception, IList tags, IDictionary userCustomData, - RaygunIdentifierMessage userInfo) + protected virtual async Task StripAndSend(Exception exception, IList tags, IDictionary userCustomData, RaygunIdentifierMessage userInfo) { foreach (var e in StripWrapperExceptions(exception)) { @@ -516,7 +515,7 @@ public Task Send(RaygunMessage raygunMessage) /// public async Task Send(RaygunMessage raygunMessage, CancellationToken cancellationToken) { - if (!ValidateApiKey(_settings.ApiKey)) + if (!ValidateApiKey()) { return; } @@ -560,7 +559,7 @@ internal async Task SendPayloadAsync(string payload, string apiKey, Cancellation if ((int)response.StatusCode >= 500) { // If we got a server error then add it to offline storage to send later - hasMessageBeenStored = await SaveMessageToOfflineStore(payload, apiKey, cancellationToken); + hasMessageBeenStored = await SaveMessageToOfflineCache(payload, apiKey, cancellationToken); } // Cause an exception to be bubbled up the stack @@ -574,7 +573,7 @@ internal async Task SendPayloadAsync(string payload, string apiKey, Cancellation if (!hasMessageBeenStored) { // Do our best to save it in the offline storage if it hasn't been saved yet - await SaveMessageToOfflineStore(payload, apiKey, cancellationToken); + await SaveMessageToOfflineCache(payload, apiKey, cancellationToken); } throw; @@ -582,20 +581,16 @@ internal async Task SendPayloadAsync(string payload, string apiKey, Cancellation } - private async Task SaveMessageToOfflineStore(string messagePayload, string apiKey, CancellationToken cancellationToken) + private async Task SaveMessageToOfflineCache(string messagePayload, string apiKey, CancellationToken cancellationToken) { // Can't store it anywhere - if (_offlineErrorStore is null) + if (_crashReportCache is null) { return false; } - return await _offlineErrorStore.Save(new OfflineErrorRecord - { - Id = Guid.NewGuid(), - MessagePayload = messagePayload, - ApiKey = apiKey - }, cancellationToken); + var cacheEntry = await _crashReportCache.Save(messagePayload, apiKey, cancellationToken); + return cacheEntry != null; } } } \ No newline at end of file diff --git a/Mindscape.Raygun4Net.NetCore.Common/Storage/BackgroundOfflineErrorSender.cs b/Mindscape.Raygun4Net.NetCore.Common/Storage/CachedCrashReportSender.cs similarity index 58% rename from Mindscape.Raygun4Net.NetCore.Common/Storage/BackgroundOfflineErrorSender.cs rename to Mindscape.Raygun4Net.NetCore.Common/Storage/CachedCrashReportSender.cs index dff81e3d..edb37079 100644 --- a/Mindscape.Raygun4Net.NetCore.Common/Storage/BackgroundOfflineErrorSender.cs +++ b/Mindscape.Raygun4Net.NetCore.Common/Storage/CachedCrashReportSender.cs @@ -5,7 +5,7 @@ namespace Mindscape.Raygun4Net.Storage; -public static class BackgroundOfflineErrorReporter +public static class CachedCrashReportSender { internal delegate Task SendHandler(string messagePayload, string apiKey, CancellationToken cancellationToken); @@ -13,7 +13,7 @@ public static class BackgroundOfflineErrorReporter private static volatile bool _isRunning; private static TimeSpan _interval = TimeSpan.FromSeconds(30); private static SendHandler _sendHandler; - private static Func _offlineErrorStore; + private static Func _crashReportCache; public static TimeSpan Interval { @@ -23,20 +23,20 @@ public static TimeSpan Interval _interval = value; // Set the new interval on the timer - BackgroundTimer.Change(TimeSpan.Zero, Interval); + BackgroundTimer.Change(Interval, TimeSpan.FromMilliseconds(int.MaxValue)); } } public static bool IsRunning => _isRunning; - static BackgroundOfflineErrorReporter() + static CachedCrashReportSender() { Start(); } - public static void SetErrorStore(Func offlineStoreFunc) + public static void SetErrorStore(Func offlineStoreFunc) { - _offlineErrorStore = offlineStoreFunc; + _crashReportCache = offlineStoreFunc; } internal static void SetSendCallback(SendHandler sendHandler) @@ -46,7 +46,20 @@ internal static void SetSendCallback(SendHandler sendHandler) private static async void SendOfflineErrors(object state) { - var store = _offlineErrorStore?.Invoke(); + try + { + await SendCachedErrors(); + } + finally + { + // Always restart the timer + BackgroundTimer.Change(Interval, TimeSpan.FromMilliseconds(int.MaxValue)); + } + } + + private static async Task SendCachedErrors() + { + var store = _crashReportCache?.Invoke(); // We don't have a store set, or a send handler - so we can't actually do anything if (store is null || _sendHandler is null) @@ -54,11 +67,19 @@ private static async void SendOfflineErrors(object state) try { - var errors = await store.GetAll(CancellationToken.None); - foreach (var error in errors) + var cachedCrashReports = await store.GetAll(CancellationToken.None); + foreach (var crashReport in cachedCrashReports) { - await _sendHandler(error.MessagePayload, error.ApiKey, CancellationToken.None); - await store.Remove(error.Id, CancellationToken.None); + try + { + await _sendHandler(crashReport.MessagePayload, crashReport.ApiKey, CancellationToken.None); + await store.Remove(crashReport, CancellationToken.None); + } + catch (Exception ex) + { + Debug.WriteLine($"Exception sending offline error [{crashReport.Id}]: {ex}"); + throw; + } } } catch (Exception ex) @@ -73,7 +94,7 @@ private static async void SendOfflineErrors(object state) /// public static void Start() { - BackgroundTimer.Change(TimeSpan.Zero, Interval); + BackgroundTimer.Change(Interval, TimeSpan.FromMilliseconds(int.MaxValue)); _isRunning = true; } diff --git a/Mindscape.Raygun4Net.NetCore.Common/Storage/CrashReportCacheEntry.cs b/Mindscape.Raygun4Net.NetCore.Common/Storage/CrashReportCacheEntry.cs new file mode 100644 index 00000000..bbdd9f91 --- /dev/null +++ b/Mindscape.Raygun4Net.NetCore.Common/Storage/CrashReportCacheEntry.cs @@ -0,0 +1,43 @@ +using System; +using System.Runtime.Serialization; + +namespace Mindscape.Raygun4Net.Storage; + +public class CrashReportCacheEntry +{ + /// + /// Unique ID for the record + /// + public Guid Id { get; } + + /// + /// The application api key that the payload was intended for + /// + public string ApiKey { get; } + + public string Payload { get; } + + /// + /// The JSON serialized payload of the error + /// + public string MessagePayload { get; } + + /// + /// The location of the cache entry - most likely the path on disk + /// + [IgnoreDataMember] + public string Location { get; set; } + + public CrashReportCacheEntry(string apiKey, string payload, string location = null) + : this(Guid.NewGuid(), apiKey, payload, location) + { + } + + public CrashReportCacheEntry(Guid id, string apiKey, string payload, string location = null) + { + Id = id; + ApiKey = apiKey; + Payload = payload; + Location = location; + } +} \ No newline at end of file diff --git a/Mindscape.Raygun4Net.NetCore.Common/Storage/ICrashReportCache.cs b/Mindscape.Raygun4Net.NetCore.Common/Storage/ICrashReportCache.cs new file mode 100644 index 00000000..80409d1e --- /dev/null +++ b/Mindscape.Raygun4Net.NetCore.Common/Storage/ICrashReportCache.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace Mindscape.Raygun4Net.Storage; + +public interface ICrashReportCache +{ + public Task> GetAll(CancellationToken cancellationToken); + public Task Save(string crashPayload, string apiKey, CancellationToken cancellationToken); + public Task Remove(CrashReportCacheEntry cacheEntry, CancellationToken cancellationToken); +} \ No newline at end of file diff --git a/Mindscape.Raygun4Net.NetCore.Common/Storage/IOfflineErrorStore.cs b/Mindscape.Raygun4Net.NetCore.Common/Storage/IOfflineErrorStore.cs deleted file mode 100644 index ca2e37c8..00000000 --- a/Mindscape.Raygun4Net.NetCore.Common/Storage/IOfflineErrorStore.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; - -namespace Mindscape.Raygun4Net.Storage; - -public interface IOfflineErrorStore -{ - public Task> GetAll(CancellationToken cancellationToken); - public Task Save(OfflineErrorRecord errorRecord, CancellationToken cancellationToken); - public Task Remove(Guid errorId, CancellationToken cancellationToken); -} \ No newline at end of file diff --git a/Mindscape.Raygun4Net.NetCore.Common/Storage/OfflineErrorRecord.cs b/Mindscape.Raygun4Net.NetCore.Common/Storage/OfflineErrorRecord.cs deleted file mode 100644 index f2e26606..00000000 --- a/Mindscape.Raygun4Net.NetCore.Common/Storage/OfflineErrorRecord.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System; - -namespace Mindscape.Raygun4Net.Storage; - -public sealed class OfflineErrorRecord -{ - /// - /// Unique ID for the record - /// - public Guid Id { get; set; } = Guid.NewGuid(); - - /// - /// The application api key that the payload was intended for - /// - public string ApiKey { get; set; } - - /// - /// The JSON serialized payload of the error - /// - public string MessagePayload { get; set; } -} \ No newline at end of file diff --git a/Mindscape.Raygun4Net.NetCore/RaygunClient.cs b/Mindscape.Raygun4Net.NetCore/RaygunClient.cs index 905766ac..2387eb5e 100644 --- a/Mindscape.Raygun4Net.NetCore/RaygunClient.cs +++ b/Mindscape.Raygun4Net.NetCore/RaygunClient.cs @@ -31,7 +31,7 @@ public RaygunClient(RaygunSettings settings, IRaygunUserProvider userProvider) : { } - public RaygunClient(RaygunSettings settings, IOfflineErrorStore offlineErrorStore) : base(settings, offlineErrorStore) + public RaygunClient(RaygunSettings settings, ICrashReportCache crashReportCache) : base(settings, crashReportCache) { } @@ -39,11 +39,11 @@ public RaygunClient(RaygunSettings settings, HttpClient httpClient, IRaygunUserP { } - public RaygunClient(RaygunSettings settings, HttpClient httpClient, IOfflineErrorStore offlineErrorStore) : base(settings, httpClient, null, offlineErrorStore) + public RaygunClient(RaygunSettings settings, HttpClient httpClient, ICrashReportCache crashReportCache) : base(settings, httpClient, null, crashReportCache) { } - public RaygunClient(RaygunSettings settings, HttpClient httpClient, IRaygunUserProvider userProvider, IOfflineErrorStore offlineErrorStore) : base(settings, httpClient, userProvider, offlineErrorStore) + public RaygunClient(RaygunSettings settings, HttpClient httpClient, IRaygunUserProvider userProvider, ICrashReportCache crashReportCache) : base(settings, httpClient, userProvider, crashReportCache) { } diff --git a/Mindscape.Raygun4Net.NetCore/Storage/FileSystemCrashReportCache.cs b/Mindscape.Raygun4Net.NetCore/Storage/FileSystemCrashReportCache.cs new file mode 100644 index 00000000..1f30face --- /dev/null +++ b/Mindscape.Raygun4Net.NetCore/Storage/FileSystemCrashReportCache.cs @@ -0,0 +1,110 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.IO.Compression; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace Mindscape.Raygun4Net.Storage; + +public class FileSystemCrashReportCache : ICrashReportCache +{ + private const string CacheFileExtension = "rgcrash"; + private readonly string _storageDirectory; + + public FileSystemCrashReportCache(string storageDirectory) + { + _storageDirectory = storageDirectory; + } + + public virtual async Task> GetAll(CancellationToken cancellationToken) + { + var crashFiles = Directory.GetFiles(_storageDirectory, $"*.{CacheFileExtension}"); + var errorRecords = new List(); + + foreach (var crashFile in crashFiles) + { + try + { + using var fileStream = new FileStream(crashFile, FileMode.Open, FileAccess.Read); + using var gzipStream = new GZipStream(fileStream, CompressionMode.Decompress); + using var reader = new StreamReader(gzipStream, Encoding.UTF8); + + Trace.WriteLine($"Attempting to load offline crash at {crashFile}"); + var jsonString = await reader.ReadToEndAsync(); + var errorRecord = SimpleJson.DeserializeObject(jsonString); + errorRecord.Location = crashFile; + + errorRecords.Add(errorRecord); + } + catch (Exception ex) + { + Debug.WriteLine("Error deserializing offline crash: {0}", ex.ToString()); + } + } + + return errorRecords; + } + + public virtual async Task Save(string payload, string apiKey, + CancellationToken cancellationToken) + { + var cacheEntryId = Guid.NewGuid(); + try + { + Directory.CreateDirectory(_storageDirectory); + + var cacheEntry = new CrashReportCacheEntry(cacheEntryId, apiKey, payload); + var filePath = GetFilePathForCacheEntry(cacheEntryId); + var jsonContent = SimpleJson.SerializeObject(cacheEntry); + + using var fileStream = new FileStream(filePath, FileMode.Create); + using var gzipStream = new GZipStream(fileStream, CompressionLevel.Optimal); + using var writer = new StreamWriter(gzipStream, Encoding.UTF8); + + Trace.WriteLine($"Saving crash {cacheEntry.Id} to {filePath}"); + await writer.WriteAsync(jsonContent); + await writer.FlushAsync(); + + return cacheEntry; + } + catch (Exception ex) + { + Debug.WriteLine($"Error adding crash [{cacheEntryId}] to store: {ex}"); + return null; + } + } + + public virtual Task Remove(CrashReportCacheEntry cacheEntry, CancellationToken cancellationToken) + { + try + { + var result = RemoveFile(cacheEntry.Location); + return Task.FromResult(result); + } + catch (Exception ex) + { + Debug.WriteLine($"Error remove crash [{cacheEntry.Id}] from store: {ex}"); + } + + return Task.FromResult(false); + } + + private static bool RemoveFile(string filePath) + { + if (!File.Exists(filePath)) + { + return false; + } + + File.Delete(filePath); + return true; + } + + private string GetFilePathForCacheEntry(Guid cacheId) + { + return Path.Combine(_storageDirectory, $"{cacheId:N}.{CacheFileExtension}"); + } +} \ No newline at end of file diff --git a/Mindscape.Raygun4Net.NetCore/Storage/FileSystemErrorStore.cs b/Mindscape.Raygun4Net.NetCore/Storage/FileSystemErrorStore.cs deleted file mode 100644 index 1c504adb..00000000 --- a/Mindscape.Raygun4Net.NetCore/Storage/FileSystemErrorStore.cs +++ /dev/null @@ -1,98 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.IO; -using System.IO.Compression; -using System.Text; -using System.Threading; -using System.Threading.Tasks; - -namespace Mindscape.Raygun4Net.Storage; - -public class FileSystemErrorStore : IOfflineErrorStore -{ - private readonly string _storageDirectory; - - public FileSystemErrorStore(string storageDirectory) - { - _storageDirectory = storageDirectory; - } - - public virtual async Task> GetAll(CancellationToken cancellationToken) - { - var crashFiles = Directory.GetFiles(_storageDirectory, "*.crash"); - var errorRecords = new List(); - - foreach (var crashFile in crashFiles) - { - try - { - using var fileStream = new FileStream(crashFile, FileMode.Open, FileAccess.Read); - using var gzipStream = new GZipStream(fileStream, CompressionMode.Decompress); - using var reader = new StreamReader(gzipStream, Encoding.UTF8); - - Trace.WriteLine($"Attempting to load offline crash at {crashFile}"); - var jsonString = await reader.ReadToEndAsync(); - var errorRecord = SimpleJson.DeserializeObject(jsonString); - - errorRecords.Add(errorRecord); - } - catch (Exception ex) - { - Debug.WriteLine("Error deserializing offline crash: {0}", ex.ToString()); - } - } - - return errorRecords; - } - - public virtual async Task Save(OfflineErrorRecord errorRecord, CancellationToken cancellationToken) - { - try - { - Directory.CreateDirectory(_storageDirectory); - - var filePath = GetFilePath(errorRecord.Id); - var jsonContent = SimpleJson.SerializeObject(errorRecord); - - using var fileStream = new FileStream(filePath, FileMode.Create); - using var gzipStream = new GZipStream(fileStream, CompressionLevel.Optimal); - using var writer = new StreamWriter(gzipStream, Encoding.UTF8); - - Trace.WriteLine($"Saving crash {errorRecord.Id} to {filePath}"); - await writer.WriteAsync(jsonContent); - await writer.FlushAsync(); - - return true; - } - catch (Exception ex) - { - Debug.WriteLine($"Error adding crash [{errorRecord.Id}] to store: {ex}"); - return false; - } - } - - public virtual Task Remove(Guid errorId, CancellationToken cancellationToken) - { - try - { - var filePath = GetFilePath(errorId); - if (File.Exists(filePath)) - { - File.Delete(filePath); - return Task.FromResult(true); - } - } - catch (Exception ex) - { - Debug.WriteLine($"Error remove crash [{errorId}] from store: {ex}"); - } - - return Task.FromResult(false); - } - - private string GetFilePath(Guid errorId) - { - return Path.Combine(_storageDirectory, $"raygun-cr-{errorId}.crash"); - } -} \ No newline at end of file diff --git a/Mindscape.Raygun4Net.NetCore/Storage/LocalApplicationDataCrashReportCache.cs b/Mindscape.Raygun4Net.NetCore/Storage/LocalApplicationDataCrashReportCache.cs new file mode 100644 index 00000000..0360846c --- /dev/null +++ b/Mindscape.Raygun4Net.NetCore/Storage/LocalApplicationDataCrashReportCache.cs @@ -0,0 +1,49 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Reflection; +using System.Security.Cryptography; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace Mindscape.Raygun4Net.Storage; + +/// +/// Stores a cached copy of crash reports that failed to send in Local App Data +/// Creates a directory if specified, otherwise creates a unique directory based off the location of the application +/// +public sealed class LocalApplicationDataCrashReportCache : ICrashReportCache +{ + private readonly FileSystemCrashReportCache _fileSystemErrorStorage; + + public LocalApplicationDataCrashReportCache(string directoryName = null) + { + if (directoryName is null) + { + // Try generate a unique id, from the executable location + var uniqueId = Assembly.GetEntryAssembly()?.Location ?? throw new ApplicationException("Cannot determine unique application id"); + + var uniqueIdHash = SHA1.Create().ComputeHash(Encoding.UTF8.GetBytes(uniqueId)); + directoryName = BitConverter.ToString(uniqueIdHash).Replace("-", "").ToLowerInvariant(); + } + + var localAppDirectory = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), directoryName); + _fileSystemErrorStorage = new FileSystemCrashReportCache(localAppDirectory); + } + + public Task> GetAll(CancellationToken cancellationToken) + { + return _fileSystemErrorStorage.GetAll(cancellationToken); + } + + public Task Save(string crashPayload, string apiKey, CancellationToken cancellationToken) + { + return _fileSystemErrorStorage.Save(crashPayload, apiKey, cancellationToken); + } + + public Task Remove(CrashReportCacheEntry cacheEntry, CancellationToken cancellationToken) + { + return _fileSystemErrorStorage.Remove(cacheEntry, cancellationToken); + } +} \ No newline at end of file diff --git a/Mindscape.Raygun4Net.NetCore/Storage/LocalApplicationDataStorage.cs b/Mindscape.Raygun4Net.NetCore/Storage/LocalApplicationDataStorage.cs deleted file mode 100644 index 24a32d8f..00000000 --- a/Mindscape.Raygun4Net.NetCore/Storage/LocalApplicationDataStorage.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Reflection; -using System.Security.Cryptography; -using System.Text; -using System.Threading; -using System.Threading.Tasks; - -namespace Mindscape.Raygun4Net.Storage; - -public sealed class LocalApplicationDataStorage : IOfflineErrorStore -{ - private readonly FileSystemErrorStore _fileSystemErrorStorage; - - public LocalApplicationDataStorage(string uniqueApplicationId = null) - { - var uniqueId = uniqueApplicationId ?? Assembly.GetEntryAssembly()?.Location ?? throw new ApplicationException("Cannot determine unique id"); - var uniqueIdHash = SHA1.Create().ComputeHash(Encoding.UTF8.GetBytes(uniqueId)); - var uniqueIdDirectory = BitConverter.ToString(uniqueIdHash).Replace("-", "").ToLowerInvariant(); - - var localAppDirectory = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), uniqueIdDirectory); - _fileSystemErrorStorage = new FileSystemErrorStore(localAppDirectory); - } - - public Task> GetAll(CancellationToken cancellationToken) - { - return _fileSystemErrorStorage.GetAll(cancellationToken); - } - - public Task Save(OfflineErrorRecord errorRecord, CancellationToken cancellationToken) - { - return _fileSystemErrorStorage.Save(errorRecord, cancellationToken); - } - - public Task Remove(Guid errorId, CancellationToken cancellationToken) - { - return _fileSystemErrorStorage.Remove(errorId, cancellationToken); - } -} \ No newline at end of file From d0b6f7ff117b4f8c97c20a91de1aee77fd2d54b9 Mon Sep 17 00:00:00 2001 From: Sean Date: Wed, 15 May 2024 14:22:02 +1200 Subject: [PATCH 05/21] Store a map of locations for removing cache entries Mark failed files --- .../RaygunClientBase.cs | 2 +- ...s => CachedCrashReportBackgroundWorker.cs} | 6 ++-- .../Storage/CrashReportCacheEntry.cs | 29 +++---------------- .../Storage/ICrashReportCache.cs | 2 +- .../Storage/FileSystemCrashReportCache.cs | 24 +++++++++++---- .../LocalApplicationDataCrashReportCache.cs | 4 +-- 6 files changed, 29 insertions(+), 38 deletions(-) rename Mindscape.Raygun4Net.NetCore.Common/Storage/{CachedCrashReportSender.cs => CachedCrashReportBackgroundWorker.cs} (94%) diff --git a/Mindscape.Raygun4Net.NetCore.Common/RaygunClientBase.cs b/Mindscape.Raygun4Net.NetCore.Common/RaygunClientBase.cs index f9edb8ca..019b6c1a 100644 --- a/Mindscape.Raygun4Net.NetCore.Common/RaygunClientBase.cs +++ b/Mindscape.Raygun4Net.NetCore.Common/RaygunClientBase.cs @@ -154,7 +154,7 @@ protected RaygunClientBase(RaygunSettingsBase settings, HttpClient client, IRayg if (crashReportCache != null) { _crashReportCache = crashReportCache; - CachedCrashReportSender.SetSendCallback(SendPayloadAsync); + CachedCrashReportBackgroundWorker.SetSendCallback(SendPayloadAsync); } } diff --git a/Mindscape.Raygun4Net.NetCore.Common/Storage/CachedCrashReportSender.cs b/Mindscape.Raygun4Net.NetCore.Common/Storage/CachedCrashReportBackgroundWorker.cs similarity index 94% rename from Mindscape.Raygun4Net.NetCore.Common/Storage/CachedCrashReportSender.cs rename to Mindscape.Raygun4Net.NetCore.Common/Storage/CachedCrashReportBackgroundWorker.cs index edb37079..067c8d55 100644 --- a/Mindscape.Raygun4Net.NetCore.Common/Storage/CachedCrashReportSender.cs +++ b/Mindscape.Raygun4Net.NetCore.Common/Storage/CachedCrashReportBackgroundWorker.cs @@ -5,7 +5,7 @@ namespace Mindscape.Raygun4Net.Storage; -public static class CachedCrashReportSender +public static class CachedCrashReportBackgroundWorker { internal delegate Task SendHandler(string messagePayload, string apiKey, CancellationToken cancellationToken); @@ -29,7 +29,7 @@ public static TimeSpan Interval public static bool IsRunning => _isRunning; - static CachedCrashReportSender() + static CachedCrashReportBackgroundWorker() { Start(); } @@ -73,7 +73,7 @@ private static async Task SendCachedErrors() try { await _sendHandler(crashReport.MessagePayload, crashReport.ApiKey, CancellationToken.None); - await store.Remove(crashReport, CancellationToken.None); + await store.Remove(crashReport.Id, CancellationToken.None); } catch (Exception ex) { diff --git a/Mindscape.Raygun4Net.NetCore.Common/Storage/CrashReportCacheEntry.cs b/Mindscape.Raygun4Net.NetCore.Common/Storage/CrashReportCacheEntry.cs index bbdd9f91..2d1b3373 100644 --- a/Mindscape.Raygun4Net.NetCore.Common/Storage/CrashReportCacheEntry.cs +++ b/Mindscape.Raygun4Net.NetCore.Common/Storage/CrashReportCacheEntry.cs @@ -3,41 +3,20 @@ namespace Mindscape.Raygun4Net.Storage; -public class CrashReportCacheEntry +public sealed class CrashReportCacheEntry { /// /// Unique ID for the record /// - public Guid Id { get; } + public Guid Id { get; set; } /// /// The application api key that the payload was intended for /// - public string ApiKey { get; } - - public string Payload { get; } + public string ApiKey { get; set; } /// /// The JSON serialized payload of the error /// - public string MessagePayload { get; } - - /// - /// The location of the cache entry - most likely the path on disk - /// - [IgnoreDataMember] - public string Location { get; set; } - - public CrashReportCacheEntry(string apiKey, string payload, string location = null) - : this(Guid.NewGuid(), apiKey, payload, location) - { - } - - public CrashReportCacheEntry(Guid id, string apiKey, string payload, string location = null) - { - Id = id; - ApiKey = apiKey; - Payload = payload; - Location = location; - } + public string MessagePayload { get; set; } } \ No newline at end of file diff --git a/Mindscape.Raygun4Net.NetCore.Common/Storage/ICrashReportCache.cs b/Mindscape.Raygun4Net.NetCore.Common/Storage/ICrashReportCache.cs index 80409d1e..f0912198 100644 --- a/Mindscape.Raygun4Net.NetCore.Common/Storage/ICrashReportCache.cs +++ b/Mindscape.Raygun4Net.NetCore.Common/Storage/ICrashReportCache.cs @@ -9,5 +9,5 @@ public interface ICrashReportCache { public Task> GetAll(CancellationToken cancellationToken); public Task Save(string crashPayload, string apiKey, CancellationToken cancellationToken); - public Task Remove(CrashReportCacheEntry cacheEntry, CancellationToken cancellationToken); + public Task Remove(Guid cacheEntryId, CancellationToken cancellationToken); } \ No newline at end of file diff --git a/Mindscape.Raygun4Net.NetCore/Storage/FileSystemCrashReportCache.cs b/Mindscape.Raygun4Net.NetCore/Storage/FileSystemCrashReportCache.cs index 1f30face..4693b0d4 100644 --- a/Mindscape.Raygun4Net.NetCore/Storage/FileSystemCrashReportCache.cs +++ b/Mindscape.Raygun4Net.NetCore/Storage/FileSystemCrashReportCache.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.IO; @@ -13,6 +14,7 @@ public class FileSystemCrashReportCache : ICrashReportCache { private const string CacheFileExtension = "rgcrash"; private readonly string _storageDirectory; + private readonly ConcurrentDictionary _cacheLocationMap = new(); public FileSystemCrashReportCache(string storageDirectory) { @@ -35,13 +37,14 @@ public virtual async Task> GetAll(CancellationToken Trace.WriteLine($"Attempting to load offline crash at {crashFile}"); var jsonString = await reader.ReadToEndAsync(); var errorRecord = SimpleJson.DeserializeObject(jsonString); - errorRecord.Location = crashFile; errorRecords.Add(errorRecord); + _cacheLocationMap[errorRecord.Id] = crashFile; } catch (Exception ex) { Debug.WriteLine("Error deserializing offline crash: {0}", ex.ToString()); + File.Move(crashFile, $"{crashFile}.failed"); } } @@ -56,7 +59,12 @@ public virtual async Task Save(string payload, string api { Directory.CreateDirectory(_storageDirectory); - var cacheEntry = new CrashReportCacheEntry(cacheEntryId, apiKey, payload); + var cacheEntry = new CrashReportCacheEntry + { + Id = cacheEntryId, + ApiKey = apiKey, + MessagePayload = payload + }; var filePath = GetFilePathForCacheEntry(cacheEntryId); var jsonContent = SimpleJson.SerializeObject(cacheEntry); @@ -77,16 +85,20 @@ public virtual async Task Save(string payload, string api } } - public virtual Task Remove(CrashReportCacheEntry cacheEntry, CancellationToken cancellationToken) + public virtual Task Remove(Guid cacheId, CancellationToken cancellationToken) { try { - var result = RemoveFile(cacheEntry.Location); - return Task.FromResult(result); + if (_cacheLocationMap.TryGetValue(cacheId, out var filePath)) + { + var result = RemoveFile(filePath); + _cacheLocationMap.TryRemove(cacheId, out _); + return Task.FromResult(result); + } } catch (Exception ex) { - Debug.WriteLine($"Error remove crash [{cacheEntry.Id}] from store: {ex}"); + Debug.WriteLine($"Error remove crash [{cacheId}] from store: {ex}"); } return Task.FromResult(false); diff --git a/Mindscape.Raygun4Net.NetCore/Storage/LocalApplicationDataCrashReportCache.cs b/Mindscape.Raygun4Net.NetCore/Storage/LocalApplicationDataCrashReportCache.cs index 0360846c..afac8de5 100644 --- a/Mindscape.Raygun4Net.NetCore/Storage/LocalApplicationDataCrashReportCache.cs +++ b/Mindscape.Raygun4Net.NetCore/Storage/LocalApplicationDataCrashReportCache.cs @@ -42,8 +42,8 @@ public Task Save(string crashPayload, string apiKey, Canc return _fileSystemErrorStorage.Save(crashPayload, apiKey, cancellationToken); } - public Task Remove(CrashReportCacheEntry cacheEntry, CancellationToken cancellationToken) + public Task Remove(Guid cacheId, CancellationToken cancellationToken) { - return _fileSystemErrorStorage.Remove(cacheEntry, cancellationToken); + return _fileSystemErrorStorage.Remove(cacheId, cancellationToken); } } \ No newline at end of file From e1b697e3c631e647e62112ec785026005076ede8 Mon Sep 17 00:00:00 2001 From: Sean Date: Wed, 15 May 2024 14:27:21 +1200 Subject: [PATCH 06/21] Fix tests --- .../Model/FakeRaygunClient.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Mindscape.Raygun4Net.NetCore.Tests/Model/FakeRaygunClient.cs b/Mindscape.Raygun4Net.NetCore.Tests/Model/FakeRaygunClient.cs index 801d1b4e..5a836649 100644 --- a/Mindscape.Raygun4Net.NetCore.Tests/Model/FakeRaygunClient.cs +++ b/Mindscape.Raygun4Net.NetCore.Tests/Model/FakeRaygunClient.cs @@ -7,15 +7,15 @@ namespace Mindscape.Raygun4Net.NetCore.Tests { public class FakeRaygunClient : RaygunClient { - public FakeRaygunClient() : base(new RaygunSettings { ApiKey = string.Empty}, null, null) + public FakeRaygunClient() : base(new RaygunSettings { ApiKey = string.Empty}, null, null, null) { } - public FakeRaygunClient(string apiKey) : base(new RaygunSettings { ApiKey = apiKey}, null, null) + public FakeRaygunClient(string apiKey) : base(new RaygunSettings { ApiKey = apiKey}, null, null, null) { } - public FakeRaygunClient(RaygunSettings settings) : base(settings, null, null) + public FakeRaygunClient(RaygunSettings settings) : base(settings, null, null, null) { } From 781916a074fab6d1103ca8df2b7f1d25e806c63c Mon Sep 17 00:00:00 2001 From: Sean Date: Wed, 15 May 2024 15:21:16 +1200 Subject: [PATCH 07/21] naming --- .../Storage/CachedCrashReportBackgroundWorker.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Mindscape.Raygun4Net.NetCore.Common/Storage/CachedCrashReportBackgroundWorker.cs b/Mindscape.Raygun4Net.NetCore.Common/Storage/CachedCrashReportBackgroundWorker.cs index 067c8d55..4b9ca27f 100644 --- a/Mindscape.Raygun4Net.NetCore.Common/Storage/CachedCrashReportBackgroundWorker.cs +++ b/Mindscape.Raygun4Net.NetCore.Common/Storage/CachedCrashReportBackgroundWorker.cs @@ -34,7 +34,7 @@ static CachedCrashReportBackgroundWorker() Start(); } - public static void SetErrorStore(Func offlineStoreFunc) + public static void SetCrashReportCache(Func offlineStoreFunc) { _crashReportCache = offlineStoreFunc; } From 0e6ddd1f5d920a686fb959a863912d4ce291fdc1 Mon Sep 17 00:00:00 2001 From: Sean Date: Wed, 29 May 2024 11:10:29 +1200 Subject: [PATCH 08/21] No cast! --- Mindscape.Raygun4Net.NetCore.Common/RaygunClientBase.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Mindscape.Raygun4Net.NetCore.Common/RaygunClientBase.cs b/Mindscape.Raygun4Net.NetCore.Common/RaygunClientBase.cs index 019b6c1a..f851cfe0 100644 --- a/Mindscape.Raygun4Net.NetCore.Common/RaygunClientBase.cs +++ b/Mindscape.Raygun4Net.NetCore.Common/RaygunClientBase.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; +using System.Net; using System.Net.Http; using System.Reflection; using System.Text; @@ -556,7 +557,7 @@ internal async Task SendPayloadAsync(string payload, string apiKey, Cancellation { Debug.WriteLine($"Error Logging Exception to Raygun {response.ReasonPhrase}"); - if ((int)response.StatusCode >= 500) + if (response.StatusCode >= HttpStatusCode.InternalServerError) { // If we got a server error then add it to offline storage to send later hasMessageBeenStored = await SaveMessageToOfflineCache(payload, apiKey, cancellationToken); From f34d373d1e7d9a84ff4c7a592df928be77b6a434 Mon Sep 17 00:00:00 2001 From: Sean Date: Wed, 29 May 2024 11:12:33 +1200 Subject: [PATCH 09/21] Employ a finally block to clean up the logic --- .../RaygunClientBase.cs | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/Mindscape.Raygun4Net.NetCore.Common/RaygunClientBase.cs b/Mindscape.Raygun4Net.NetCore.Common/RaygunClientBase.cs index f851cfe0..f84a4a27 100644 --- a/Mindscape.Raygun4Net.NetCore.Common/RaygunClientBase.cs +++ b/Mindscape.Raygun4Net.NetCore.Common/RaygunClientBase.cs @@ -544,7 +544,7 @@ public async Task Send(RaygunMessage raygunMessage, CancellationToken cancellati internal async Task SendPayloadAsync(string payload, string apiKey, CancellationToken cancellationToken) { - var hasMessageBeenStored = false; + var shouldStoreMessage = false; var requestMessage = new HttpRequestMessage(HttpMethod.Post, _settings.ApiEndpoint); requestMessage.Headers.Add("X-ApiKey", apiKey); requestMessage.Content = new StringContent(payload, Encoding.UTF8, "application/json"); @@ -560,7 +560,7 @@ internal async Task SendPayloadAsync(string payload, string apiKey, Cancellation if (response.StatusCode >= HttpStatusCode.InternalServerError) { // If we got a server error then add it to offline storage to send later - hasMessageBeenStored = await SaveMessageToOfflineCache(payload, apiKey, cancellationToken); + shouldStoreMessage = true; } // Cause an exception to be bubbled up the stack @@ -570,14 +570,16 @@ internal async Task SendPayloadAsync(string payload, string apiKey, Cancellation catch (Exception ex) { Debug.WriteLine($"Error Logging Exception to Raygun {ex.Message}"); + shouldStoreMessage = true; - if (!hasMessageBeenStored) + throw; + } + finally + { + if (shouldStoreMessage) { - // Do our best to save it in the offline storage if it hasn't been saved yet await SaveMessageToOfflineCache(payload, apiKey, cancellationToken); } - - throw; } } From 5b7358c39c94c85b0a6bbe54587c911531b27380 Mon Sep 17 00:00:00 2001 From: Sean Date: Wed, 29 May 2024 11:21:29 +1200 Subject: [PATCH 10/21] Move the crash store into a getter/setter property instead of the ctor --- .../RaygunClientBase.cs | 38 +++---- .../Model/FakeRaygunClient.cs | 98 +++++++++---------- Mindscape.Raygun4Net.NetCore/RaygunClient.cs | 12 --- 3 files changed, 62 insertions(+), 86 deletions(-) diff --git a/Mindscape.Raygun4Net.NetCore.Common/RaygunClientBase.cs b/Mindscape.Raygun4Net.NetCore.Common/RaygunClientBase.cs index f84a4a27..57a563c8 100644 --- a/Mindscape.Raygun4Net.NetCore.Common/RaygunClientBase.cs +++ b/Mindscape.Raygun4Net.NetCore.Common/RaygunClientBase.cs @@ -49,7 +49,7 @@ public abstract class RaygunClientBase // ReSharper disable once PrivateFieldCanBeConvertedToLocalVariable private readonly UnhandledExceptionBridge.UnhandledExceptionHandler _onUnhandledExceptionDelegate; - private readonly ICrashReportCache _crashReportCache; + private ICrashReportCache _crashReportCache; /// @@ -97,6 +97,16 @@ public virtual bool CatchUnhandledExceptions set => _settings.CatchUnhandledExceptions = value; } + public ICrashReportCache CrashReportCache + { + get => _crashReportCache; + set + { + _crashReportCache = value; + CachedCrashReportBackgroundWorker.SetSendCallback(SendPayloadAsync); + } + } + private void OnApplicationUnhandledException(Exception exception, bool isTerminating) { if (!_settings.CatchUnhandledExceptions) @@ -108,13 +118,13 @@ private void OnApplicationUnhandledException(Exception exception, bool isTermina } protected RaygunClientBase(RaygunSettingsBase settings) - : this(settings, DefaultClient, null, null) + : this(settings, DefaultClient, null) { } // ReSharper disable once IntroduceOptionalParameters.Global protected RaygunClientBase(RaygunSettingsBase settings, HttpClient client) - : this(settings, client, null, null) + : this(settings, client, null) { } @@ -123,23 +133,7 @@ protected RaygunClientBase(RaygunSettingsBase settings, IRaygunUserProvider user { } - protected RaygunClientBase(RaygunSettingsBase settings, ICrashReportCache crashReportCache) - : this(settings, DefaultClient, crashReportCache) - { - } - - // ReSharper disable once IntroduceOptionalParameters.Global protected RaygunClientBase(RaygunSettingsBase settings, HttpClient client, IRaygunUserProvider userProvider) - : this(settings, client, userProvider, null) - { - } - - protected RaygunClientBase(RaygunSettingsBase settings, HttpClient client, ICrashReportCache crashReportCache) - : this(settings, client, null, crashReportCache) - { - } - - protected RaygunClientBase(RaygunSettingsBase settings, HttpClient client, IRaygunUserProvider userProvider, ICrashReportCache crashReportCache) { _client = client ?? DefaultClient; _settings = settings; @@ -151,12 +145,6 @@ protected RaygunClientBase(RaygunSettingsBase settings, HttpClient client, IRayg _onUnhandledExceptionDelegate = OnApplicationUnhandledException; UnhandledExceptionBridge.OnUnhandledException(_onUnhandledExceptionDelegate); - - if (crashReportCache != null) - { - _crashReportCache = crashReportCache; - CachedCrashReportBackgroundWorker.SetSendCallback(SendPayloadAsync); - } } /// diff --git a/Mindscape.Raygun4Net.NetCore.Tests/Model/FakeRaygunClient.cs b/Mindscape.Raygun4Net.NetCore.Tests/Model/FakeRaygunClient.cs index 5a836649..cd2002ae 100644 --- a/Mindscape.Raygun4Net.NetCore.Tests/Model/FakeRaygunClient.cs +++ b/Mindscape.Raygun4Net.NetCore.Tests/Model/FakeRaygunClient.cs @@ -5,52 +5,52 @@ namespace Mindscape.Raygun4Net.NetCore.Tests { - public class FakeRaygunClient : RaygunClient - { - public FakeRaygunClient() : base(new RaygunSettings { ApiKey = string.Empty}, null, null, null) - { - } - - public FakeRaygunClient(string apiKey) : base(new RaygunSettings { ApiKey = apiKey}, null, null, null) - { - } - - public FakeRaygunClient(RaygunSettings settings) : base(settings, null, null, null) - { - } - - public RaygunMessage ExposeBuildMessage(Exception exception, [Optional] IList tags, [Optional] IDictionary userCustomData, [Optional] RaygunIdentifierMessage user) - { - var task = BuildMessage(exception, tags, userCustomData, user); - - task.Wait(); - - return task.Result; - } - - public bool ExposeValidateApiKey() - { - return ValidateApiKey(); - } - - public bool ExposeOnSendingMessage(RaygunMessage raygunMessage) - { - return OnSendingMessage(raygunMessage); - } - - public bool ExposeCanSend(Exception exception) - { - return CanSend(exception); - } - - public void ExposeFlagAsSent(Exception exception) - { - FlagAsSent(exception); - } - - public IEnumerable ExposeStripWrapperExceptions(Exception exception) - { - return StripWrapperExceptions(exception); - } - } -} + public class FakeRaygunClient : RaygunClient + { + public FakeRaygunClient() : base(new RaygunSettings { ApiKey = string.Empty }, null, null) + { + } + + public FakeRaygunClient(string apiKey) : base(new RaygunSettings { ApiKey = apiKey }, null, null) + { + } + + public FakeRaygunClient(RaygunSettings settings) : base(settings, null, null) + { + } + + public RaygunMessage ExposeBuildMessage(Exception exception, [Optional] IList tags, [Optional] IDictionary userCustomData, [Optional] RaygunIdentifierMessage user) + { + var task = BuildMessage(exception, tags, userCustomData, user); + + task.Wait(); + + return task.Result; + } + + public bool ExposeValidateApiKey() + { + return ValidateApiKey(); + } + + public bool ExposeOnSendingMessage(RaygunMessage raygunMessage) + { + return OnSendingMessage(raygunMessage); + } + + public bool ExposeCanSend(Exception exception) + { + return CanSend(exception); + } + + public void ExposeFlagAsSent(Exception exception) + { + FlagAsSent(exception); + } + + public IEnumerable ExposeStripWrapperExceptions(Exception exception) + { + return StripWrapperExceptions(exception); + } + } +} \ No newline at end of file diff --git a/Mindscape.Raygun4Net.NetCore/RaygunClient.cs b/Mindscape.Raygun4Net.NetCore/RaygunClient.cs index 2387eb5e..a9242cd3 100644 --- a/Mindscape.Raygun4Net.NetCore/RaygunClient.cs +++ b/Mindscape.Raygun4Net.NetCore/RaygunClient.cs @@ -30,23 +30,11 @@ public RaygunClient(RaygunSettings settings, HttpClient httpClient) : base(setti public RaygunClient(RaygunSettings settings, IRaygunUserProvider userProvider) : base(settings, userProvider) { } - - public RaygunClient(RaygunSettings settings, ICrashReportCache crashReportCache) : base(settings, crashReportCache) - { - } public RaygunClient(RaygunSettings settings, HttpClient httpClient, IRaygunUserProvider userProvider) : base(settings, httpClient, userProvider) { } - public RaygunClient(RaygunSettings settings, HttpClient httpClient, ICrashReportCache crashReportCache) : base(settings, httpClient, null, crashReportCache) - { - } - - public RaygunClient(RaygunSettings settings, HttpClient httpClient, IRaygunUserProvider userProvider, ICrashReportCache crashReportCache) : base(settings, httpClient, userProvider, crashReportCache) - { - } - // ReSharper restore MemberCanBeProtected.Global // ReSharper restore SuggestBaseTypeForParameterInConstructor // ReSharper restore UnusedMember.Global From 4c4dbda22010533e55d216e76883266b80ba3e8e Mon Sep 17 00:00:00 2001 From: Sean Date: Thu, 30 May 2024 07:21:29 +1200 Subject: [PATCH 11/21] Rename to store --- .../RaygunClientBase.cs | 12 ++++++------ .../Storage/CachedCrashReportBackgroundWorker.cs | 6 +++--- ...eportCacheEntry.cs => CrashReportStoreEntry.cs} | 2 +- .../{ICrashReportCache.cs => ICrashReportStore.cs} | 6 +++--- ...eportCache.cs => FileSystemCrashReportStore.cs} | 14 +++++++------- ....cs => LocalApplicationDataCrashReportStore.cs} | 12 ++++++------ 6 files changed, 26 insertions(+), 26 deletions(-) rename Mindscape.Raygun4Net.NetCore.Common/Storage/{CrashReportCacheEntry.cs => CrashReportStoreEntry.cs} (91%) rename Mindscape.Raygun4Net.NetCore.Common/Storage/{ICrashReportCache.cs => ICrashReportStore.cs} (65%) rename Mindscape.Raygun4Net.NetCore/Storage/{FileSystemCrashReportCache.cs => FileSystemCrashReportStore.cs} (89%) rename Mindscape.Raygun4Net.NetCore/Storage/{LocalApplicationDataCrashReportCache.cs => LocalApplicationDataCrashReportStore.cs} (78%) diff --git a/Mindscape.Raygun4Net.NetCore.Common/RaygunClientBase.cs b/Mindscape.Raygun4Net.NetCore.Common/RaygunClientBase.cs index 57a563c8..d46dc61b 100644 --- a/Mindscape.Raygun4Net.NetCore.Common/RaygunClientBase.cs +++ b/Mindscape.Raygun4Net.NetCore.Common/RaygunClientBase.cs @@ -49,7 +49,7 @@ public abstract class RaygunClientBase // ReSharper disable once PrivateFieldCanBeConvertedToLocalVariable private readonly UnhandledExceptionBridge.UnhandledExceptionHandler _onUnhandledExceptionDelegate; - private ICrashReportCache _crashReportCache; + private ICrashReportStore _crashReportStore; /// @@ -97,12 +97,12 @@ public virtual bool CatchUnhandledExceptions set => _settings.CatchUnhandledExceptions = value; } - public ICrashReportCache CrashReportCache + public ICrashReportStore CrashReportStore { - get => _crashReportCache; + get => _crashReportStore; set { - _crashReportCache = value; + _crashReportStore = value; CachedCrashReportBackgroundWorker.SetSendCallback(SendPayloadAsync); } } @@ -575,12 +575,12 @@ internal async Task SendPayloadAsync(string payload, string apiKey, Cancellation private async Task SaveMessageToOfflineCache(string messagePayload, string apiKey, CancellationToken cancellationToken) { // Can't store it anywhere - if (_crashReportCache is null) + if (_crashReportStore is null) { return false; } - var cacheEntry = await _crashReportCache.Save(messagePayload, apiKey, cancellationToken); + var cacheEntry = await _crashReportStore.Save(messagePayload, apiKey, cancellationToken); return cacheEntry != null; } } diff --git a/Mindscape.Raygun4Net.NetCore.Common/Storage/CachedCrashReportBackgroundWorker.cs b/Mindscape.Raygun4Net.NetCore.Common/Storage/CachedCrashReportBackgroundWorker.cs index 4b9ca27f..7fae15ba 100644 --- a/Mindscape.Raygun4Net.NetCore.Common/Storage/CachedCrashReportBackgroundWorker.cs +++ b/Mindscape.Raygun4Net.NetCore.Common/Storage/CachedCrashReportBackgroundWorker.cs @@ -13,11 +13,11 @@ public static class CachedCrashReportBackgroundWorker private static volatile bool _isRunning; private static TimeSpan _interval = TimeSpan.FromSeconds(30); private static SendHandler _sendHandler; - private static Func _crashReportCache; + private static Func _crashReportCache; public static TimeSpan Interval { - get { return _interval; } + get => _interval; set { _interval = value; @@ -34,7 +34,7 @@ static CachedCrashReportBackgroundWorker() Start(); } - public static void SetCrashReportCache(Func offlineStoreFunc) + public static void SetCrashReportCache(Func offlineStoreFunc) { _crashReportCache = offlineStoreFunc; } diff --git a/Mindscape.Raygun4Net.NetCore.Common/Storage/CrashReportCacheEntry.cs b/Mindscape.Raygun4Net.NetCore.Common/Storage/CrashReportStoreEntry.cs similarity index 91% rename from Mindscape.Raygun4Net.NetCore.Common/Storage/CrashReportCacheEntry.cs rename to Mindscape.Raygun4Net.NetCore.Common/Storage/CrashReportStoreEntry.cs index 2d1b3373..40f13a16 100644 --- a/Mindscape.Raygun4Net.NetCore.Common/Storage/CrashReportCacheEntry.cs +++ b/Mindscape.Raygun4Net.NetCore.Common/Storage/CrashReportStoreEntry.cs @@ -3,7 +3,7 @@ namespace Mindscape.Raygun4Net.Storage; -public sealed class CrashReportCacheEntry +public sealed class CrashReportStoreEntry { /// /// Unique ID for the record diff --git a/Mindscape.Raygun4Net.NetCore.Common/Storage/ICrashReportCache.cs b/Mindscape.Raygun4Net.NetCore.Common/Storage/ICrashReportStore.cs similarity index 65% rename from Mindscape.Raygun4Net.NetCore.Common/Storage/ICrashReportCache.cs rename to Mindscape.Raygun4Net.NetCore.Common/Storage/ICrashReportStore.cs index f0912198..4c97520f 100644 --- a/Mindscape.Raygun4Net.NetCore.Common/Storage/ICrashReportCache.cs +++ b/Mindscape.Raygun4Net.NetCore.Common/Storage/ICrashReportStore.cs @@ -5,9 +5,9 @@ namespace Mindscape.Raygun4Net.Storage; -public interface ICrashReportCache +public interface ICrashReportStore { - public Task> GetAll(CancellationToken cancellationToken); - public Task Save(string crashPayload, string apiKey, CancellationToken cancellationToken); + public Task> GetAll(CancellationToken cancellationToken); + public Task Save(string crashPayload, string apiKey, CancellationToken cancellationToken); public Task Remove(Guid cacheEntryId, CancellationToken cancellationToken); } \ No newline at end of file diff --git a/Mindscape.Raygun4Net.NetCore/Storage/FileSystemCrashReportCache.cs b/Mindscape.Raygun4Net.NetCore/Storage/FileSystemCrashReportStore.cs similarity index 89% rename from Mindscape.Raygun4Net.NetCore/Storage/FileSystemCrashReportCache.cs rename to Mindscape.Raygun4Net.NetCore/Storage/FileSystemCrashReportStore.cs index 4693b0d4..0cbc3647 100644 --- a/Mindscape.Raygun4Net.NetCore/Storage/FileSystemCrashReportCache.cs +++ b/Mindscape.Raygun4Net.NetCore/Storage/FileSystemCrashReportStore.cs @@ -10,21 +10,21 @@ namespace Mindscape.Raygun4Net.Storage; -public class FileSystemCrashReportCache : ICrashReportCache +public class FileSystemCrashReportStore : ICrashReportStore { private const string CacheFileExtension = "rgcrash"; private readonly string _storageDirectory; private readonly ConcurrentDictionary _cacheLocationMap = new(); - public FileSystemCrashReportCache(string storageDirectory) + public FileSystemCrashReportStore(string storageDirectory) { _storageDirectory = storageDirectory; } - public virtual async Task> GetAll(CancellationToken cancellationToken) + public virtual async Task> GetAll(CancellationToken cancellationToken) { var crashFiles = Directory.GetFiles(_storageDirectory, $"*.{CacheFileExtension}"); - var errorRecords = new List(); + var errorRecords = new List(); foreach (var crashFile in crashFiles) { @@ -36,7 +36,7 @@ public virtual async Task> GetAll(CancellationToken Trace.WriteLine($"Attempting to load offline crash at {crashFile}"); var jsonString = await reader.ReadToEndAsync(); - var errorRecord = SimpleJson.DeserializeObject(jsonString); + var errorRecord = SimpleJson.DeserializeObject(jsonString); errorRecords.Add(errorRecord); _cacheLocationMap[errorRecord.Id] = crashFile; @@ -51,7 +51,7 @@ public virtual async Task> GetAll(CancellationToken return errorRecords; } - public virtual async Task Save(string payload, string apiKey, + public virtual async Task Save(string payload, string apiKey, CancellationToken cancellationToken) { var cacheEntryId = Guid.NewGuid(); @@ -59,7 +59,7 @@ public virtual async Task Save(string payload, string api { Directory.CreateDirectory(_storageDirectory); - var cacheEntry = new CrashReportCacheEntry + var cacheEntry = new CrashReportStoreEntry { Id = cacheEntryId, ApiKey = apiKey, diff --git a/Mindscape.Raygun4Net.NetCore/Storage/LocalApplicationDataCrashReportCache.cs b/Mindscape.Raygun4Net.NetCore/Storage/LocalApplicationDataCrashReportStore.cs similarity index 78% rename from Mindscape.Raygun4Net.NetCore/Storage/LocalApplicationDataCrashReportCache.cs rename to Mindscape.Raygun4Net.NetCore/Storage/LocalApplicationDataCrashReportStore.cs index afac8de5..8e22abe6 100644 --- a/Mindscape.Raygun4Net.NetCore/Storage/LocalApplicationDataCrashReportCache.cs +++ b/Mindscape.Raygun4Net.NetCore/Storage/LocalApplicationDataCrashReportStore.cs @@ -13,11 +13,11 @@ namespace Mindscape.Raygun4Net.Storage; /// Stores a cached copy of crash reports that failed to send in Local App Data /// Creates a directory if specified, otherwise creates a unique directory based off the location of the application /// -public sealed class LocalApplicationDataCrashReportCache : ICrashReportCache +public sealed class LocalApplicationDataCrashReportStore : ICrashReportStore { - private readonly FileSystemCrashReportCache _fileSystemErrorStorage; + private readonly FileSystemCrashReportStore _fileSystemErrorStorage; - public LocalApplicationDataCrashReportCache(string directoryName = null) + public LocalApplicationDataCrashReportStore(string directoryName = null) { if (directoryName is null) { @@ -29,15 +29,15 @@ public LocalApplicationDataCrashReportCache(string directoryName = null) } var localAppDirectory = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), directoryName); - _fileSystemErrorStorage = new FileSystemCrashReportCache(localAppDirectory); + _fileSystemErrorStorage = new FileSystemCrashReportStore(localAppDirectory); } - public Task> GetAll(CancellationToken cancellationToken) + public Task> GetAll(CancellationToken cancellationToken) { return _fileSystemErrorStorage.GetAll(cancellationToken); } - public Task Save(string crashPayload, string apiKey, CancellationToken cancellationToken) + public Task Save(string crashPayload, string apiKey, CancellationToken cancellationToken) { return _fileSystemErrorStorage.Save(crashPayload, apiKey, cancellationToken); } From 9f4b667f39cadaaa1014f5e99c0e94653c791bed Mon Sep 17 00:00:00 2001 From: Sean Date: Thu, 30 May 2024 11:10:49 +1200 Subject: [PATCH 12/21] Move store to settings, and clean up send logic --- .../RaygunClientBase.cs | 59 +++++++------------ ...aygunSettings.cs => RaygunSettingsBase.cs} | 7 ++- .../Storage/ICrashReportStore.cs | 2 +- .../Storage/FileSystemCrashReportStore.cs | 11 ++-- .../LocalApplicationDataCrashReportStore.cs | 34 ++++------- 5 files changed, 43 insertions(+), 70 deletions(-) rename Mindscape.Raygun4Net.NetCore.Common/{RaygunSettings.cs => RaygunSettingsBase.cs} (93%) diff --git a/Mindscape.Raygun4Net.NetCore.Common/RaygunClientBase.cs b/Mindscape.Raygun4Net.NetCore.Common/RaygunClientBase.cs index d46dc61b..eb86787c 100644 --- a/Mindscape.Raygun4Net.NetCore.Common/RaygunClientBase.cs +++ b/Mindscape.Raygun4Net.NetCore.Common/RaygunClientBase.cs @@ -43,17 +43,15 @@ public abstract class RaygunClientBase protected internal const string SentKey = "AlreadySentByRaygun"; /// - /// Store a strong reference to the OnApplicationUnhandledException delegate so it does not get garbage collected while + /// Store a strong reference to the OnApplicationUnhandledException delegate, so it does not get garbage collected while /// the client is still alive /// // ReSharper disable once PrivateFieldCanBeConvertedToLocalVariable private readonly UnhandledExceptionBridge.UnhandledExceptionHandler _onUnhandledExceptionDelegate; - private ICrashReportStore _crashReportStore; - /// - /// Raised just before a message is sent. This can be used to make final adjustments to the , or to cancel the send. + /// Raised just before a message is sent. This can be used to make final adjustments to the or to cancel the send. /// public event EventHandler SendingMessage; @@ -97,16 +95,6 @@ public virtual bool CatchUnhandledExceptions set => _settings.CatchUnhandledExceptions = value; } - public ICrashReportStore CrashReportStore - { - get => _crashReportStore; - set - { - _crashReportStore = value; - CachedCrashReportBackgroundWorker.SetSendCallback(SendPayloadAsync); - } - } - private void OnApplicationUnhandledException(Exception exception, bool isTerminating) { if (!_settings.CatchUnhandledExceptions) @@ -145,6 +133,11 @@ protected RaygunClientBase(RaygunSettingsBase settings, HttpClient client, IRayg _onUnhandledExceptionDelegate = OnApplicationUnhandledException; UnhandledExceptionBridge.OnUnhandledException(_onUnhandledExceptionDelegate); + + if (settings.OfflineStore != null) + { + CachedCrashReportBackgroundWorker.SetSendCallback(SendPayloadAsync); + } } /// @@ -532,56 +525,44 @@ public async Task Send(RaygunMessage raygunMessage, CancellationToken cancellati internal async Task SendPayloadAsync(string payload, string apiKey, CancellationToken cancellationToken) { - var shouldStoreMessage = false; + HttpResponseMessage response = null; var requestMessage = new HttpRequestMessage(HttpMethod.Post, _settings.ApiEndpoint); requestMessage.Headers.Add("X-ApiKey", apiKey); requestMessage.Content = new StringContent(payload, Encoding.UTF8, "application/json"); try { - var response = await _client.SendAsync(requestMessage, cancellationToken).ConfigureAwait(false); - - if (!response.IsSuccessStatusCode) - { - Debug.WriteLine($"Error Logging Exception to Raygun {response.ReasonPhrase}"); - - if (response.StatusCode >= HttpStatusCode.InternalServerError) - { - // If we got a server error then add it to offline storage to send later - shouldStoreMessage = true; - } + response = await _client.SendAsync(requestMessage, cancellationToken).ConfigureAwait(false); - // Cause an exception to be bubbled up the stack - response.EnsureSuccessStatusCode(); - } + // Raises an HttpRequestException if the request was unsuccessful + response.EnsureSuccessStatusCode(); } catch (Exception ex) { - Debug.WriteLine($"Error Logging Exception to Raygun {ex.Message}"); - shouldStoreMessage = true; + Debug.WriteLine($"Error Logging Exception to Raygun: {ex.Message}"); + + // If we got no response or an unexpected server error then add it to offline storage to send later + // we get no response if the send call fails for any other reason (network etc) + var shouldStoreMessage = response is null || response.StatusCode >= HttpStatusCode.InternalServerError; - throw; - } - finally - { if (shouldStoreMessage) { await SaveMessageToOfflineCache(payload, apiKey, cancellationToken); } + + throw; } } - private async Task SaveMessageToOfflineCache(string messagePayload, string apiKey, CancellationToken cancellationToken) { // Can't store it anywhere - if (_crashReportStore is null) + if (_settings.OfflineStore is null) { return false; } - var cacheEntry = await _crashReportStore.Save(messagePayload, apiKey, cancellationToken); - return cacheEntry != null; + return await _settings.OfflineStore.Save(messagePayload, apiKey, cancellationToken); } } } \ No newline at end of file diff --git a/Mindscape.Raygun4Net.NetCore.Common/RaygunSettings.cs b/Mindscape.Raygun4Net.NetCore.Common/RaygunSettingsBase.cs similarity index 93% rename from Mindscape.Raygun4Net.NetCore.Common/RaygunSettings.cs rename to Mindscape.Raygun4Net.NetCore.Common/RaygunSettingsBase.cs index ce92128f..69220401 100644 --- a/Mindscape.Raygun4Net.NetCore.Common/RaygunSettings.cs +++ b/Mindscape.Raygun4Net.NetCore.Common/RaygunSettingsBase.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using Mindscape.Raygun4Net.Storage; namespace Mindscape.Raygun4Net; @@ -23,8 +24,8 @@ public RaygunSettingsBase() /// Raygun Application API Key, can be found in the Raygun application dashboard by clicking the "Application settings" button /// public string ApiKey { get; set; } - - public Uri ApiEndpoint { get; set; } = new (DefaultApiEndPoint); + + public Uri ApiEndpoint { get; set; } = new(DefaultApiEndPoint); public bool ThrowOnError { get; set; } @@ -62,4 +63,6 @@ public RaygunSettingsBase() /// Passing in * will be ignored as we do not want to support collecting 'all' environment variables for security reasons. /// public IList EnvironmentVariables { get; set; } = new List(); + + public ICrashReportStore OfflineStore { get; set; } } \ No newline at end of file diff --git a/Mindscape.Raygun4Net.NetCore.Common/Storage/ICrashReportStore.cs b/Mindscape.Raygun4Net.NetCore.Common/Storage/ICrashReportStore.cs index 4c97520f..ad06abee 100644 --- a/Mindscape.Raygun4Net.NetCore.Common/Storage/ICrashReportStore.cs +++ b/Mindscape.Raygun4Net.NetCore.Common/Storage/ICrashReportStore.cs @@ -8,6 +8,6 @@ namespace Mindscape.Raygun4Net.Storage; public interface ICrashReportStore { public Task> GetAll(CancellationToken cancellationToken); - public Task Save(string crashPayload, string apiKey, CancellationToken cancellationToken); + public Task Save(string crashPayload, string apiKey, CancellationToken cancellationToken); public Task Remove(Guid cacheEntryId, CancellationToken cancellationToken); } \ No newline at end of file diff --git a/Mindscape.Raygun4Net.NetCore/Storage/FileSystemCrashReportStore.cs b/Mindscape.Raygun4Net.NetCore/Storage/FileSystemCrashReportStore.cs index 0cbc3647..a56ec440 100644 --- a/Mindscape.Raygun4Net.NetCore/Storage/FileSystemCrashReportStore.cs +++ b/Mindscape.Raygun4Net.NetCore/Storage/FileSystemCrashReportStore.cs @@ -14,11 +14,13 @@ public class FileSystemCrashReportStore : ICrashReportStore { private const string CacheFileExtension = "rgcrash"; private readonly string _storageDirectory; + private readonly int _maxOfflineFiles; private readonly ConcurrentDictionary _cacheLocationMap = new(); - public FileSystemCrashReportStore(string storageDirectory) + public FileSystemCrashReportStore(string storageDirectory, int maxOfflineFiles = 50) { _storageDirectory = storageDirectory; + _maxOfflineFiles = maxOfflineFiles; } public virtual async Task> GetAll(CancellationToken cancellationToken) @@ -51,8 +53,7 @@ public virtual async Task> GetAll(CancellationToken return errorRecords; } - public virtual async Task Save(string payload, string apiKey, - CancellationToken cancellationToken) + public virtual async Task Save(string payload, string apiKey, CancellationToken cancellationToken) { var cacheEntryId = Guid.NewGuid(); try @@ -76,12 +77,12 @@ public virtual async Task Save(string payload, string api await writer.WriteAsync(jsonContent); await writer.FlushAsync(); - return cacheEntry; + return true; } catch (Exception ex) { Debug.WriteLine($"Error adding crash [{cacheEntryId}] to store: {ex}"); - return null; + return false; } } diff --git a/Mindscape.Raygun4Net.NetCore/Storage/LocalApplicationDataCrashReportStore.cs b/Mindscape.Raygun4Net.NetCore/Storage/LocalApplicationDataCrashReportStore.cs index 8e22abe6..82f0bded 100644 --- a/Mindscape.Raygun4Net.NetCore/Storage/LocalApplicationDataCrashReportStore.cs +++ b/Mindscape.Raygun4Net.NetCore/Storage/LocalApplicationDataCrashReportStore.cs @@ -13,37 +13,25 @@ namespace Mindscape.Raygun4Net.Storage; /// Stores a cached copy of crash reports that failed to send in Local App Data /// Creates a directory if specified, otherwise creates a unique directory based off the location of the application /// -public sealed class LocalApplicationDataCrashReportStore : ICrashReportStore +public sealed class LocalApplicationDataCrashReportStore : FileSystemCrashReportStore { - private readonly FileSystemCrashReportStore _fileSystemErrorStorage; - - public LocalApplicationDataCrashReportStore(string directoryName = null) + public LocalApplicationDataCrashReportStore(string directoryName = null, int maxOfflineFiles = 50) + : base(GetLocalAppDirectory(directoryName), maxOfflineFiles) { - if (directoryName is null) - { - // Try generate a unique id, from the executable location - var uniqueId = Assembly.GetEntryAssembly()?.Location ?? throw new ApplicationException("Cannot determine unique application id"); - - var uniqueIdHash = SHA1.Create().ComputeHash(Encoding.UTF8.GetBytes(uniqueId)); - directoryName = BitConverter.ToString(uniqueIdHash).Replace("-", "").ToLowerInvariant(); - } - - var localAppDirectory = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), directoryName); - _fileSystemErrorStorage = new FileSystemCrashReportStore(localAppDirectory); } - public Task> GetAll(CancellationToken cancellationToken) + private static string GetLocalAppDirectory(string directoryName) { - return _fileSystemErrorStorage.GetAll(cancellationToken); + directoryName ??= CreateUniqueDirectory(); + return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), directoryName); } - public Task Save(string crashPayload, string apiKey, CancellationToken cancellationToken) + private static string CreateUniqueDirectory() { - return _fileSystemErrorStorage.Save(crashPayload, apiKey, cancellationToken); - } + // Try to generate a unique id, from the executable location + var uniqueId = Assembly.GetEntryAssembly()?.Location ?? throw new ApplicationException("Cannot determine unique application id"); - public Task Remove(Guid cacheId, CancellationToken cancellationToken) - { - return _fileSystemErrorStorage.Remove(cacheId, cancellationToken); + var uniqueIdHash = SHA1.Create().ComputeHash(Encoding.UTF8.GetBytes(uniqueId)); + return BitConverter.ToString(uniqueIdHash).Replace("-", "").ToLowerInvariant(); } } \ No newline at end of file From 610d34741a6b184eda8c2a622a572112d4e56042 Mon Sep 17 00:00:00 2001 From: Sean Date: Thu, 30 May 2024 11:57:35 +1200 Subject: [PATCH 13/21] Flip the processing to the store, move the sending into a strategy. This should encapsulate the background timer logic, but not mess with weird statics. It ties a store to a send strategy, and that store can be a singleton managed by the user --- .../CrashReportStoreEntry.cs | 2 +- .../{Storage => Offline}/ICrashReportStore.cs | 5 +- .../Offline/IOfflineSendStrategy.cs | 11 ++ .../Offline/TimerBasedSendStrategy.cs | 45 ++++++++ .../RaygunClientBase.cs | 3 +- .../RaygunSettingsBase.cs | 2 +- .../CachedCrashReportBackgroundWorker.cs | 109 ------------------ .../Storage/FileSystemCrashReportStore.cs | 45 +++++++- .../LocalApplicationDataCrashReportStore.cs | 7 +- 9 files changed, 110 insertions(+), 119 deletions(-) rename Mindscape.Raygun4Net.NetCore.Common/{Storage => Offline}/CrashReportStoreEntry.cs (91%) rename Mindscape.Raygun4Net.NetCore.Common/{Storage => Offline}/ICrashReportStore.cs (67%) create mode 100644 Mindscape.Raygun4Net.NetCore.Common/Offline/IOfflineSendStrategy.cs create mode 100644 Mindscape.Raygun4Net.NetCore.Common/Offline/TimerBasedSendStrategy.cs delete mode 100644 Mindscape.Raygun4Net.NetCore.Common/Storage/CachedCrashReportBackgroundWorker.cs diff --git a/Mindscape.Raygun4Net.NetCore.Common/Storage/CrashReportStoreEntry.cs b/Mindscape.Raygun4Net.NetCore.Common/Offline/CrashReportStoreEntry.cs similarity index 91% rename from Mindscape.Raygun4Net.NetCore.Common/Storage/CrashReportStoreEntry.cs rename to Mindscape.Raygun4Net.NetCore.Common/Offline/CrashReportStoreEntry.cs index 40f13a16..0605d14b 100644 --- a/Mindscape.Raygun4Net.NetCore.Common/Storage/CrashReportStoreEntry.cs +++ b/Mindscape.Raygun4Net.NetCore.Common/Offline/CrashReportStoreEntry.cs @@ -1,7 +1,7 @@ using System; using System.Runtime.Serialization; -namespace Mindscape.Raygun4Net.Storage; +namespace Mindscape.Raygun4Net.Offline; public sealed class CrashReportStoreEntry { diff --git a/Mindscape.Raygun4Net.NetCore.Common/Storage/ICrashReportStore.cs b/Mindscape.Raygun4Net.NetCore.Common/Offline/ICrashReportStore.cs similarity index 67% rename from Mindscape.Raygun4Net.NetCore.Common/Storage/ICrashReportStore.cs rename to Mindscape.Raygun4Net.NetCore.Common/Offline/ICrashReportStore.cs index ad06abee..418fc348 100644 --- a/Mindscape.Raygun4Net.NetCore.Common/Storage/ICrashReportStore.cs +++ b/Mindscape.Raygun4Net.NetCore.Common/Offline/ICrashReportStore.cs @@ -3,10 +3,13 @@ using System.Threading; using System.Threading.Tasks; -namespace Mindscape.Raygun4Net.Storage; +namespace Mindscape.Raygun4Net.Offline; + +public delegate Task SendHandler(string messagePayload, string apiKey, CancellationToken cancellationToken); public interface ICrashReportStore { + void SetSendCallback(SendHandler sendHandler); public Task> GetAll(CancellationToken cancellationToken); public Task Save(string crashPayload, string apiKey, CancellationToken cancellationToken); public Task Remove(Guid cacheEntryId, CancellationToken cancellationToken); diff --git a/Mindscape.Raygun4Net.NetCore.Common/Offline/IOfflineSendStrategy.cs b/Mindscape.Raygun4Net.NetCore.Common/Offline/IOfflineSendStrategy.cs new file mode 100644 index 00000000..1970a63d --- /dev/null +++ b/Mindscape.Raygun4Net.NetCore.Common/Offline/IOfflineSendStrategy.cs @@ -0,0 +1,11 @@ +using System; +using System.Threading.Tasks; + +namespace Mindscape.Raygun4Net.Offline; + +public interface IOfflineSendStrategy : IDisposable +{ + public event Action OnSend; + void Start(); + void Stop(); +} \ No newline at end of file diff --git a/Mindscape.Raygun4Net.NetCore.Common/Offline/TimerBasedSendStrategy.cs b/Mindscape.Raygun4Net.NetCore.Common/Offline/TimerBasedSendStrategy.cs new file mode 100644 index 00000000..25c45346 --- /dev/null +++ b/Mindscape.Raygun4Net.NetCore.Common/Offline/TimerBasedSendStrategy.cs @@ -0,0 +1,45 @@ +using System; +using System.Threading; + +namespace Mindscape.Raygun4Net.Offline; + +public class TimerBasedSendStrategy : IOfflineSendStrategy +{ + private static readonly TimeSpan DefaultInternal = TimeSpan.FromSeconds(30); + + private readonly Timer _backgroundTimer; + public event Action OnSend; + + public TimeSpan Interval { get; } + + public TimerBasedSendStrategy(TimeSpan? interval = null) + { + Interval = interval ?? DefaultInternal; + _backgroundTimer = new Timer(SendOfflineErrors); + } + + ~TimerBasedSendStrategy() + { + Dispose(); + } + + private void SendOfflineErrors(object state) + { + OnSend?.Invoke(); + } + + public void Start() + { + _backgroundTimer.Change(Interval, TimeSpan.FromMilliseconds(int.MaxValue)); + } + + public void Stop() + { + _backgroundTimer.Change(Timeout.Infinite, 0); + } + + public void Dispose() + { + _backgroundTimer?.Dispose(); + } +} \ No newline at end of file diff --git a/Mindscape.Raygun4Net.NetCore.Common/RaygunClientBase.cs b/Mindscape.Raygun4Net.NetCore.Common/RaygunClientBase.cs index eb86787c..433d890e 100644 --- a/Mindscape.Raygun4Net.NetCore.Common/RaygunClientBase.cs +++ b/Mindscape.Raygun4Net.NetCore.Common/RaygunClientBase.cs @@ -10,7 +10,6 @@ using System.Threading; using System.Threading.Tasks; using Mindscape.Raygun4Net.Breadcrumbs; -using Mindscape.Raygun4Net.Storage; namespace Mindscape.Raygun4Net { @@ -136,7 +135,7 @@ protected RaygunClientBase(RaygunSettingsBase settings, HttpClient client, IRayg if (settings.OfflineStore != null) { - CachedCrashReportBackgroundWorker.SetSendCallback(SendPayloadAsync); + settings.OfflineStore.SetSendCallback(SendPayloadAsync); } } diff --git a/Mindscape.Raygun4Net.NetCore.Common/RaygunSettingsBase.cs b/Mindscape.Raygun4Net.NetCore.Common/RaygunSettingsBase.cs index 69220401..2762e912 100644 --- a/Mindscape.Raygun4Net.NetCore.Common/RaygunSettingsBase.cs +++ b/Mindscape.Raygun4Net.NetCore.Common/RaygunSettingsBase.cs @@ -1,6 +1,6 @@ using System; using System.Collections.Generic; -using Mindscape.Raygun4Net.Storage; +using Mindscape.Raygun4Net.Offline; namespace Mindscape.Raygun4Net; diff --git a/Mindscape.Raygun4Net.NetCore.Common/Storage/CachedCrashReportBackgroundWorker.cs b/Mindscape.Raygun4Net.NetCore.Common/Storage/CachedCrashReportBackgroundWorker.cs deleted file mode 100644 index 7fae15ba..00000000 --- a/Mindscape.Raygun4Net.NetCore.Common/Storage/CachedCrashReportBackgroundWorker.cs +++ /dev/null @@ -1,109 +0,0 @@ -using System; -using System.Diagnostics; -using System.Threading; -using System.Threading.Tasks; - -namespace Mindscape.Raygun4Net.Storage; - -public static class CachedCrashReportBackgroundWorker -{ - internal delegate Task SendHandler(string messagePayload, string apiKey, CancellationToken cancellationToken); - - private static readonly Timer BackgroundTimer = new Timer(SendOfflineErrors); - private static volatile bool _isRunning; - private static TimeSpan _interval = TimeSpan.FromSeconds(30); - private static SendHandler _sendHandler; - private static Func _crashReportCache; - - public static TimeSpan Interval - { - get => _interval; - set - { - _interval = value; - - // Set the new interval on the timer - BackgroundTimer.Change(Interval, TimeSpan.FromMilliseconds(int.MaxValue)); - } - } - - public static bool IsRunning => _isRunning; - - static CachedCrashReportBackgroundWorker() - { - Start(); - } - - public static void SetCrashReportCache(Func offlineStoreFunc) - { - _crashReportCache = offlineStoreFunc; - } - - internal static void SetSendCallback(SendHandler sendHandler) - { - _sendHandler = sendHandler; - } - - private static async void SendOfflineErrors(object state) - { - try - { - await SendCachedErrors(); - } - finally - { - // Always restart the timer - BackgroundTimer.Change(Interval, TimeSpan.FromMilliseconds(int.MaxValue)); - } - } - - private static async Task SendCachedErrors() - { - var store = _crashReportCache?.Invoke(); - - // We don't have a store set, or a send handler - so we can't actually do anything - if (store is null || _sendHandler is null) - return; - - try - { - var cachedCrashReports = await store.GetAll(CancellationToken.None); - foreach (var crashReport in cachedCrashReports) - { - try - { - await _sendHandler(crashReport.MessagePayload, crashReport.ApiKey, CancellationToken.None); - await store.Remove(crashReport.Id, CancellationToken.None); - } - catch (Exception ex) - { - Debug.WriteLine($"Exception sending offline error [{crashReport.Id}]: {ex}"); - throw; - } - } - } - catch (Exception ex) - { - Debug.WriteLine($"Exception sending offline errors: {ex}"); - } - } - - /// - /// Start the internal timer. This will enable the sending of any offline stored errors. - /// This requires SetSendCallback to be - /// - public static void Start() - { - BackgroundTimer.Change(Interval, TimeSpan.FromMilliseconds(int.MaxValue)); - _isRunning = true; - } - - /// - /// Stop the internal timer - and prevents any offline errors from being sent in the background - /// - public static void Stop() - { - BackgroundTimer.Change(Timeout.Infinite, 0); - _isRunning = false; - } -} \ No newline at end of file diff --git a/Mindscape.Raygun4Net.NetCore/Storage/FileSystemCrashReportStore.cs b/Mindscape.Raygun4Net.NetCore/Storage/FileSystemCrashReportStore.cs index a56ec440..d6b51489 100644 --- a/Mindscape.Raygun4Net.NetCore/Storage/FileSystemCrashReportStore.cs +++ b/Mindscape.Raygun4Net.NetCore/Storage/FileSystemCrashReportStore.cs @@ -7,20 +7,31 @@ using System.Text; using System.Threading; using System.Threading.Tasks; +using Mindscape.Raygun4Net.Offline; namespace Mindscape.Raygun4Net.Storage; public class FileSystemCrashReportStore : ICrashReportStore { private const string CacheFileExtension = "rgcrash"; + private readonly IOfflineSendStrategy _offlineSendStrategy; private readonly string _storageDirectory; private readonly int _maxOfflineFiles; private readonly ConcurrentDictionary _cacheLocationMap = new(); + private SendHandler _sendHandler; - public FileSystemCrashReportStore(string storageDirectory, int maxOfflineFiles = 50) + public FileSystemCrashReportStore(IOfflineSendStrategy offlineSendStrategy, string storageDirectory, int maxOfflineFiles = 50) { + _offlineSendStrategy = offlineSendStrategy; _storageDirectory = storageDirectory; _maxOfflineFiles = maxOfflineFiles; + + _offlineSendStrategy.OnSend += ProcessOfflineErrors; + } + + public void SetSendCallback(SendHandler sendHandler) + { + _sendHandler = sendHandler; } public virtual async Task> GetAll(CancellationToken cancellationToken) @@ -60,6 +71,13 @@ public virtual async Task Save(string payload, string apiKey, Cancellation { Directory.CreateDirectory(_storageDirectory); + var crashFiles = Directory.GetFiles(_storageDirectory, $"*.{CacheFileExtension}"); + if (crashFiles.Length >= _maxOfflineFiles) + { + Debug.WriteLine($"Maximum offline files of [{_maxOfflineFiles}] has been reached"); + return false; + } + var cacheEntry = new CrashReportStoreEntry { Id = cacheEntryId, @@ -120,4 +138,29 @@ private string GetFilePathForCacheEntry(Guid cacheId) { return Path.Combine(_storageDirectory, $"{cacheId:N}.{CacheFileExtension}"); } + + private async void ProcessOfflineErrors() + { + try + { + var cachedCrashReports = await GetAll(CancellationToken.None); + foreach (var crashReport in cachedCrashReports) + { + try + { + await _sendHandler(crashReport.MessagePayload, crashReport.ApiKey, CancellationToken.None); + await Remove(crashReport.Id, CancellationToken.None); + } + catch (Exception ex) + { + Debug.WriteLine($"Exception sending offline error [{crashReport.Id}]: {ex}"); + throw; + } + } + } + catch (Exception ex) + { + Debug.WriteLine($"Exception sending offline errors: {ex}"); + } + } } \ No newline at end of file diff --git a/Mindscape.Raygun4Net.NetCore/Storage/LocalApplicationDataCrashReportStore.cs b/Mindscape.Raygun4Net.NetCore/Storage/LocalApplicationDataCrashReportStore.cs index 82f0bded..efbaa035 100644 --- a/Mindscape.Raygun4Net.NetCore/Storage/LocalApplicationDataCrashReportStore.cs +++ b/Mindscape.Raygun4Net.NetCore/Storage/LocalApplicationDataCrashReportStore.cs @@ -4,8 +4,7 @@ using System.Reflection; using System.Security.Cryptography; using System.Text; -using System.Threading; -using System.Threading.Tasks; +using Mindscape.Raygun4Net.Offline; namespace Mindscape.Raygun4Net.Storage; @@ -15,8 +14,8 @@ namespace Mindscape.Raygun4Net.Storage; /// public sealed class LocalApplicationDataCrashReportStore : FileSystemCrashReportStore { - public LocalApplicationDataCrashReportStore(string directoryName = null, int maxOfflineFiles = 50) - : base(GetLocalAppDirectory(directoryName), maxOfflineFiles) + public LocalApplicationDataCrashReportStore(IOfflineSendStrategy offlineSendStrategy, string directoryName = null, int maxOfflineFiles = 50) + : base(offlineSendStrategy, GetLocalAppDirectory(directoryName), maxOfflineFiles) { } From 4986a5734437803bf67ee3b9822223f34eee1ae8 Mon Sep 17 00:00:00 2001 From: Sean Date: Thu, 30 May 2024 12:03:39 +1200 Subject: [PATCH 14/21] Make interfaces explicit --- .../Offline/CrashReportStoreEntry.cs | 1 - .../Offline/ICrashReportStore.cs | 2 +- .../Offline/IOfflineSendStrategy.cs | 5 ++--- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/Mindscape.Raygun4Net.NetCore.Common/Offline/CrashReportStoreEntry.cs b/Mindscape.Raygun4Net.NetCore.Common/Offline/CrashReportStoreEntry.cs index 0605d14b..9d19c917 100644 --- a/Mindscape.Raygun4Net.NetCore.Common/Offline/CrashReportStoreEntry.cs +++ b/Mindscape.Raygun4Net.NetCore.Common/Offline/CrashReportStoreEntry.cs @@ -1,5 +1,4 @@ using System; -using System.Runtime.Serialization; namespace Mindscape.Raygun4Net.Offline; diff --git a/Mindscape.Raygun4Net.NetCore.Common/Offline/ICrashReportStore.cs b/Mindscape.Raygun4Net.NetCore.Common/Offline/ICrashReportStore.cs index 418fc348..1f3ae215 100644 --- a/Mindscape.Raygun4Net.NetCore.Common/Offline/ICrashReportStore.cs +++ b/Mindscape.Raygun4Net.NetCore.Common/Offline/ICrashReportStore.cs @@ -9,7 +9,7 @@ namespace Mindscape.Raygun4Net.Offline; public interface ICrashReportStore { - void SetSendCallback(SendHandler sendHandler); + public void SetSendCallback(SendHandler sendHandler); public Task> GetAll(CancellationToken cancellationToken); public Task Save(string crashPayload, string apiKey, CancellationToken cancellationToken); public Task Remove(Guid cacheEntryId, CancellationToken cancellationToken); diff --git a/Mindscape.Raygun4Net.NetCore.Common/Offline/IOfflineSendStrategy.cs b/Mindscape.Raygun4Net.NetCore.Common/Offline/IOfflineSendStrategy.cs index 1970a63d..e333546b 100644 --- a/Mindscape.Raygun4Net.NetCore.Common/Offline/IOfflineSendStrategy.cs +++ b/Mindscape.Raygun4Net.NetCore.Common/Offline/IOfflineSendStrategy.cs @@ -1,11 +1,10 @@ using System; -using System.Threading.Tasks; namespace Mindscape.Raygun4Net.Offline; public interface IOfflineSendStrategy : IDisposable { public event Action OnSend; - void Start(); - void Stop(); + public void Start(); + public void Stop(); } \ No newline at end of file From 22fea6a2ac1184a1d87b829aacc58ad95bff6e26 Mon Sep 17 00:00:00 2001 From: Sean Date: Thu, 30 May 2024 12:58:10 +1200 Subject: [PATCH 15/21] Not everything needs to be an interface! --- ...Strategy.cs => IBackgroundSendStrategy.cs} | 2 +- .../Offline/ICrashReportStore.cs | 16 ------ .../Offline/OfflineStoreBase.cs | 55 +++++++++++++++++++ .../Offline/TimerBasedSendStrategy.cs | 2 +- .../RaygunSettingsBase.cs | 2 +- .../Storage/FileSystemCrashReportStore.cs | 46 ++-------------- .../LocalApplicationDataCrashReportStore.cs | 4 +- 7 files changed, 66 insertions(+), 61 deletions(-) rename Mindscape.Raygun4Net.NetCore.Common/Offline/{IOfflineSendStrategy.cs => IBackgroundSendStrategy.cs} (70%) delete mode 100644 Mindscape.Raygun4Net.NetCore.Common/Offline/ICrashReportStore.cs create mode 100644 Mindscape.Raygun4Net.NetCore.Common/Offline/OfflineStoreBase.cs diff --git a/Mindscape.Raygun4Net.NetCore.Common/Offline/IOfflineSendStrategy.cs b/Mindscape.Raygun4Net.NetCore.Common/Offline/IBackgroundSendStrategy.cs similarity index 70% rename from Mindscape.Raygun4Net.NetCore.Common/Offline/IOfflineSendStrategy.cs rename to Mindscape.Raygun4Net.NetCore.Common/Offline/IBackgroundSendStrategy.cs index e333546b..2ca72dd4 100644 --- a/Mindscape.Raygun4Net.NetCore.Common/Offline/IOfflineSendStrategy.cs +++ b/Mindscape.Raygun4Net.NetCore.Common/Offline/IBackgroundSendStrategy.cs @@ -2,7 +2,7 @@ namespace Mindscape.Raygun4Net.Offline; -public interface IOfflineSendStrategy : IDisposable +public interface IBackgroundSendStrategy : IDisposable { public event Action OnSend; public void Start(); diff --git a/Mindscape.Raygun4Net.NetCore.Common/Offline/ICrashReportStore.cs b/Mindscape.Raygun4Net.NetCore.Common/Offline/ICrashReportStore.cs deleted file mode 100644 index 1f3ae215..00000000 --- a/Mindscape.Raygun4Net.NetCore.Common/Offline/ICrashReportStore.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; - -namespace Mindscape.Raygun4Net.Offline; - -public delegate Task SendHandler(string messagePayload, string apiKey, CancellationToken cancellationToken); - -public interface ICrashReportStore -{ - public void SetSendCallback(SendHandler sendHandler); - public Task> GetAll(CancellationToken cancellationToken); - public Task Save(string crashPayload, string apiKey, CancellationToken cancellationToken); - public Task Remove(Guid cacheEntryId, CancellationToken cancellationToken); -} \ No newline at end of file diff --git a/Mindscape.Raygun4Net.NetCore.Common/Offline/OfflineStoreBase.cs b/Mindscape.Raygun4Net.NetCore.Common/Offline/OfflineStoreBase.cs new file mode 100644 index 00000000..b2a986e1 --- /dev/null +++ b/Mindscape.Raygun4Net.NetCore.Common/Offline/OfflineStoreBase.cs @@ -0,0 +1,55 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Threading; +using System.Threading.Tasks; + +namespace Mindscape.Raygun4Net.Offline; + +public delegate Task SendHandler(string messagePayload, string apiKey, CancellationToken cancellationToken); + +public abstract class OfflineStoreBase +{ + private readonly IBackgroundSendStrategy _backgroundSendStrategy; + protected SendHandler SendCallback { get; set; } + + protected OfflineStoreBase(IBackgroundSendStrategy backgroundSendStrategy) + { + _backgroundSendStrategy = backgroundSendStrategy; + _backgroundSendStrategy.OnSend += async () => await ProcessOfflineCrashReports(); + } + + public virtual void SetSendCallback(SendHandler sendHandler) + { + SendCallback = sendHandler; + } + + protected virtual async Task ProcessOfflineCrashReports() + { + try + { + var cachedCrashReports = await GetAll(CancellationToken.None); + foreach (var crashReport in cachedCrashReports) + { + try + { + await SendCallback(crashReport.MessagePayload, crashReport.ApiKey, CancellationToken.None); + await Remove(crashReport.Id, CancellationToken.None); + } + catch (Exception ex) + { + Debug.WriteLine($"Exception sending offline error [{crashReport.Id}]: {ex}"); + throw; + } + } + } + catch (Exception ex) + { + Debug.WriteLine($"Exception sending offline errors: {ex}"); + } + } + + public abstract Task> GetAll(CancellationToken cancellationToken); + public abstract Task Save(string crashPayload, string apiKey, CancellationToken cancellationToken); + public abstract Task Remove(Guid cacheEntryId, CancellationToken cancellationToken); +} \ No newline at end of file diff --git a/Mindscape.Raygun4Net.NetCore.Common/Offline/TimerBasedSendStrategy.cs b/Mindscape.Raygun4Net.NetCore.Common/Offline/TimerBasedSendStrategy.cs index 25c45346..e60a013a 100644 --- a/Mindscape.Raygun4Net.NetCore.Common/Offline/TimerBasedSendStrategy.cs +++ b/Mindscape.Raygun4Net.NetCore.Common/Offline/TimerBasedSendStrategy.cs @@ -3,7 +3,7 @@ namespace Mindscape.Raygun4Net.Offline; -public class TimerBasedSendStrategy : IOfflineSendStrategy +public class TimerBasedSendStrategy : IBackgroundSendStrategy { private static readonly TimeSpan DefaultInternal = TimeSpan.FromSeconds(30); diff --git a/Mindscape.Raygun4Net.NetCore.Common/RaygunSettingsBase.cs b/Mindscape.Raygun4Net.NetCore.Common/RaygunSettingsBase.cs index 2762e912..ac39ed31 100644 --- a/Mindscape.Raygun4Net.NetCore.Common/RaygunSettingsBase.cs +++ b/Mindscape.Raygun4Net.NetCore.Common/RaygunSettingsBase.cs @@ -64,5 +64,5 @@ public RaygunSettingsBase() /// public IList EnvironmentVariables { get; set; } = new List(); - public ICrashReportStore OfflineStore { get; set; } + public OfflineStoreBase OfflineStore { get; set; } } \ No newline at end of file diff --git a/Mindscape.Raygun4Net.NetCore/Storage/FileSystemCrashReportStore.cs b/Mindscape.Raygun4Net.NetCore/Storage/FileSystemCrashReportStore.cs index d6b51489..6f5b1178 100644 --- a/Mindscape.Raygun4Net.NetCore/Storage/FileSystemCrashReportStore.cs +++ b/Mindscape.Raygun4Net.NetCore/Storage/FileSystemCrashReportStore.cs @@ -11,30 +11,21 @@ namespace Mindscape.Raygun4Net.Storage; -public class FileSystemCrashReportStore : ICrashReportStore +public class FileSystemCrashReportStore : OfflineStoreBase { private const string CacheFileExtension = "rgcrash"; - private readonly IOfflineSendStrategy _offlineSendStrategy; private readonly string _storageDirectory; private readonly int _maxOfflineFiles; private readonly ConcurrentDictionary _cacheLocationMap = new(); - private SendHandler _sendHandler; - public FileSystemCrashReportStore(IOfflineSendStrategy offlineSendStrategy, string storageDirectory, int maxOfflineFiles = 50) + public FileSystemCrashReportStore(IBackgroundSendStrategy backgroundSendStrategy, string storageDirectory, int maxOfflineFiles = 50) + : base(backgroundSendStrategy) { - _offlineSendStrategy = offlineSendStrategy; _storageDirectory = storageDirectory; _maxOfflineFiles = maxOfflineFiles; - - _offlineSendStrategy.OnSend += ProcessOfflineErrors; - } - - public void SetSendCallback(SendHandler sendHandler) - { - _sendHandler = sendHandler; } - public virtual async Task> GetAll(CancellationToken cancellationToken) + public override async Task> GetAll(CancellationToken cancellationToken) { var crashFiles = Directory.GetFiles(_storageDirectory, $"*.{CacheFileExtension}"); var errorRecords = new List(); @@ -64,7 +55,7 @@ public virtual async Task> GetAll(CancellationToken return errorRecords; } - public virtual async Task Save(string payload, string apiKey, CancellationToken cancellationToken) + public override async Task Save(string payload, string apiKey, CancellationToken cancellationToken) { var cacheEntryId = Guid.NewGuid(); try @@ -104,7 +95,7 @@ public virtual async Task Save(string payload, string apiKey, Cancellation } } - public virtual Task Remove(Guid cacheId, CancellationToken cancellationToken) + public override Task Remove(Guid cacheId, CancellationToken cancellationToken) { try { @@ -138,29 +129,4 @@ private string GetFilePathForCacheEntry(Guid cacheId) { return Path.Combine(_storageDirectory, $"{cacheId:N}.{CacheFileExtension}"); } - - private async void ProcessOfflineErrors() - { - try - { - var cachedCrashReports = await GetAll(CancellationToken.None); - foreach (var crashReport in cachedCrashReports) - { - try - { - await _sendHandler(crashReport.MessagePayload, crashReport.ApiKey, CancellationToken.None); - await Remove(crashReport.Id, CancellationToken.None); - } - catch (Exception ex) - { - Debug.WriteLine($"Exception sending offline error [{crashReport.Id}]: {ex}"); - throw; - } - } - } - catch (Exception ex) - { - Debug.WriteLine($"Exception sending offline errors: {ex}"); - } - } } \ No newline at end of file diff --git a/Mindscape.Raygun4Net.NetCore/Storage/LocalApplicationDataCrashReportStore.cs b/Mindscape.Raygun4Net.NetCore/Storage/LocalApplicationDataCrashReportStore.cs index efbaa035..35de5986 100644 --- a/Mindscape.Raygun4Net.NetCore/Storage/LocalApplicationDataCrashReportStore.cs +++ b/Mindscape.Raygun4Net.NetCore/Storage/LocalApplicationDataCrashReportStore.cs @@ -14,8 +14,8 @@ namespace Mindscape.Raygun4Net.Storage; /// public sealed class LocalApplicationDataCrashReportStore : FileSystemCrashReportStore { - public LocalApplicationDataCrashReportStore(IOfflineSendStrategy offlineSendStrategy, string directoryName = null, int maxOfflineFiles = 50) - : base(offlineSendStrategy, GetLocalAppDirectory(directoryName), maxOfflineFiles) + public LocalApplicationDataCrashReportStore(IBackgroundSendStrategy backgroundSendStrategy, string directoryName = null, int maxOfflineFiles = 50) + : base(backgroundSendStrategy, GetLocalAppDirectory(directoryName), maxOfflineFiles) { } From cba4c80625fc8bb390d4e8f61744e0493e9790b4 Mon Sep 17 00:00:00 2001 From: Sean Date: Thu, 30 May 2024 13:37:17 +1200 Subject: [PATCH 16/21] Correct some of the sending logic. --- .../Offline/IBackgroundSendStrategy.cs | 3 ++- .../Offline/OfflineStoreBase.cs | 2 +- .../Offline/TimerBasedSendStrategy.cs | 21 ++++++++++++++++--- 3 files changed, 21 insertions(+), 5 deletions(-) diff --git a/Mindscape.Raygun4Net.NetCore.Common/Offline/IBackgroundSendStrategy.cs b/Mindscape.Raygun4Net.NetCore.Common/Offline/IBackgroundSendStrategy.cs index 2ca72dd4..8dc4196b 100644 --- a/Mindscape.Raygun4Net.NetCore.Common/Offline/IBackgroundSendStrategy.cs +++ b/Mindscape.Raygun4Net.NetCore.Common/Offline/IBackgroundSendStrategy.cs @@ -1,10 +1,11 @@ using System; +using System.Threading.Tasks; namespace Mindscape.Raygun4Net.Offline; public interface IBackgroundSendStrategy : IDisposable { - public event Action OnSend; + public event Func OnSendAsync; public void Start(); public void Stop(); } \ No newline at end of file diff --git a/Mindscape.Raygun4Net.NetCore.Common/Offline/OfflineStoreBase.cs b/Mindscape.Raygun4Net.NetCore.Common/Offline/OfflineStoreBase.cs index b2a986e1..4fefc7a8 100644 --- a/Mindscape.Raygun4Net.NetCore.Common/Offline/OfflineStoreBase.cs +++ b/Mindscape.Raygun4Net.NetCore.Common/Offline/OfflineStoreBase.cs @@ -16,7 +16,7 @@ public abstract class OfflineStoreBase protected OfflineStoreBase(IBackgroundSendStrategy backgroundSendStrategy) { _backgroundSendStrategy = backgroundSendStrategy; - _backgroundSendStrategy.OnSend += async () => await ProcessOfflineCrashReports(); + _backgroundSendStrategy.OnSendAsync += ProcessOfflineCrashReports; } public virtual void SetSendCallback(SendHandler sendHandler) diff --git a/Mindscape.Raygun4Net.NetCore.Common/Offline/TimerBasedSendStrategy.cs b/Mindscape.Raygun4Net.NetCore.Common/Offline/TimerBasedSendStrategy.cs index e60a013a..06b9b57b 100644 --- a/Mindscape.Raygun4Net.NetCore.Common/Offline/TimerBasedSendStrategy.cs +++ b/Mindscape.Raygun4Net.NetCore.Common/Offline/TimerBasedSendStrategy.cs @@ -1,5 +1,7 @@ using System; +using System.Linq; using System.Threading; +using System.Threading.Tasks; namespace Mindscape.Raygun4Net.Offline; @@ -8,7 +10,7 @@ public class TimerBasedSendStrategy : IBackgroundSendStrategy private static readonly TimeSpan DefaultInternal = TimeSpan.FromSeconds(30); private readonly Timer _backgroundTimer; - public event Action OnSend; + public event Func OnSendAsync; public TimeSpan Interval { get; } @@ -16,6 +18,7 @@ public TimerBasedSendStrategy(TimeSpan? interval = null) { Interval = interval ?? DefaultInternal; _backgroundTimer = new Timer(SendOfflineErrors); + Start(); } ~TimerBasedSendStrategy() @@ -23,9 +26,21 @@ public TimerBasedSendStrategy(TimeSpan? interval = null) Dispose(); } - private void SendOfflineErrors(object state) + private async void SendOfflineErrors(object state) { - OnSend?.Invoke(); + try + { + var invocationList = OnSendAsync?.GetInvocationList(); + if (invocationList != null) + { + var tasks = invocationList.OfType>().Select(handler => handler()); + await Task.WhenAll(tasks); + } + } + finally + { + Start(); + } } public void Start() From 690c7057a425b3c513ac5060a7478242cd81ae1b Mon Sep 17 00:00:00 2001 From: Sean Date: Thu, 30 May 2024 13:48:42 +1200 Subject: [PATCH 17/21] Add boolean to prevent re-saving failed offline errors --- .../RaygunClientBase.cs | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/Mindscape.Raygun4Net.NetCore.Common/RaygunClientBase.cs b/Mindscape.Raygun4Net.NetCore.Common/RaygunClientBase.cs index 433d890e..1149eea1 100644 --- a/Mindscape.Raygun4Net.NetCore.Common/RaygunClientBase.cs +++ b/Mindscape.Raygun4Net.NetCore.Common/RaygunClientBase.cs @@ -135,7 +135,7 @@ protected RaygunClientBase(RaygunSettingsBase settings, HttpClient client, IRayg if (settings.OfflineStore != null) { - settings.OfflineStore.SetSendCallback(SendPayloadAsync); + settings.OfflineStore.SetSendCallback(SendOfflinePayloadAsync); } } @@ -511,7 +511,7 @@ public async Task Send(RaygunMessage raygunMessage, CancellationToken cancellati try { var messagePayload = SimpleJson.SerializeObject(raygunMessage); - await SendPayloadAsync(messagePayload, _settings.ApiKey, cancellationToken); + await SendPayloadAsync(messagePayload, _settings.ApiKey, true, cancellationToken); } catch (Exception ex) { @@ -522,7 +522,12 @@ public async Task Send(RaygunMessage raygunMessage, CancellationToken cancellati } } - internal async Task SendPayloadAsync(string payload, string apiKey, CancellationToken cancellationToken) + private async Task SendOfflinePayloadAsync(string payload, string apiKey, CancellationToken cancellationToken) + { + await SendPayloadAsync(payload, apiKey, false, cancellationToken); + } + + private async Task SendPayloadAsync(string payload, string apiKey, bool useOfflineStore, CancellationToken cancellationToken) { HttpResponseMessage response = null; var requestMessage = new HttpRequestMessage(HttpMethod.Post, _settings.ApiEndpoint); @@ -544,7 +549,7 @@ internal async Task SendPayloadAsync(string payload, string apiKey, Cancellation // we get no response if the send call fails for any other reason (network etc) var shouldStoreMessage = response is null || response.StatusCode >= HttpStatusCode.InternalServerError; - if (shouldStoreMessage) + if (useOfflineStore && shouldStoreMessage) { await SaveMessageToOfflineCache(payload, apiKey, cancellationToken); } From 7a8d20576e7fb0c246e8f8e03220db7b0763e0fe Mon Sep 17 00:00:00 2001 From: Sean Date: Thu, 30 May 2024 14:01:43 +1200 Subject: [PATCH 18/21] Attempt to fix the env var tests --- Mindscape.Raygun4Net.NetCore.Tests/RaygunMessageBuilderTests.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Mindscape.Raygun4Net.NetCore.Tests/RaygunMessageBuilderTests.cs b/Mindscape.Raygun4Net.NetCore.Tests/RaygunMessageBuilderTests.cs index 81b02e8f..0c03b42d 100644 --- a/Mindscape.Raygun4Net.NetCore.Tests/RaygunMessageBuilderTests.cs +++ b/Mindscape.Raygun4Net.NetCore.Tests/RaygunMessageBuilderTests.cs @@ -5,6 +5,7 @@ namespace Mindscape.Raygun4Net.NetCore.Tests { [TestFixture] + [NonParallelizable] public class RaygunMessageBuilderTests { private RaygunSettings _settings; From 1ce8541c511b7c19731bdd2102d3894db2416abb Mon Sep 17 00:00:00 2001 From: Sean Date: Thu, 30 May 2024 14:09:05 +1200 Subject: [PATCH 19/21] Reset the last updated on each test --- .../RaygunMessageBuilderTests.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Mindscape.Raygun4Net.NetCore.Tests/RaygunMessageBuilderTests.cs b/Mindscape.Raygun4Net.NetCore.Tests/RaygunMessageBuilderTests.cs index 0c03b42d..89dada8f 100644 --- a/Mindscape.Raygun4Net.NetCore.Tests/RaygunMessageBuilderTests.cs +++ b/Mindscape.Raygun4Net.NetCore.Tests/RaygunMessageBuilderTests.cs @@ -5,7 +5,6 @@ namespace Mindscape.Raygun4Net.NetCore.Tests { [TestFixture] - [NonParallelizable] public class RaygunMessageBuilderTests { private RaygunSettings _settings; @@ -213,6 +212,8 @@ public void SetEnvironmentDetails_WithEnvironmentVariables_Contains() "*_Banana*" } }; + + RaygunEnvironmentMessageBuilder.LastUpdate = DateTime.MinValue; var builder = RaygunMessageBuilder.New(settings) .SetEnvironmentDetails(); @@ -243,6 +244,8 @@ public void SetEnvironmentDetails_WithEnvironmentVariables_Star_ShouldReturnNoth "* *", } }; + + RaygunEnvironmentMessageBuilder.LastUpdate = DateTime.MinValue; var builder = RaygunMessageBuilder.New(settings) .SetEnvironmentDetails(); @@ -270,6 +273,8 @@ public void SetEnvironmentDetails_WithEnvironmentVariablesWithDifferentCasing_Sh search } }; + + RaygunEnvironmentMessageBuilder.LastUpdate = DateTime.MinValue; var builder = RaygunMessageBuilder.New(settings) .SetEnvironmentDetails(); From a7940e0ae049ad709a83b9233ed687902d5fb7b8 Mon Sep 17 00:00:00 2001 From: Sean Date: Tue, 4 Jun 2024 10:15:26 +1200 Subject: [PATCH 20/21] Add nullables --- .../Offline/OfflineStoreBase.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/Mindscape.Raygun4Net.NetCore.Common/Offline/OfflineStoreBase.cs b/Mindscape.Raygun4Net.NetCore.Common/Offline/OfflineStoreBase.cs index 4fefc7a8..9ef5b71d 100644 --- a/Mindscape.Raygun4Net.NetCore.Common/Offline/OfflineStoreBase.cs +++ b/Mindscape.Raygun4Net.NetCore.Common/Offline/OfflineStoreBase.cs @@ -1,3 +1,4 @@ +#nullable enable using System; using System.Collections.Generic; using System.Diagnostics; @@ -11,11 +12,11 @@ namespace Mindscape.Raygun4Net.Offline; public abstract class OfflineStoreBase { private readonly IBackgroundSendStrategy _backgroundSendStrategy; - protected SendHandler SendCallback { get; set; } + protected SendHandler? SendCallback { get; set; } protected OfflineStoreBase(IBackgroundSendStrategy backgroundSendStrategy) { - _backgroundSendStrategy = backgroundSendStrategy; + _backgroundSendStrategy = backgroundSendStrategy ?? throw new ArgumentNullException(nameof(backgroundSendStrategy)); _backgroundSendStrategy.OnSendAsync += ProcessOfflineCrashReports; } @@ -26,6 +27,9 @@ public virtual void SetSendCallback(SendHandler sendHandler) protected virtual async Task ProcessOfflineCrashReports() { + if (SendCallback is null) + return; + try { var cachedCrashReports = await GetAll(CancellationToken.None); From 0074decb23c64d893569641b25bfdcf615b799ce Mon Sep 17 00:00:00 2001 From: Sean Date: Wed, 5 Jun 2024 11:13:28 +1200 Subject: [PATCH 21/21] pr feedbacks --- .../Offline/OfflineStoreBase.cs | 6 ++++-- .../Offline/TimerBasedSendStrategy.cs | 2 ++ .../RaygunClientBase.cs | 13 ++++--------- Mindscape.Raygun4Net.NetCore/RaygunClient.cs | 11 +++++------ .../Storage/LocalApplicationDataCrashReportStore.cs | 5 ++--- 5 files changed, 17 insertions(+), 20 deletions(-) diff --git a/Mindscape.Raygun4Net.NetCore.Common/Offline/OfflineStoreBase.cs b/Mindscape.Raygun4Net.NetCore.Common/Offline/OfflineStoreBase.cs index 9ef5b71d..13359e7a 100644 --- a/Mindscape.Raygun4Net.NetCore.Common/Offline/OfflineStoreBase.cs +++ b/Mindscape.Raygun4Net.NetCore.Common/Offline/OfflineStoreBase.cs @@ -1,4 +1,4 @@ -#nullable enable +#nullable enable using System; using System.Collections.Generic; using System.Diagnostics; @@ -28,8 +28,10 @@ public virtual void SetSendCallback(SendHandler sendHandler) protected virtual async Task ProcessOfflineCrashReports() { if (SendCallback is null) + { return; - + } + try { var cachedCrashReports = await GetAll(CancellationToken.None); diff --git a/Mindscape.Raygun4Net.NetCore.Common/Offline/TimerBasedSendStrategy.cs b/Mindscape.Raygun4Net.NetCore.Common/Offline/TimerBasedSendStrategy.cs index 06b9b57b..3f6340a8 100644 --- a/Mindscape.Raygun4Net.NetCore.Common/Offline/TimerBasedSendStrategy.cs +++ b/Mindscape.Raygun4Net.NetCore.Common/Offline/TimerBasedSendStrategy.cs @@ -45,6 +45,8 @@ private async void SendOfflineErrors(object state) public void Start() { + // This sets the timer to trigger once at the interval, and then "never again". + // This inherently prevents the timer from being re-entrant _backgroundTimer.Change(Interval, TimeSpan.FromMilliseconds(int.MaxValue)); } diff --git a/Mindscape.Raygun4Net.NetCore.Common/RaygunClientBase.cs b/Mindscape.Raygun4Net.NetCore.Common/RaygunClientBase.cs index 1149eea1..a29da438 100644 --- a/Mindscape.Raygun4Net.NetCore.Common/RaygunClientBase.cs +++ b/Mindscape.Raygun4Net.NetCore.Common/RaygunClientBase.cs @@ -271,12 +271,7 @@ protected async Task OnCustomGroupingKey(Exception exception, RaygunMess protected bool ValidateApiKey() { - return ValidateApiKey(_settings.ApiKey); - } - - private bool ValidateApiKey(string apiKey) - { - if (string.IsNullOrEmpty(apiKey)) + if (string.IsNullOrEmpty(_settings.ApiKey)) { Debug.WriteLine("ApiKey has not been provided, exception will not be logged"); return false; @@ -449,8 +444,7 @@ protected virtual async Task StripAndSend(Exception exception, IList tag protected virtual IEnumerable StripWrapperExceptions(Exception exception) { - if (exception != null && _wrapperExceptions.Any(wrapperException => - exception.GetType() == wrapperException && exception.InnerException != null)) + if (exception != null && _wrapperExceptions.Any(wrapperException => exception.GetType() == wrapperException && exception.InnerException != null)) { var aggregate = exception as AggregateException; @@ -546,7 +540,8 @@ private async Task SendPayloadAsync(string payload, string apiKey, bool useOffli Debug.WriteLine($"Error Logging Exception to Raygun: {ex.Message}"); // If we got no response or an unexpected server error then add it to offline storage to send later - // we get no response if the send call fails for any other reason (network etc) + // we get no response if the send call fails for any other reason (network etc.) + // checking that response.StatusCode >= 500, is an efficient check for any server errors var shouldStoreMessage = response is null || response.StatusCode >= HttpStatusCode.InternalServerError; if (useOfflineStore && shouldStoreMessage) diff --git a/Mindscape.Raygun4Net.NetCore/RaygunClient.cs b/Mindscape.Raygun4Net.NetCore/RaygunClient.cs index a9242cd3..8005318a 100644 --- a/Mindscape.Raygun4Net.NetCore/RaygunClient.cs +++ b/Mindscape.Raygun4Net.NetCore/RaygunClient.cs @@ -1,6 +1,5 @@ using System; using System.Net.Http; -using Mindscape.Raygun4Net.Storage; namespace Mindscape.Raygun4Net; @@ -10,19 +9,19 @@ public class RaygunClient : RaygunClientBase public RaygunClient(string apiKey) : base(new RaygunSettings { ApiKey = apiKey }) { } - + [Obsolete("Use the RaygunClient(RaygunSettings, HttpClient) constructor instead")] public RaygunClient(string apiKey, HttpClient httpClient) : base(new RaygunSettings { ApiKey = apiKey }, httpClient) { } - + // ReSharper disable MemberCanBeProtected.Global // ReSharper disable SuggestBaseTypeForParameterInConstructor // ReSharper disable UnusedMember.Global public RaygunClient(RaygunSettings settings) : base(settings) { } - + public RaygunClient(RaygunSettings settings, HttpClient httpClient) : base(settings, httpClient) { } @@ -30,11 +29,11 @@ public RaygunClient(RaygunSettings settings, HttpClient httpClient) : base(setti public RaygunClient(RaygunSettings settings, IRaygunUserProvider userProvider) : base(settings, userProvider) { } - + public RaygunClient(RaygunSettings settings, HttpClient httpClient, IRaygunUserProvider userProvider) : base(settings, httpClient, userProvider) { } - + // ReSharper restore MemberCanBeProtected.Global // ReSharper restore SuggestBaseTypeForParameterInConstructor // ReSharper restore UnusedMember.Global diff --git a/Mindscape.Raygun4Net.NetCore/Storage/LocalApplicationDataCrashReportStore.cs b/Mindscape.Raygun4Net.NetCore/Storage/LocalApplicationDataCrashReportStore.cs index 35de5986..49a28709 100644 --- a/Mindscape.Raygun4Net.NetCore/Storage/LocalApplicationDataCrashReportStore.cs +++ b/Mindscape.Raygun4Net.NetCore/Storage/LocalApplicationDataCrashReportStore.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.IO; using System.Reflection; using System.Security.Cryptography; @@ -21,11 +20,11 @@ public LocalApplicationDataCrashReportStore(IBackgroundSendStrategy backgroundSe private static string GetLocalAppDirectory(string directoryName) { - directoryName ??= CreateUniqueDirectory(); + directoryName ??= CreateUniqueDirectoryName(); return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), directoryName); } - private static string CreateUniqueDirectory() + private static string CreateUniqueDirectoryName() { // Try to generate a unique id, from the executable location var uniqueId = Assembly.GetEntryAssembly()?.Location ?? throw new ApplicationException("Cannot determine unique application id");