diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5a1291a5ab..6f9192844a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,9 @@
## Unreleased
+## Features
+
+- Add CaptureLastError as an extension method to the Server class on ASP.NET ([#1411](https://github.com/getsentry/sentry-dotnet/pull/1411))
- Add IsDynamicCode* to events ([#1418](https://github.com/getsentry/sentry-dotnet/pull/1418))
## 3.12.3
diff --git a/src/Sentry.AspNet/SentryHttpServerUtilityExtensions.cs b/src/Sentry.AspNet/SentryHttpServerUtilityExtensions.cs
new file mode 100644
index 0000000000..d0b142bca9
--- /dev/null
+++ b/src/Sentry.AspNet/SentryHttpServerUtilityExtensions.cs
@@ -0,0 +1,30 @@
+using System.Web;
+using Sentry.Extensibility;
+using Sentry.Protocol;
+
+namespace Sentry.AspNet;
+
+///
+/// HttpServerUtility extensions.
+///
+public static class SentryHttpServerUtilityExtensions
+{
+ ///
+ /// Captures the last error from the given HttpServerUtility and sends it to Sentry.
+ ///
+ /// The HttpServerUtility that contains the last error.
+ /// A SentryId.
+ public static SentryId CaptureLastError(this HttpServerUtility server) => server.CaptureLastError(HubAdapter.Instance);
+
+ // for testing
+ internal static SentryId CaptureLastError(this HttpServerUtility server, IHub hub)
+ {
+ if (server.GetLastError() is { } exception)
+ {
+ exception.Data[Mechanism.HandledKey] = false;
+ exception.Data[Mechanism.MechanismKey] = "HttpApplication.Application_Error";
+ return hub.CaptureException(exception);
+ }
+ return SentryId.Empty;
+ }
+}
diff --git a/test/Sentry.AspNet.Tests/ApiApprovalTests.Run.DotNet4_6.verified.txt b/test/Sentry.AspNet.Tests/ApiApprovalTests.Run.DotNet4_6.verified.txt
index 8fdf157305..18dc43c940 100644
--- a/test/Sentry.AspNet.Tests/ApiApprovalTests.Run.DotNet4_6.verified.txt
+++ b/test/Sentry.AspNet.Tests/ApiApprovalTests.Run.DotNet4_6.verified.txt
@@ -10,4 +10,8 @@ namespace Sentry.AspNet
{
public static void AddAspNet(this Sentry.SentryOptions options, Sentry.Extensibility.RequestSize maxRequestBodySize = 0) { }
}
+ public static class SentryHttpServerUtilityExtensions
+ {
+ public static Sentry.SentryId CaptureLastError(this System.Web.HttpServerUtility server) { }
+ }
}
\ No newline at end of file
diff --git a/test/Sentry.AspNet.Tests/SentryHttpServerUtilityExtensionsTests.cs b/test/Sentry.AspNet.Tests/SentryHttpServerUtilityExtensionsTests.cs
new file mode 100644
index 0000000000..1186031c23
--- /dev/null
+++ b/test/Sentry.AspNet.Tests/SentryHttpServerUtilityExtensionsTests.cs
@@ -0,0 +1,57 @@
+using System.Web;
+
+namespace Sentry.AspNet.Tests;
+
+public class SentryHttpServerUtilityExtensionsTests
+{
+ private class Fixture
+ {
+ public SentryId Id { get; set; }
+
+ public IHub GetSut()
+ {
+ Id = SentryId.Create();
+ var hub = Substitute.For();
+ hub.IsEnabled.Returns(true);
+ hub.CaptureEvent(Arg.Any()).Returns(Id);
+ return hub;
+ }
+ }
+
+ private readonly Fixture _fixture = new();
+
+ [Fact]
+ public void CaptureLastError_WithError_UnhandledErrorCaptured()
+ {
+ // Arrange
+ var hub = _fixture.GetSut();
+ var exception = new Exception();
+
+ var context = new HttpContext(new HttpRequest("", "http://test", null), new HttpResponse(new StringWriter()));
+ context.AddError(exception);
+
+ // Act
+ var receivedId = context.Server.CaptureLastError(hub);
+
+ // Assert
+ hub.Received(1).CaptureEvent(Arg.Is(@event => @event.Exception == exception));
+ Assert.False(exception.Data[Mechanism.HandledKey] as bool?);
+ Assert.Equal("HttpApplication.Application_Error", exception.Data[Mechanism.MechanismKey]);
+ Assert.Equal(_fixture.Id, receivedId);
+ }
+
+ [Fact]
+ public void CaptureLastError_WithoutError_DoNothing()
+ {
+ // Arrange
+ var hub = _fixture.GetSut();
+ var context = new HttpContext(new HttpRequest("", "http://test", null), new HttpResponse(new StringWriter()));
+
+ // Act
+ var receivedId = context.Server.CaptureLastError(hub);
+
+ // Assert
+ hub.Received(0).CaptureEvent(Arg.Any());
+ Assert.Equal(SentryId.Empty, receivedId);
+ }
+}