Skip to content

Stop/resume client on fatal crash #10

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 74 additions & 30 deletions src/AptabaseClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,82 +6,126 @@ namespace Aptabase.Maui;
public class AptabaseClient : IAptabaseClient
{
private readonly Channel<EventData> _channel;
private readonly Task _processingTask;
private readonly AptabaseClientBase _client;
private readonly ILogger<AptabaseClient>? _logger;
private readonly CancellationTokenSource _cts;
private Task? _processingTask;
private CancellationTokenSource? _cts;

public bool IsRunning => _processingTask != null && !_processingTask.IsCompleted;

public AptabaseClient(string appKey, AptabaseOptions? options, ILogger<AptabaseClient>? logger)
{
_client = new AptabaseClientBase(appKey, options, logger);
_channel = Channel.CreateUnbounded<EventData>();
_processingTask = Task.Run(ProcessEventsAsync);
_logger = logger;
_cts = new CancellationTokenSource();

StartAsync();
}

public Task TrackEvent(string eventName, Dictionary<string, object>? props = null)
public Task StartAsync()
{
if (!_channel.Writer.TryWrite(new EventData(eventName, props)))
if (IsRunning)
{
_logger?.LogError("Failed to perform TrackEvent");
return Task.CompletedTask;
}

_cts = new CancellationTokenSource();
_processingTask = ProcessEventsAsync(_cts.Token);

return Task.CompletedTask;
}

private async Task ProcessEventsAsync()
public async Task StopAsync()
{
if (!IsRunning)
{
return;
}

if (_cts != null)
{
await _cts.CancelAsync();
}

try
{
while (await _channel.Reader.WaitToReadAsync())
if (_processingTask != null)
{
if (_cts.IsCancellationRequested)
{
break;
}
await _processingTask;
}
}
catch (OperationCanceledException)
{
// ignore
}
finally
{
_cts?.Dispose();
_cts = null;
_processingTask = null;
}
}

public async Task TrackEventAsync(string eventName, Dictionary<string, object>? props = null, CancellationToken cancellationToken = default)
{
var eventData = new EventData(eventName, props);

try
{
await _channel.Writer.WriteAsync(eventData, cancellationToken);
}
catch (Exception ex)
{
_logger?.LogError(ex, "Failed to perform TrackEvent");
}
}

private async Task ProcessEventsAsync(CancellationToken cancellationToken)
{
try
{
while (await _channel.Reader.WaitToReadAsync(cancellationToken))
{
while (_channel.Reader.TryRead(out var eventData))
{
if (_cts.IsCancellationRequested)
{
break;
}

try
{
await _client.TrackEvent(eventData);
await _client.TrackEventAsync(eventData, cancellationToken);
}
catch (Exception ex)
{
// best effort
_logger?.LogError(ex, "Failed to perform TrackEvent");
}

if (cancellationToken.IsCancellationRequested)
{
break;
}
}

if (cancellationToken.IsCancellationRequested)
{
break;
}
}
}
catch (ChannelClosedException)
{
// ignore
}
catch (OperationCanceledException)
{
// ignore
}
}

public async ValueTask DisposeAsync()
{
try
{
_cts.Cancel();
}
catch { }

_channel.Writer.Complete();

if (_processingTask?.IsCompleted == false)
{
await _processingTask;
}

_cts.Dispose();
await StopAsync();

await _client.DisposeAsync();

Expand Down
6 changes: 3 additions & 3 deletions src/AptabaseClientBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ public AptabaseClientBase(string appKey, AptabaseOptions? options, ILogger? logg
_http.DefaultRequestHeaders.Add("App-Key", appKey);
}

internal async Task TrackEvent(EventData eventData)
internal async Task TrackEventAsync(EventData eventData, CancellationToken cancellationToken)
{
if (_http is null)
{
Expand All @@ -67,7 +67,7 @@ internal async Task TrackEvent(EventData eventData)

var body = JsonContent.Create(eventData);

var response = await _http.PostAsync("/api/v0/event", body);
var response = await _http.PostAsync("/api/v0/event", body, cancellationToken);

if (!response.IsSuccessStatusCode)
{
Expand All @@ -79,7 +79,7 @@ internal async Task TrackEvent(EventData eventData)
response.EnsureSuccessStatusCode();
}

var responseBody = await response.Content.ReadAsStringAsync();
var responseBody = await response.Content.ReadAsStringAsync(CancellationToken.None);

_logger?.LogError("Failed to perform TrackEvent due to {StatusCode} and response body {Body}", response.StatusCode, responseBody);
}
Expand Down
24 changes: 21 additions & 3 deletions src/AptabaseCrashReporter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,19 @@ public class AptabaseCrashReporter
{
private readonly IAptabaseClient _client;
private readonly ILogger<AptabaseCrashReporter>? _logger;
private readonly AptabaseOptions? _options;
private const int _pauseTimeoutSeconds = 30;

#if ANDROID
// the UnhandledExceptionRaiser fires first, but others may fire redundantly soon after
private bool _nativeThrown;
#endif

public AptabaseCrashReporter(IAptabaseClient client, ILogger<AptabaseCrashReporter>? logger)
public AptabaseCrashReporter(IAptabaseClient client, AptabaseOptions? options, ILogger<AptabaseCrashReporter>? logger)
{
_client = client;
_logger = logger;
_options = options;

RegisterUncaughtExceptionHandler();
}
Expand Down Expand Up @@ -61,8 +64,23 @@ private void TrackError(Exception e, string error, DateTime timeStamp, bool fata
string stamp = $"{timeStamp:o}";
int i = 0;

// include additional useful platform info
var di = DeviceInfo.Current;
thing += $" ({di.Platform}{di.VersionString}-{di.Manufacturer}-{di.Idiom}-{di.Model})";

if (fatal && _options?.EnablePersistence == true)
{
_client.StopAsync(); // queue events but don't start sending, to avoid duplicates or errors

Task.Run(async () =>
{
await Task.Delay(_pauseTimeoutSeconds * 1000);
await _client.StartAsync();
});
}

// event 00 is the exception summary
_client.TrackEvent(error, new Dictionary<string, object> { { stamp, $"{i++:00} {thing}" } });
_client.TrackEventAsync(error, new Dictionary<string, object> { { stamp, $"{i++:00} {thing}" } });

// plus any stacktrace, events 01..nn will be sequenced under same stamp
if (string.IsNullOrEmpty(e.StackTrace))
Expand All @@ -73,7 +91,7 @@ private void TrackError(Exception e, string error, DateTime timeStamp, bool fata
{
// elide noisy separators and runtime frames
if (!f.StartsWith("---") && !f.Contains(" System.Runtime."))
_client.TrackEvent(error, new Dictionary<string, object> { { stamp, $"{i++:00} {f}" } });
_client.TrackEventAsync(error, new Dictionary<string, object> { { stamp, $"{i++:00} {f}" } });
}

_logger?.LogError(e, "Tracked error: {ErrorType}", error);
Expand Down
5 changes: 2 additions & 3 deletions src/AptabaseExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ public static class AptabaseExtensions
/// <returns>The <paramref name="builder"/>.</returns>
public static MauiAppBuilder UseAptabase(this MauiAppBuilder builder, string appKey, AptabaseOptions? options = null)
{
builder.Services.AddSingleton<IAptabaseClient>(serviceProvider =>
builder.Services.AddSingleton(serviceProvider =>
{
IAptabaseClient client;
var loggerFactory = serviceProvider.GetRequiredService<ILoggerFactory>();
Expand All @@ -32,9 +32,8 @@ public static MauiAppBuilder UseAptabase(this MauiAppBuilder builder, string app
}

if (options?.EnableCrashReporting == true)

{
_ = new AptabaseCrashReporter(client, loggerFactory.CreateLogger<AptabaseCrashReporter>());
_ = new AptabaseCrashReporter(client, options, loggerFactory.CreateLogger<AptabaseCrashReporter>());
}

return client;
Expand Down
Loading