diff --git a/src/Echo/src/EchoTranslationEngine/Usings.cs b/src/Echo/src/EchoTranslationEngine/Usings.cs
index 0404305b..c02c0f22 100644
--- a/src/Echo/src/EchoTranslationEngine/Usings.cs
+++ b/src/Echo/src/EchoTranslationEngine/Usings.cs
@@ -1,5 +1,4 @@
global using System.Threading.Channels;
-global using Bugsnag.AspNet.Core;
global using EchoTranslationEngine;
global using Google.Protobuf.WellKnownTypes;
global using Grpc.Core;
diff --git a/src/Machine/src/Serval.Machine.EngineServer/Program.cs b/src/Machine/src/Serval.Machine.EngineServer/Program.cs
index b03f6575..72a62b25 100644
--- a/src/Machine/src/Serval.Machine.EngineServer/Program.cs
+++ b/src/Machine/src/Serval.Machine.EngineServer/Program.cs
@@ -1,4 +1,3 @@
-using Bugsnag.AspNet.Core;
using Hangfire;
using OpenTelemetry.Trace;
diff --git a/src/Machine/src/Serval.Machine.JobServer/Program.cs b/src/Machine/src/Serval.Machine.JobServer/Program.cs
index 99388a62..f1ad384d 100644
--- a/src/Machine/src/Serval.Machine.JobServer/Program.cs
+++ b/src/Machine/src/Serval.Machine.JobServer/Program.cs
@@ -1,4 +1,3 @@
-using Bugsnag.AspNet.Core;
using OpenTelemetry.Trace;
var builder = WebApplication.CreateBuilder(args);
diff --git a/src/Machine/src/Serval.Machine.Shared/Utils/AsyncTimer.cs b/src/Machine/src/Serval.Machine.Shared/Utils/AsyncTimer.cs
index 711826d5..86bcbe75 100644
--- a/src/Machine/src/Serval.Machine.Shared/Utils/AsyncTimer.cs
+++ b/src/Machine/src/Serval.Machine.Shared/Utils/AsyncTimer.cs
@@ -57,6 +57,7 @@ private void StopTimer()
protected override async ValueTask DisposeAsyncCore()
{
+ await base.DisposeAsyncCore();
await StopAsync();
_timer.Dispose();
}
diff --git a/src/Serval/src/Serval.ApiServer/Serval.ApiServer.csproj b/src/Serval/src/Serval.ApiServer/Serval.ApiServer.csproj
index 3ea7d09f..7089533f 100644
--- a/src/Serval/src/Serval.ApiServer/Serval.ApiServer.csproj
+++ b/src/Serval/src/Serval.ApiServer/Serval.ApiServer.csproj
@@ -30,12 +30,12 @@
runtime; build; native; contentfiles; analyzers; buildtransitive
-
-
-
-
-
-
+
+
+
+
+
+
diff --git a/src/Serval/src/Serval.ApiServer/Usings.cs b/src/Serval/src/Serval.ApiServer/Usings.cs
index 13f508d8..8f4b6446 100644
--- a/src/Serval/src/Serval.ApiServer/Usings.cs
+++ b/src/Serval/src/Serval.ApiServer/Usings.cs
@@ -2,7 +2,6 @@
global using System.Security.Claims;
global using System.Text.Json.Serialization;
global using Asp.Versioning;
-global using Bugsnag.AspNet.Core;
global using Hangfire;
global using Hangfire.Mongo;
global using Hangfire.Mongo.Migration.Strategies;
diff --git a/src/Serval/src/Serval.Assessment/Serval.Assessment.csproj b/src/Serval/src/Serval.Assessment/Serval.Assessment.csproj
index 81b1b5a4..692caedf 100644
--- a/src/Serval/src/Serval.Assessment/Serval.Assessment.csproj
+++ b/src/Serval/src/Serval.Assessment/Serval.Assessment.csproj
@@ -14,7 +14,7 @@
-
+
diff --git a/src/Serval/src/Serval.DataFiles/Serval.DataFiles.csproj b/src/Serval/src/Serval.DataFiles/Serval.DataFiles.csproj
index 146b0824..efc26d52 100644
--- a/src/Serval/src/Serval.DataFiles/Serval.DataFiles.csproj
+++ b/src/Serval/src/Serval.DataFiles/Serval.DataFiles.csproj
@@ -13,7 +13,7 @@
-
+
diff --git a/src/Serval/src/Serval.Translation/Serval.Translation.csproj b/src/Serval/src/Serval.Translation/Serval.Translation.csproj
index 81b1b5a4..692caedf 100644
--- a/src/Serval/src/Serval.Translation/Serval.Translation.csproj
+++ b/src/Serval/src/Serval.Translation/Serval.Translation.csproj
@@ -14,7 +14,7 @@
-
+
diff --git a/src/Serval/src/Serval.Webhooks/Serval.Webhooks.csproj b/src/Serval/src/Serval.Webhooks/Serval.Webhooks.csproj
index 4f9fa6d8..bb1af79a 100644
--- a/src/Serval/src/Serval.Webhooks/Serval.Webhooks.csproj
+++ b/src/Serval/src/Serval.Webhooks/Serval.Webhooks.csproj
@@ -13,7 +13,7 @@
-
+
diff --git a/src/ServiceToolkit/src/SIL.ServiceToolkit/Configuration/IServiceCollectionsExtensions.cs b/src/ServiceToolkit/src/SIL.ServiceToolkit/Configuration/IServiceCollectionsExtensions.cs
index d5a6424f..db6f1c01 100644
--- a/src/ServiceToolkit/src/SIL.ServiceToolkit/Configuration/IServiceCollectionsExtensions.cs
+++ b/src/ServiceToolkit/src/SIL.ServiceToolkit/Configuration/IServiceCollectionsExtensions.cs
@@ -8,4 +8,25 @@ public static IServiceCollection AddParallelCorpusPreprocessor(this IServiceColl
services.AddSingleton();
return services;
}
+
+ ///
+ /// Add Bugsnag to your application. Configures the required bugsnag
+ /// services and attaches the Bugsnag middleware to catch unhandled
+ /// exceptions.
+ ///
+ ///
+ ///
+ public static IServiceCollection AddBugsnag(this IServiceCollection services)
+ {
+ services.TryAddSingleton();
+
+ return services
+ .AddSingleton()
+ .AddScoped(context =>
+ {
+ var configuration = context.GetService>();
+ var client = new Bugsnag.Client(configuration!.Value);
+ return client;
+ });
+ }
}
diff --git a/src/ServiceToolkit/src/SIL.ServiceToolkit/SIL.ServiceToolkit.csproj b/src/ServiceToolkit/src/SIL.ServiceToolkit/SIL.ServiceToolkit.csproj
index ced38ebc..3c5964fe 100644
--- a/src/ServiceToolkit/src/SIL.ServiceToolkit/SIL.ServiceToolkit.csproj
+++ b/src/ServiceToolkit/src/SIL.ServiceToolkit/SIL.ServiceToolkit.csproj
@@ -11,12 +11,13 @@
-
+
+
-
+
diff --git a/src/ServiceToolkit/src/SIL.ServiceToolkit/Services/BugsnagMiddleware.cs b/src/ServiceToolkit/src/SIL.ServiceToolkit/Services/BugsnagMiddleware.cs
new file mode 100644
index 00000000..660787c8
--- /dev/null
+++ b/src/ServiceToolkit/src/SIL.ServiceToolkit/Services/BugsnagMiddleware.cs
@@ -0,0 +1,60 @@
+namespace SIL.ServiceToolkit.Services;
+
+///
+/// The Bugsnag AspNetCore middleware.
+///
+public class BugsnagMiddleware(RequestDelegate requestDelegate)
+{
+ public const string HttpContextItemsKey = "Bugsnag.Client";
+
+ private readonly RequestDelegate _next = requestDelegate;
+
+ public async Task Invoke(HttpContext context, Bugsnag.IClient client)
+ {
+ if (client.Configuration.AutoCaptureSessions)
+ client.SessionTracking.CreateSession();
+
+ // capture the request information now as the http context
+ // may be changed by other error handlers after an exception
+ // has occurred
+ Bugsnag.Payload.Request bugsnagRequestInformation = ToRequest(context);
+
+ client.BeforeNotify(report =>
+ {
+ report.Event.Request = bugsnagRequestInformation;
+ });
+
+ context.Items[HttpContextItemsKey] = client;
+
+ if (client.Configuration.AutoNotify)
+ {
+ try
+ {
+ await _next(context);
+ }
+ catch (Exception exception)
+ {
+ client.Notify(exception, Bugsnag.Payload.HandledState.ForUnhandledException());
+ throw;
+ }
+ }
+ else
+ {
+ await _next(context);
+ }
+ }
+
+ private static Bugsnag.Payload.Request ToRequest(HttpContext httpContext)
+ {
+ IPAddress? ip = httpContext.Connection.RemoteIpAddress ?? httpContext.Connection.LocalIpAddress;
+
+ return new Bugsnag.Payload.Request
+ {
+ ClientIp = ip?.ToString(),
+ Headers = httpContext.Request.Headers.ToDictionary(x => x.Key, x => string.Join(",", x.Value!)),
+ HttpMethod = httpContext.Request.Method,
+ Url = httpContext.Request.GetDisplayUrl(),
+ Referer = httpContext.Request.Headers[HeaderNames.Referer],
+ };
+ }
+}
diff --git a/src/ServiceToolkit/src/SIL.ServiceToolkit/Services/BugsnagStartupFilter.cs b/src/ServiceToolkit/src/SIL.ServiceToolkit/Services/BugsnagStartupFilter.cs
new file mode 100644
index 00000000..11b705e0
--- /dev/null
+++ b/src/ServiceToolkit/src/SIL.ServiceToolkit/Services/BugsnagStartupFilter.cs
@@ -0,0 +1,71 @@
+namespace SIL.ServiceToolkit.Services;
+
+///
+/// A startup filter to ensure that the Bugsnag middleware is
+/// executed at the start of the middleware stack.
+///
+public class BugsnagStartupFilter : IStartupFilter
+{
+ static BugsnagStartupFilter()
+ {
+ // populate the env variable that the client expects with the netcore
+ // provided value unless it has already been specified
+ if (Environment.GetEnvironmentVariable("BUGSNAG_RELEASE_STAGE") == null)
+ {
+ Environment.SetEnvironmentVariable(
+ "BUGSNAG_RELEASE_STAGE",
+ Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT")
+ );
+ }
+ }
+
+ public Action Configure(Action next)
+ {
+ return builder =>
+ {
+ builder
+ .ApplicationServices.GetService()
+ ?.SubscribeWithAdapter(new DiagnosticSubscriber());
+ builder.UseMiddleware();
+ next(builder);
+ };
+ }
+
+ private class DiagnosticSubscriber
+ {
+ ///
+ /// Handles exceptions that the Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddleware
+ /// swallows.
+ ///
+ ///
+ ///
+ [DiagnosticName("Microsoft.AspNetCore.Diagnostics.HandledException")]
+ public virtual void OnHandledException(Exception exception, HttpContext httpContext)
+ {
+ LogException(exception, httpContext);
+ }
+
+ ///
+ /// Handles exceptions that the Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware
+ /// swallows.
+ ///
+ ///
+ ///
+ [DiagnosticName("Microsoft.AspNetCore.Diagnostics.UnhandledException")]
+ public virtual void OnUnhandledException(Exception exception, HttpContext httpContext)
+ {
+ LogException(exception, httpContext);
+ }
+
+ private static void LogException(Exception exception, HttpContext httpContext)
+ {
+ httpContext.Items.TryGetValue(BugsnagMiddleware.HttpContextItemsKey, out object? clientObject);
+
+ if (clientObject is Bugsnag.IClient client)
+ {
+ if (client.Configuration.AutoNotify)
+ client.Notify(exception, Bugsnag.Payload.HandledState.ForUnhandledException());
+ }
+ }
+ }
+}
diff --git a/src/ServiceToolkit/src/SIL.ServiceToolkit/Usings.cs b/src/ServiceToolkit/src/SIL.ServiceToolkit/Usings.cs
index a5800d9f..e3d986c6 100644
--- a/src/ServiceToolkit/src/SIL.ServiceToolkit/Usings.cs
+++ b/src/ServiceToolkit/src/SIL.ServiceToolkit/Usings.cs
@@ -1,15 +1,24 @@
-global using System.Diagnostics.CodeAnalysis;
+global using System.Diagnostics;
+global using System.Diagnostics.CodeAnalysis;
+global using System.Net;
global using System.Text;
global using System.Text.Json.Nodes;
global using System.Text.RegularExpressions;
global using Grpc.Core;
global using Grpc.Core.Interceptors;
global using Hangfire;
+global using Microsoft.AspNetCore.Builder;
+global using Microsoft.AspNetCore.Hosting;
+global using Microsoft.AspNetCore.Http;
+global using Microsoft.AspNetCore.Http.Extensions;
global using Microsoft.Extensions.DependencyInjection;
+global using Microsoft.Extensions.DependencyInjection.Extensions;
+global using Microsoft.Extensions.DiagnosticAdapter;
global using Microsoft.Extensions.Diagnostics.HealthChecks;
global using Microsoft.Extensions.Hosting;
global using Microsoft.Extensions.Logging;
global using Microsoft.Extensions.Options;
+global using Microsoft.Net.Http.Headers;
global using SIL.Machine.Corpora;
global using SIL.ServiceToolkit.Models;
global using SIL.ServiceToolkit.Services;