Skip to content

Commit

Permalink
Merge pull request #530 from MindscapeHQ/sean/offline-storage
Browse files Browse the repository at this point in the history
Add support for offline storage of errors to Raygun4Net Core
  • Loading branch information
xenolightning authored Jun 7, 2024
2 parents 6678ade + 0074dec commit bebaf89
Show file tree
Hide file tree
Showing 11 changed files with 474 additions and 94 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using System;

namespace Mindscape.Raygun4Net.Offline;

public sealed class CrashReportStoreEntry
{
/// <summary>
/// Unique ID for the record
/// </summary>
public Guid Id { get; set; }

/// <summary>
/// The application api key that the payload was intended for
/// </summary>
public string ApiKey { get; set; }

/// <summary>
/// The JSON serialized payload of the error
/// </summary>
public string MessagePayload { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using System;
using System.Threading.Tasks;

namespace Mindscape.Raygun4Net.Offline;

public interface IBackgroundSendStrategy : IDisposable
{
public event Func<Task> OnSendAsync;
public void Start();
public void Stop();
}
61 changes: 61 additions & 0 deletions Mindscape.Raygun4Net.NetCore.Common/Offline/OfflineStoreBase.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
#nullable enable
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 ?? throw new ArgumentNullException(nameof(backgroundSendStrategy));
_backgroundSendStrategy.OnSendAsync += ProcessOfflineCrashReports;
}

public virtual void SetSendCallback(SendHandler sendHandler)
{
SendCallback = sendHandler;
}

protected virtual async Task ProcessOfflineCrashReports()
{
if (SendCallback is null)
{
return;
}

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<List<CrashReportStoreEntry>> GetAll(CancellationToken cancellationToken);
public abstract Task<bool> Save(string crashPayload, string apiKey, CancellationToken cancellationToken);
public abstract Task<bool> Remove(Guid cacheEntryId, CancellationToken cancellationToken);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

namespace Mindscape.Raygun4Net.Offline;

public class TimerBasedSendStrategy : IBackgroundSendStrategy
{
private static readonly TimeSpan DefaultInternal = TimeSpan.FromSeconds(30);

private readonly Timer _backgroundTimer;
public event Func<Task> OnSendAsync;

public TimeSpan Interval { get; }

public TimerBasedSendStrategy(TimeSpan? interval = null)
{
Interval = interval ?? DefaultInternal;
_backgroundTimer = new Timer(SendOfflineErrors);
Start();
}

~TimerBasedSendStrategy()
{
Dispose();
}

private async void SendOfflineErrors(object state)
{
try
{
var invocationList = OnSendAsync?.GetInvocationList();
if (invocationList != null)
{
var tasks = invocationList.OfType<Func<Task>>().Select(handler => handler());
await Task.WhenAll(tasks);
}
}
finally
{
Start();
}
}

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));
}

public void Stop()
{
_backgroundTimer.Change(Timeout.Infinite, 0);
}

public void Dispose()
{
_backgroundTimer?.Dispose();
}
}
Loading

0 comments on commit bebaf89

Please sign in to comment.