diff --git a/.generated.NoMobile.slnx b/.generated.NoMobile.slnx
index babff9ea4d..1ca7ed8b1d 100644
--- a/.generated.NoMobile.slnx
+++ b/.generated.NoMobile.slnx
@@ -118,6 +118,7 @@
+
@@ -168,6 +169,7 @@
+
diff --git a/README.md b/README.md
index 117721f92b..273606efb2 100644
--- a/README.md
+++ b/README.md
@@ -35,6 +35,7 @@ Sentry SDK for .NET
| **Sentry.OpenTelemetry** | [](https://www.nuget.org/packages/Sentry.OpenTelemetry) | [](https://www.nuget.org/packages/Sentry.OpenTelemetry) | [](https://www.nuget.org/packages/Sentry.OpenTelemetry) | [](https://docs.sentry.io/platforms/dotnet/tracing/instrumentation/opentelemetry/) |
| **Sentry.OpenTelemetry.Exporter** | [](https://www.nuget.org/packages/Sentry.OpenTelemetry.Exporter) | [](https://www.nuget.org/packages/Sentry.OpenTelemetry.Exporter) | [](https://www.nuget.org/packages/Sentry.OpenTelemetry.Exporter) | [](https://docs.sentry.io/platforms/dotnet/tracing/instrumentation/opentelemetry-otlp/) |
| **Sentry.Profiling** | [](https://www.nuget.org/packages/Sentry.Profiling) | [](https://www.nuget.org/packages/Sentry.Profiling) | [](https://www.nuget.org/packages/Sentry.Profiling) | [](https://docs.sentry.io/platforms/dotnet/profiling/) |
+| **Sentry.Quartz** | [](https://www.nuget.org/packages/Sentry.Quartz) | [](https://www.nuget.org/packages/Sentry.Quartz) | [](https://www.nuget.org/packages/Sentry.Quartz) | [](https://docs.sentry.io/platforms/dotnet/crons/quartz/) |
| **Sentry.Serilog** | [](https://www.nuget.org/packages/Serilog) | [](https://www.nuget.org/packages/Sentry.Serilog) | [](https://www.nuget.org/packages/Sentry.Serilog) | [](https://docs.sentry.io/platforms/dotnet/guides/serilog) |
## More Sentry .NET Integrations
diff --git a/Sentry-CI-Build-Linux-NoMobile.slnf b/Sentry-CI-Build-Linux-NoMobile.slnf
index c8f5aecc53..b0c237d9e2 100644
--- a/Sentry-CI-Build-Linux-NoMobile.slnf
+++ b/Sentry-CI-Build-Linux-NoMobile.slnf
@@ -32,6 +32,7 @@
"samples\\Sentry.Samples.OpenTelemetry.AzureFunctions\\Sentry.Samples.OpenTelemetry.AzureFunctions.csproj",
"samples\\Sentry.Samples.OpenTelemetry.Console\\Sentry.Samples.OpenTelemetry.Console.csproj",
"samples\\Sentry.Samples.OpenTelemetry.MongoDB\\Sentry.Samples.OpenTelemetry.MongoDB.csproj",
+ "samples\\Sentry.Samples.Quartz\\Sentry.Samples.Quartz.csproj",
"samples\\Sentry.Samples.Serilog\\Sentry.Samples.Serilog.csproj",
"src\\Sentry.Analyzers\\Sentry.Analyzers.csproj",
"src\\Sentry.AspNetCore.Blazor.WebAssembly\\Sentry.AspNetCore.Blazor.WebAssembly.csproj",
@@ -49,6 +50,7 @@
"src\\Sentry.OpenTelemetry.Exporter\\Sentry.OpenTelemetry.Exporter.csproj",
"src\\Sentry.OpenTelemetry\\Sentry.OpenTelemetry.csproj",
"src\\Sentry.Profiling\\Sentry.Profiling.csproj",
+ "src\\Sentry.Quartz\\Sentry.Quartz.csproj",
"src\\Sentry.Serilog\\Sentry.Serilog.csproj",
"src\\Sentry\\Sentry.csproj",
"test\\Sentry.Analyzers.Tests\\Sentry.Analyzers.Tests.csproj",
diff --git a/Sentry-CI-Build-Linux.slnf b/Sentry-CI-Build-Linux.slnf
index 3025600da5..403d25eb11 100644
--- a/Sentry-CI-Build-Linux.slnf
+++ b/Sentry-CI-Build-Linux.slnf
@@ -34,6 +34,7 @@
"samples\\Sentry.Samples.OpenTelemetry.AzureFunctions\\Sentry.Samples.OpenTelemetry.AzureFunctions.csproj",
"samples\\Sentry.Samples.OpenTelemetry.Console\\Sentry.Samples.OpenTelemetry.Console.csproj",
"samples\\Sentry.Samples.OpenTelemetry.MongoDB\\Sentry.Samples.OpenTelemetry.MongoDB.csproj",
+ "samples\\Sentry.Samples.Quartz\\Sentry.Samples.Quartz.csproj",
"samples\\Sentry.Samples.Serilog\\Sentry.Samples.Serilog.csproj",
"src\\Sentry.Analyzers\\Sentry.Analyzers.csproj",
"src\\Sentry.Android.AssemblyReader\\Sentry.Android.AssemblyReader.csproj",
@@ -55,6 +56,7 @@
"src\\Sentry.OpenTelemetry.Exporter\\Sentry.OpenTelemetry.Exporter.csproj",
"src\\Sentry.OpenTelemetry\\Sentry.OpenTelemetry.csproj",
"src\\Sentry.Profiling\\Sentry.Profiling.csproj",
+ "src\\Sentry.Quartz\\Sentry.Quartz.csproj",
"src\\Sentry.Serilog\\Sentry.Serilog.csproj",
"src\\Sentry\\Sentry.csproj",
"test\\Sentry.Analyzers.Tests\\Sentry.Analyzers.Tests.csproj",
diff --git a/Sentry-CI-Build-Windows-arm64.slnf b/Sentry-CI-Build-Windows-arm64.slnf
index b2ecf8df01..6cda378799 100644
--- a/Sentry-CI-Build-Windows-arm64.slnf
+++ b/Sentry-CI-Build-Windows-arm64.slnf
@@ -35,6 +35,7 @@
"samples\\Sentry.Samples.OpenTelemetry.AzureFunctions\\Sentry.Samples.OpenTelemetry.AzureFunctions.csproj",
"samples\\Sentry.Samples.OpenTelemetry.Console\\Sentry.Samples.OpenTelemetry.Console.csproj",
"samples\\Sentry.Samples.OpenTelemetry.MongoDB\\Sentry.Samples.OpenTelemetry.MongoDB.csproj",
+ "samples\\Sentry.Samples.Quartz\\Sentry.Samples.Quartz.csproj",
"samples\\Sentry.Samples.Serilog\\Sentry.Samples.Serilog.csproj",
"src\\Sentry.Analyzers\\Sentry.Analyzers.csproj",
"src\\Sentry.Android.AssemblyReader\\Sentry.Android.AssemblyReader.csproj",
@@ -57,6 +58,7 @@
"src\\Sentry.OpenTelemetry.Exporter\\Sentry.OpenTelemetry.Exporter.csproj",
"src\\Sentry.OpenTelemetry\\Sentry.OpenTelemetry.csproj",
"src\\Sentry.Profiling\\Sentry.Profiling.csproj",
+ "src\\Sentry.Quartz\\Sentry.Quartz.csproj",
"src\\Sentry.Serilog\\Sentry.Serilog.csproj",
"src\\Sentry\\Sentry.csproj",
"test\\Sentry.Analyzers.Tests\\Sentry.Analyzers.Tests.csproj",
diff --git a/Sentry-CI-Build-Windows.slnf b/Sentry-CI-Build-Windows.slnf
index 528d7817dd..46f97c15df 100644
--- a/Sentry-CI-Build-Windows.slnf
+++ b/Sentry-CI-Build-Windows.slnf
@@ -35,6 +35,7 @@
"samples\\Sentry.Samples.OpenTelemetry.AzureFunctions\\Sentry.Samples.OpenTelemetry.AzureFunctions.csproj",
"samples\\Sentry.Samples.OpenTelemetry.Console\\Sentry.Samples.OpenTelemetry.Console.csproj",
"samples\\Sentry.Samples.OpenTelemetry.MongoDB\\Sentry.Samples.OpenTelemetry.MongoDB.csproj",
+ "samples\\Sentry.Samples.Quartz\\Sentry.Samples.Quartz.csproj",
"samples\\Sentry.Samples.Serilog\\Sentry.Samples.Serilog.csproj",
"src\\Sentry.Analyzers\\Sentry.Analyzers.csproj",
"src\\Sentry.Android.AssemblyReader\\Sentry.Android.AssemblyReader.csproj",
@@ -57,6 +58,7 @@
"src\\Sentry.OpenTelemetry.Exporter\\Sentry.OpenTelemetry.Exporter.csproj",
"src\\Sentry.OpenTelemetry\\Sentry.OpenTelemetry.csproj",
"src\\Sentry.Profiling\\Sentry.Profiling.csproj",
+ "src\\Sentry.Quartz\\Sentry.Quartz.csproj",
"src\\Sentry.Serilog\\Sentry.Serilog.csproj",
"src\\Sentry\\Sentry.csproj",
"test\\Sentry.Analyzers.Tests\\Sentry.Analyzers.Tests.csproj",
diff --git a/Sentry-CI-Build-macOS.slnf b/Sentry-CI-Build-macOS.slnf
index f8d2b3ca69..4e9e69d3b8 100644
--- a/Sentry-CI-Build-macOS.slnf
+++ b/Sentry-CI-Build-macOS.slnf
@@ -39,6 +39,7 @@
"samples\\Sentry.Samples.OpenTelemetry.AzureFunctions\\Sentry.Samples.OpenTelemetry.AzureFunctions.csproj",
"samples\\Sentry.Samples.OpenTelemetry.Console\\Sentry.Samples.OpenTelemetry.Console.csproj",
"samples\\Sentry.Samples.OpenTelemetry.MongoDB\\Sentry.Samples.OpenTelemetry.MongoDB.csproj",
+ "samples\\Sentry.Samples.Quartz\\Sentry.Samples.Quartz.csproj",
"samples\\Sentry.Samples.Serilog\\Sentry.Samples.Serilog.csproj",
"src\\Sentry.Analyzers\\Sentry.Analyzers.csproj",
"src\\Sentry.Android.AssemblyReader\\Sentry.Android.AssemblyReader.csproj",
@@ -62,6 +63,7 @@
"src\\Sentry.OpenTelemetry.Exporter\\Sentry.OpenTelemetry.Exporter.csproj",
"src\\Sentry.OpenTelemetry\\Sentry.OpenTelemetry.csproj",
"src\\Sentry.Profiling\\Sentry.Profiling.csproj",
+ "src\\Sentry.Quartz\\Sentry.Quartz.csproj",
"src\\Sentry.Serilog\\Sentry.Serilog.csproj",
"src\\Sentry\\Sentry.csproj",
"test\\Sentry.Analyzers.Tests\\Sentry.Analyzers.Tests.csproj",
diff --git a/Sentry-CI-CodeQL.slnf b/Sentry-CI-CodeQL.slnf
index 647fe0fa2c..b7d1fc7400 100644
--- a/Sentry-CI-CodeQL.slnf
+++ b/Sentry-CI-CodeQL.slnf
@@ -22,6 +22,7 @@
"src\\Sentry.OpenTelemetry.Exporter\\Sentry.OpenTelemetry.Exporter.csproj",
"src\\Sentry.OpenTelemetry\\Sentry.OpenTelemetry.csproj",
"src\\Sentry.Profiling\\Sentry.Profiling.csproj",
+ "src\\Sentry.Quartz\\Sentry.Quartz.csproj",
"src\\Sentry.Serilog\\Sentry.Serilog.csproj",
"src\\Sentry\\Sentry.csproj"
]
diff --git a/Sentry.slnx b/Sentry.slnx
index babff9ea4d..1ca7ed8b1d 100644
--- a/Sentry.slnx
+++ b/Sentry.slnx
@@ -118,6 +118,7 @@
+
@@ -168,6 +169,7 @@
+
diff --git a/SentryAspNetCore.slnf b/SentryAspNetCore.slnf
index ccd8904461..f1e754f58c 100644
--- a/SentryAspNetCore.slnf
+++ b/SentryAspNetCore.slnf
@@ -21,6 +21,7 @@
"src\\Sentry.Extensions.Logging\\Sentry.Extensions.Logging.csproj",
"src\\Sentry.Hangfire\\Sentry.Hangfire.csproj",
"src\\Sentry.OpenTelemetry\\Sentry.OpenTelemetry.csproj",
+ "src\\Sentry.Quartz\\Sentry.Quartz.csproj",
"src\\Sentry.Serilog\\Sentry.Serilog.csproj",
"src\\Sentry\\Sentry.csproj",
"test\\Sentry.Analyzers.Tests\\Sentry.Analyzers.Tests.csproj",
diff --git a/SentryNoMobile.slnf b/SentryNoMobile.slnf
index a6ef473815..845198d90b 100644
--- a/SentryNoMobile.slnf
+++ b/SentryNoMobile.slnf
@@ -33,6 +33,7 @@
"samples\\Sentry.Samples.OpenTelemetry.AzureFunctions\\Sentry.Samples.OpenTelemetry.AzureFunctions.csproj",
"samples\\Sentry.Samples.OpenTelemetry.Console\\Sentry.Samples.OpenTelemetry.Console.csproj",
"samples\\Sentry.Samples.OpenTelemetry.MongoDB\\Sentry.Samples.OpenTelemetry.MongoDB.csproj",
+ "samples\\Sentry.Samples.Quartz\\Sentry.Samples.Quartz.csproj",
"samples\\Sentry.Samples.Serilog\\Sentry.Samples.Serilog.csproj",
"src\\Sentry.Analyzers\\Sentry.Analyzers.csproj",
"src\\Sentry.AspNet\\Sentry.AspNet.csproj",
@@ -51,6 +52,7 @@
"src\\Sentry.OpenTelemetry.Exporter\\Sentry.OpenTelemetry.Exporter.csproj",
"src\\Sentry.OpenTelemetry\\Sentry.OpenTelemetry.csproj",
"src\\Sentry.Profiling\\Sentry.Profiling.csproj",
+ "src\\Sentry.Quartz\\Sentry.Quartz.csproj",
"src\\Sentry.Serilog\\Sentry.Serilog.csproj",
"src\\Sentry\\Sentry.csproj",
"test\\Sentry.Analyzers.Tests\\Sentry.Analyzers.Tests.csproj",
diff --git a/SentryNoSamples.slnf b/SentryNoSamples.slnf
index 7050420d7f..e07c9a652a 100644
--- a/SentryNoSamples.slnf
+++ b/SentryNoSamples.slnf
@@ -24,6 +24,7 @@
"src\\Sentry.OpenTelemetry.Exporter\\Sentry.OpenTelemetry.Exporter.csproj",
"src\\Sentry.OpenTelemetry\\Sentry.OpenTelemetry.csproj",
"src\\Sentry.Profiling\\Sentry.Profiling.csproj",
+ "src\\Sentry.Quartz\\Sentry.Quartz.csproj",
"src\\Sentry.Serilog\\Sentry.Serilog.csproj",
"src\\Sentry\\Sentry.csproj",
"test\\Sentry.Analyzers.Tests\\Sentry.Analyzers.Tests.csproj",
diff --git a/samples/Sentry.Samples.Quartz/Program.cs b/samples/Sentry.Samples.Quartz/Program.cs
new file mode 100644
index 0000000000..8716ad9bf4
--- /dev/null
+++ b/samples/Sentry.Samples.Quartz/Program.cs
@@ -0,0 +1,78 @@
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.Extensions.Hosting;
+using Quartz;
+using Sentry.Quartz;
+
+namespace Sentry.Samples.Quartz;
+
+public static class Program
+{
+ public static void Main(string[] args)
+ {
+ var builder = WebApplication.CreateBuilder(args);
+
+ builder.WebHost.UseSentry();
+
+ builder.Services.AddQuartz(quartz =>
+ {
+ quartz.UseSentry(builder.Services, options =>
+ {
+ var jobKey1 = new JobKey(nameof(FirstJob));
+ quartz.AddJob(opts => opts.WithIdentity(jobKey1));
+ quartz.AddTrigger(opts => opts.ForJob(jobKey1).WithIdentity($"{nameof(FirstJob)}-trigger").WithCronSchedule("*/10 * * ? * *"));
+
+ var jobKey2 = new JobKey(nameof(SecondJob));
+ quartz.AddJob(opts => opts.WithIdentity(jobKey2));
+ quartz.AddTrigger(opts => opts.ForJob(jobKey2).WithIdentity($"{nameof(SecondJob)}-trigger").WithCronSchedule("*/10 * * ? * *"));
+
+ var jobKey3 = new JobKey(nameof(ThirdJob));
+ quartz.AddJob(opts => opts.WithIdentity(jobKey3));
+ quartz.AddTrigger(opts => opts.ForJob(jobKey3).WithIdentity($"{nameof(ThirdJob)}-trigger").WithCronSchedule("*/10 * * ? * *"));
+
+ if (!builder.Environment.IsProduction())
+ {
+ options.EnableUpsertCronMonitor = false;
+ }
+ });
+ }).AddQuartzHostedService();
+
+ var app = builder.Build();
+
+ app.Run();
+ }
+}
+
+[SentryCronMonitorSlug("first-job")]
+public class FirstJob : IJob
+{
+ public async ValueTask Execute(IJobExecutionContext context, CancellationToken cancellationToken)
+ {
+ Console.WriteLine($"Starting to do some heavy work at: {DateTime.Now}");
+ await Task.Delay(1000, cancellationToken);
+ Console.WriteLine($"Finished doing some heavy work at: {DateTime.Now}");
+ }
+}
+
+[SentryCronMonitorSlug("job-that-throws")]
+public class SecondJob : IJob
+{
+ public async ValueTask Execute(IJobExecutionContext context, CancellationToken cancellationToken)
+ {
+ Console.WriteLine($"Starting to do some heavy work at: {DateTime.Now}");
+ await Task.Delay(1000, cancellationToken);
+ Console.WriteLine($"Finished doing some heavy work at: {DateTime.Now}");
+ throw new Exception();
+ }
+}
+
+[SentryCronMonitorSlug("RecurringBackgroundJob")]
+public class ThirdJob : IJob
+{
+ public async ValueTask Execute(IJobExecutionContext context, CancellationToken cancellationToken)
+ {
+ Console.WriteLine($"Starting to do some heavy work at: {DateTime.Now}");
+ await Task.Delay(1000, cancellationToken);
+ Console.WriteLine($"Finished doing some heavy work at: {DateTime.Now}");
+ }
+}
diff --git a/samples/Sentry.Samples.Quartz/Properties/launchSettings.json b/samples/Sentry.Samples.Quartz/Properties/launchSettings.json
new file mode 100644
index 0000000000..1195f60877
--- /dev/null
+++ b/samples/Sentry.Samples.Quartz/Properties/launchSettings.json
@@ -0,0 +1,41 @@
+{
+ "$schema": "http://json.schemastore.org/launchsettings.json",
+ "iisSettings": {
+ "windowsAuthentication": false,
+ "anonymousAuthentication": true,
+ "iisExpress": {
+ "applicationUrl": "http://localhost:5138/",
+ "sslPort": 44386
+ }
+ },
+ "profiles": {
+ "http": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": true,
+ "launchUrl": "hangfire",
+ "applicationUrl": "http://localhost:5138",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ },
+ "https": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": true,
+ "launchUrl": "hangfire",
+ "applicationUrl": "http://localhost:5138",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ },
+ "IIS Express": {
+ "commandName": "IISExpress",
+ "launchBrowser": true,
+ "launchUrl": "hangfire",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ }
+ }
+}
diff --git a/samples/Sentry.Samples.Quartz/README.md b/samples/Sentry.Samples.Quartz/README.md
new file mode 100644
index 0000000000..c2f83967d1
--- /dev/null
+++ b/samples/Sentry.Samples.Quartz/README.md
@@ -0,0 +1,8 @@
+# Sentry for Hangfire
+
+This is a standard ASP.NET Core app with nothing in it but Quartz.
+To run this sample:
+
+```sh
+dotnet run
+```
diff --git a/samples/Sentry.Samples.Quartz/Sentry.Samples.Quartz.csproj b/samples/Sentry.Samples.Quartz/Sentry.Samples.Quartz.csproj
new file mode 100644
index 0000000000..20c2fa35c0
--- /dev/null
+++ b/samples/Sentry.Samples.Quartz/Sentry.Samples.Quartz.csproj
@@ -0,0 +1,24 @@
+
+
+
+ net10.0
+ enable
+ enable
+
+
+
+
+
+
+
+ sentry-sdks
+ sentry-dotnet
+ true
+ true
+
+
+
+
+
+
+
diff --git a/samples/Sentry.Samples.Quartz/appsettings.json b/samples/Sentry.Samples.Quartz/appsettings.json
new file mode 100644
index 0000000000..d5e353d5b7
--- /dev/null
+++ b/samples/Sentry.Samples.Quartz/appsettings.json
@@ -0,0 +1,39 @@
+{
+ // Example configuration via JSON. Only a subset of the settings below:
+ // All Sentry settings can also be configured via code or environment variables:
+ "Sentry": {
+ // The DSN can also be set via environment variable
+ // "Dsn": "TODO: Configure your DSN here and uncomment this line",
+ // Opt-in for payload submission
+ "MaxRequestBodySize": "Always",
+ // Sends Cookies, User Id when one is logged on and user IP address to sentry. It's turned off by default.
+ "SendDefaultPii": true,
+ // Whether to add System.Diagnostics.Activity data to the event::
+ // For more: https://github.com/dotnet/runtime/blob/master/src/libraries/System.Diagnostics.DiagnosticSource/src/ActivityUserGuide.md
+ "IncludeActivityData": true,
+ // Record any message with this level or higher as a breadcrumb (default is Information)
+ "MinimumBreadcrumbLevel": "Information",
+ // Don't only keep Warnings as Breadcrumb but actually send an event
+ "MinimumEventLevel": "Warning",
+ // Send the stack trace of captured messages (e.g: a LogWarning without an exception)
+ "AttachStackTrace": true,
+ // The flag below can be used to see the internal logs of the SDK in the applications log (it's off by default)
+ "Debug": true,
+ // By default the level is Debug but it can be changed to any level of SentryLevel enum
+ "DiagnosticLevel": "Error",
+ "DefaultTags": {
+ "default-key-in-config": "default-value"
+ }
+ },
+ "Logging": {
+ "IncludeScopes": false,
+ "LogLevel": {
+ "Default": "Trace"
+ }
+ },
+ "Kestrel": {
+ "EndpointDefaults": {
+ "Protocols": "Http2"
+ }
+ }
+}
diff --git a/scripts/generate-solution-filters-config.yaml b/scripts/generate-solution-filters-config.yaml
index e3eaf125bc..a1a52653b4 100644
--- a/scripts/generate-solution-filters-config.yaml
+++ b/scripts/generate-solution-filters-config.yaml
@@ -167,6 +167,7 @@ filterConfigs:
- "**/Sentry.csproj"
- "**/Sentry.Analyzers.csproj"
- "**/Sentry.Hangfire.csproj"
+ - "**/Sentry.Quartz.csproj"
- "**/Sentry.Samples.Hangfire.csproj"
- "**/Sentry.Testing.csproj"
- "**/Sentry.Testing.CrashableApp.csproj"
diff --git a/src/Sentry.Quartz/GlobalConfigurationExtensions.cs b/src/Sentry.Quartz/GlobalConfigurationExtensions.cs
new file mode 100644
index 0000000000..34ab03bb1f
--- /dev/null
+++ b/src/Sentry.Quartz/GlobalConfigurationExtensions.cs
@@ -0,0 +1,58 @@
+ using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+using Quartz;
+
+namespace Sentry.Quartz;
+
+///
+/// Quartz.NET Extensions for .
+///
+public static class GlobalConfigurationExtensions
+{
+ ///
+ /// Adds middleware to track CRON jobs to Sentry
+ ///
+ ///
+ /// Configures the options
+ ///
+ public static IQuartzBuilder AddSentryCronJobs(this IQuartzBuilder configuration, Action? configure = null)
+ {
+ return configuration.AddJobMiddleware().ConfigureOptions(configure);
+ }
+
+ ///
+ /// Adds middleware to track job execution duration metrics
+ ///
+ ///
+ /// Configures the options
+ ///
+ public static IQuartzBuilder AddSentryMetrics(this IQuartzBuilder configuration, Action? configure = null)
+ {
+ return configuration.AddJobMiddleware().ConfigureOptions(configure);
+ }
+
+ ///
+ /// Adds middleware that pushes a scope to sentry before job execution
+ ///
+ ///
+ ///
+ public static IQuartzBuilder AddSentryScope(this IQuartzBuilder configuration)
+ {
+ return configuration.AddJobMiddleware();
+ }
+
+ ///
+ /// For testing
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ internal static IQuartzBuilder AddSentryCronJobs(this IQuartzBuilder configuration, IOptions options, IHub hub, ILogger logger)
+ {
+ configuration.AddJobMiddleware(new SentryCronJobMiddleware(hub, options, logger));
+ return configuration;
+ }
+}
diff --git a/src/Sentry.Quartz/RegionInfoExtensions.cs b/src/Sentry.Quartz/RegionInfoExtensions.cs
new file mode 100644
index 0000000000..b5be84d8d0
--- /dev/null
+++ b/src/Sentry.Quartz/RegionInfoExtensions.cs
@@ -0,0 +1,19 @@
+namespace Sentry.Quartz;
+
+internal static class RegionInfoExtensions
+{
+ extension(RegionInfo)
+ {
+ public static RegionInfo? GetCurrentRegionOrNull()
+ {
+ try
+ {
+ return RegionInfo.CurrentRegion;
+ }
+ catch (CultureNotFoundException)
+ {
+ return null;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Sentry.Quartz/Sentry.Quartz.csproj b/src/Sentry.Quartz/Sentry.Quartz.csproj
new file mode 100644
index 0000000000..37254dcaab
--- /dev/null
+++ b/src/Sentry.Quartz/Sentry.Quartz.csproj
@@ -0,0 +1,24 @@
+
+
+
+ Official Quartz integration for Sentry - Open-source error tracking that helps developers monitor and fix crashes in real time.
+ $(PackageTags);Quartz
+ net10.0;
+ enable
+ false
+
+
+
+ true
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Sentry.Quartz/SentryCronInformation.cs b/src/Sentry.Quartz/SentryCronInformation.cs
new file mode 100644
index 0000000000..13a76f96ea
--- /dev/null
+++ b/src/Sentry.Quartz/SentryCronInformation.cs
@@ -0,0 +1,21 @@
+using Quartz;
+
+namespace Sentry.Quartz;
+
+internal sealed class SentryCronInformation
+{
+ public SentryCronInformation(IJob job)
+ {
+ var jobType = job.GetType();
+ var monitorAttribute = jobType.GetCustomAttribute();
+
+ ShouldWriteStatusToSentry = monitorAttribute is not null;
+ MonitorSlug = monitorAttribute?.MonitorSlug ?? jobType.Name;
+ }
+
+ public string MonitorSlug { get; }
+
+ public bool ShouldWriteStatusToSentry { get; }
+
+ internal bool WarningShownForSecondsParameterIssue { get; set; }
+}
diff --git a/src/Sentry.Quartz/SentryCronJobMiddleware.cs b/src/Sentry.Quartz/SentryCronJobMiddleware.cs
new file mode 100644
index 0000000000..d84c89fc78
--- /dev/null
+++ b/src/Sentry.Quartz/SentryCronJobMiddleware.cs
@@ -0,0 +1,121 @@
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+using Quartz;
+
+namespace Sentry.Quartz;
+
+internal sealed partial class SentryCronJobMiddleware : IJobExecutionMiddleware
+{
+ private readonly SentryCronJobOptions _options;
+ private readonly ConcurrentDictionary _sentryCronInformation = [];
+ private readonly IHub _hub;
+ private readonly ILogger _logger;
+
+ public SentryCronJobMiddleware(IHub hub, IOptions options, ILogger logger)
+ {
+ _hub = hub;
+ _logger = logger;
+ _options = options.Value;
+ }
+
+ public async ValueTask Invoke(IJobExecutionContext context, JobExecutionDelegate next, CancellationToken cancellationToken)
+ {
+ var jobType = context.JobInstance.GetType();
+ var info = _sentryCronInformation.GetOrAdd(jobType, _ => new SentryCronInformation(context.JobInstance));
+
+ var sentryId = StartQuartz(context, info);
+ try
+ {
+ await next(context, cancellationToken).ConfigureAwait(false);
+ CompleteCheckIn(info, sentryId, CheckInStatus.Ok);
+ }
+ catch
+ {
+ CompleteCheckIn(info, sentryId, CheckInStatus.Error);
+ throw;
+ }
+ }
+
+ private SentryId? StartQuartz(IJobExecutionContext context, SentryCronInformation info)
+ {
+ if (context.Trigger is not ICronTrigger)
+ {
+ return null;
+ }
+
+ if (info.ShouldWriteStatusToSentry)
+ {
+ return _hub.CaptureCheckIn(info.MonitorSlug, CheckInStatus.InProgress, configureMonitorOptions: options =>
+ {
+ if (_options.EnableUpsertCronMonitor && context.Trigger is ICronTrigger cronTrigger)
+ {
+ UpsertCronMonitor(cronTrigger, info, options);
+ }
+ });
+ }
+
+ return null;
+ }
+
+ private void CompleteCheckIn(SentryCronInformation info, SentryId? sentryId, CheckInStatus status)
+ {
+ if (sentryId is not null)
+ {
+ _hub.CaptureCheckIn(info.MonitorSlug, status, sentryId);
+ }
+ }
+
+ private void UpsertCronMonitor(ICronTrigger cronTrigger, SentryCronInformation information, SentryMonitorOptions options)
+ {
+ if (string.IsNullOrWhiteSpace(cronTrigger.CronExpressionString))
+ {
+ return;
+ }
+
+ if (TimeZoneInfo.TryConvertWindowsIdToIanaId(cronTrigger.TimeZone.Id, RegionInfo.GetCurrentRegionOrNull()?.TwoLetterISORegionName, out string? iana))
+ {
+ options.TimeZone = iana;
+ }
+ else
+ {
+ options.TimeZone = cronTrigger.TimeZone.Id;
+ }
+
+ string monitorSlug = information.MonitorSlug;
+ string cron = cronTrigger.CronExpressionString;
+ string[] fields = cron.Split(' ', StringSplitOptions.RemoveEmptyEntries);
+
+ if (fields.Length is 6 or 7)
+ {
+ if (fields[0] != "0" && !information.WarningShownForSecondsParameterIssue)
+ {
+ information.WarningShownForSecondsParameterIssue = true;
+ LogGranularityWarning(monitorSlug, cron);
+ }
+
+ fields = fields[1..6];
+ }
+
+ try
+ {
+ string crontab = string.Join(" ", fields);
+ var cronExpression = CronExpression.Parse(crontab, CronFormat.Unix);
+
+ string[] normalized = cronExpression.CronExpressionString
+ .Replace("?", "*", StringComparison.OrdinalIgnoreCase)
+ .Split(' ', StringSplitOptions.RemoveEmptyEntries);
+
+ options.Interval(string.Join(" ", normalized[1..6]));
+ }
+ catch (ArgumentException ex)
+ {
+ LogApiException(ex, monitorSlug, cron);
+ }
+ }
+
+ [LoggerMessage(1, LogLevel.Warning, "Sentry Cron Monitor supports a minimum granularity of minutes. But for job {MonitorSlug} \"{Cron}\" was provided. The first field (seconds) will be ignored")]
+ private partial void LogGranularityWarning(string monitorSlug, string cron);
+
+ [LoggerMessage(2, LogLevel.Error, "Sentry Cron Monitor update failed for job {MonitorSlug}. Cron expression \"{Cron}\" is invalid")]
+ private partial void LogApiException(Exception exception, string monitorSlug, string cron);
+}
diff --git a/src/Sentry.Quartz/SentryCronJobOptions.cs b/src/Sentry.Quartz/SentryCronJobOptions.cs
new file mode 100644
index 0000000000..d05a65c26c
--- /dev/null
+++ b/src/Sentry.Quartz/SentryCronJobOptions.cs
@@ -0,0 +1,16 @@
+namespace Sentry.Quartz;
+
+///
+/// Represents configuration options for Sentry integration with Quartz Cron Jobs.
+///
+public class SentryCronJobOptions
+{
+ ///
+ /// Gets or sets a value indicating whether to enable or disable the automatic upsert of a CronMonitor when a CronTrigger is linked with the job execution context.
+ ///
+ ///
+ /// If set to true, enables the creation or update of a CronMonitor with relevant details, such as Cron expression and time zone,
+ /// during a job execution configured with a CronTrigger. This ensures that the monitor reflects accurate scheduling metadata in Sentry.
+ ///
+ public bool EnableUpsertCronMonitor { get; set; } = true;
+}
diff --git a/src/Sentry.Quartz/SentryCronMonitorSlugAttribute.cs b/src/Sentry.Quartz/SentryCronMonitorSlugAttribute.cs
new file mode 100644
index 0000000000..1b6581f541
--- /dev/null
+++ b/src/Sentry.Quartz/SentryCronMonitorSlugAttribute.cs
@@ -0,0 +1,17 @@
+namespace Sentry.Quartz;
+
+///
+/// Sentry Monitor Slug Attribute
+///
+///
+[AttributeUsage(AttributeTargets.Class)]
+public sealed class SentryCronMonitorSlugAttribute(string? monitorSlug = null) : Attribute
+{
+ ///
+ /// Gets the slug associated with the Sentry monitor. The monitor slug is used to
+ /// identify a specific Sentry monitor associated with a job. If no slug is explicitly
+ /// provided through the , it defaults to
+ /// the name of the job type.
+ ///
+ public string? MonitorSlug { get; } = monitorSlug;
+}
diff --git a/src/Sentry.Quartz/SentryMetricsMiddleware.cs b/src/Sentry.Quartz/SentryMetricsMiddleware.cs
new file mode 100644
index 0000000000..00a5855b98
--- /dev/null
+++ b/src/Sentry.Quartz/SentryMetricsMiddleware.cs
@@ -0,0 +1,63 @@
+using Microsoft.Extensions.Options;
+using Quartz;
+
+namespace Sentry.Quartz;
+
+internal sealed class SentryMetricsMiddleware : IJobExecutionMiddleware
+{
+ private readonly IHub _sentryHub;
+ private readonly IOptions _options;
+
+ public SentryMetricsMiddleware(IHub sentryHub, IOptions options)
+ {
+ _sentryHub = sentryHub;
+ _options = options;
+ }
+
+ public async ValueTask Invoke(IJobExecutionContext context, JobExecutionDelegate next, CancellationToken cancellationToken)
+ {
+ using (GetLogger(context))
+ {
+ await next(context, cancellationToken).ConfigureAwait(false);
+ }
+ }
+
+ private SentryMetricsLogger? GetLogger(IJobExecutionContext context)
+ {
+ if (!context.JobDetail.JobDataMap.TryGetBoolean("LogMetrics", out bool logMetrics) || logMetrics)
+ {
+ var metricsName = _options.Value.ResolveMetricsName(context.JobDetail);
+ var additionalAttributes = _options.Value.AdditionalAttributes?.Invoke(context.JobDetail);
+ return new SentryMetricsLogger(metricsName, additionalAttributes, _sentryHub);
+ }
+
+ return null;
+ }
+}
+
+public class SentryMetricsOptions
+{
+ public Func ResolveMetricsName { get; set; } = jobDetail => $"quartz.job.duration.{jobDetail.Key.Group}.{jobDetail.Key.Name}";
+
+ public Func>? AdditionalAttributes { get; set; }
+}
+
+internal sealed class SentryMetricsLogger : IDisposable
+{
+ private readonly string _metricName;
+ private readonly IHub _sentryHub;
+ private readonly Stopwatch _stopWatch = Stopwatch.StartNew();
+ private readonly IDictionary? _attributes;
+
+ public SentryMetricsLogger(string metricName, IDictionary? attributes, IHub sentryHub)
+ {
+ _attributes = attributes;
+ _metricName = metricName;
+ _sentryHub = sentryHub;
+ }
+
+ public void Dispose()
+ {
+ _sentryHub.Metrics.EmitDistribution(_metricName, _stopWatch.ElapsedMilliseconds, MeasurementUnit.Duration.Millisecond, _attributes);
+ }
+}
diff --git a/src/Sentry.Quartz/SentryScopeMiddleware.cs b/src/Sentry.Quartz/SentryScopeMiddleware.cs
new file mode 100644
index 0000000000..e24224b168
--- /dev/null
+++ b/src/Sentry.Quartz/SentryScopeMiddleware.cs
@@ -0,0 +1,14 @@
+using Quartz;
+
+namespace Sentry.Quartz;
+
+internal sealed class SentryScopeMiddleware(IHub sentryHub) : IJobExecutionMiddleware
+{
+ private readonly IHub _sentryHub = sentryHub;
+
+ public async ValueTask Invoke(IJobExecutionContext context, JobExecutionDelegate next, CancellationToken cancellationToken)
+ {
+ using var _ = _sentryHub.PushScope();
+ await next(context, cancellationToken).ConfigureAwait(false);
+ }
+}