Add Sentry (#19)

* add sentry

* add sentry

* better log web3proxy

* Add managing and worker on sentry

* better log web3proxy
This commit is contained in:
Oda
2025-04-22 20:49:02 +02:00
committed by GitHub
parent df5f7185c8
commit 42a4cafd8d
40 changed files with 2959 additions and 146 deletions

View File

@@ -17,6 +17,7 @@
<PackageReference Include="Essential.LoggerProvider.Elasticsearch" Version="1.3.2" />
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.20.1" />
<PackageReference Include="NSwag.AspNetCore" Version="14.0.7" />
<PackageReference Include="Sentry.AspNetCore" Version="5.5.1" />
<PackageReference Include="Serilog.AspNetCore" Version="8.0.1" />
<PackageReference Include="Serilog.Enrichers.Environment" Version="2.3.0" />
<PackageReference Include="Serilog.Exceptions" Version="8.4.0" />
@@ -33,6 +34,7 @@
<ItemGroup>
<ProjectReference Include="..\Managing.Bootstrap\Managing.Bootstrap.csproj" />
<ProjectReference Include="..\Managing.Aspire.ServiceDefaults\Managing.Aspire.ServiceDefaults.csproj" />
<ProjectReference Include="..\Managing.Core\Managing.Core.csproj" />
</ItemGroup>
<ItemGroup>

View File

@@ -0,0 +1,90 @@
using Sentry;
using System.Text;
namespace Managing.Api.Workers.Middleware
{
public class SentryDiagnosticsMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<SentryDiagnosticsMiddleware> _logger;
public SentryDiagnosticsMiddleware(RequestDelegate next, ILogger<SentryDiagnosticsMiddleware> logger)
{
_next = next;
_logger = logger;
}
public async Task InvokeAsync(HttpContext context)
{
// Only activate for the /api/sentry-diagnostics endpoint
if (context.Request.Path.StartsWithSegments("/api/sentry-diagnostics"))
{
await HandleDiagnosticsRequest(context);
return;
}
await _next(context);
}
private async Task HandleDiagnosticsRequest(HttpContext context)
{
var response = new StringBuilder();
response.AppendLine("Sentry Diagnostics Report");
response.AppendLine("========================");
response.AppendLine($"Timestamp: {DateTime.Now}");
response.AppendLine();
// Check if Sentry is initialized
response.AppendLine("## Sentry SDK Status");
response.AppendLine($"Sentry Enabled: {SentrySdk.IsEnabled}");
response.AppendLine($"Application Environment: {Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT")}");
response.AppendLine();
// Send a test event
response.AppendLine("## Test Event");
try
{
var id = SentrySdk.CaptureMessage($"Diagnostics test from {context.Request.Host} at {DateTime.Now}", SentryLevel.Info);
response.AppendLine($"Test Event ID: {id}");
response.AppendLine("Test event was sent to Sentry. Check your Sentry dashboard to confirm it was received.");
// Try to send an exception too
try
{
throw new Exception("Test exception from diagnostics middleware");
}
catch (Exception ex)
{
var exceptionId = SentrySdk.CaptureException(ex);
response.AppendLine($"Test Exception ID: {exceptionId}");
}
}
catch (Exception ex)
{
response.AppendLine($"Error sending test event: {ex.Message}");
response.AppendLine(ex.StackTrace);
}
response.AppendLine();
response.AppendLine("## Connectivity Check");
response.AppendLine("If events are not appearing in Sentry, check the following:");
response.AppendLine("1. Verify your DSN is correct in appsettings.json");
response.AppendLine("2. Ensure your network allows outbound HTTPS connections to sentry.apps.managing.live");
response.AppendLine("3. Check Sentry server logs for any ingestion issues");
response.AppendLine("4. Verify your Sentry project is correctly configured to receive events");
// Return the diagnostic information
context.Response.ContentType = "text/plain";
await context.Response.WriteAsync(response.ToString());
}
}
// Extension method used to add the middleware to the HTTP request pipeline.
public static class SentryDiagnosticsMiddlewareExtensions
{
public static IApplicationBuilder UseSentryDiagnostics(this IApplicationBuilder builder)
{
return builder.UseMiddleware<SentryDiagnosticsMiddleware>();
}
}
}

View File

@@ -8,11 +8,8 @@ using Managing.Core.Middleawares;
using Managing.Infrastructure.Databases.InfluxDb.Models;
using Managing.Infrastructure.Databases.MongoDb.Configurations;
using Managing.Infrastructure.Evm.Models.Privy;
using Microsoft.Extensions.ServiceDiscovery;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Diagnostics.HealthChecks;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Microsoft.Extensions.Hosting;
using Microsoft.OpenApi.Models;
using NSwag;
using NSwag.Generation.Processors.Security;
@@ -26,10 +23,36 @@ using OpenApiSecurityScheme = NSwag.OpenApiSecurityScheme;
// Builder
var builder = WebApplication.CreateBuilder(args);
builder.Configuration.SetBasePath(AppContext.BaseDirectory);
builder.Configuration.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
.AddJsonFile($"appsettings.{builder.Environment.EnvironmentName}.json");
var mongoConnectionString = builder.Configuration.GetSection(Constants.Databases.MongoDb)["ConnectionString"];
var influxUrl = builder.Configuration.GetSection(Constants.Databases.InfluxDb)["Url"];
var web3ProxyUrl = builder.Configuration.GetSection("Web3Proxy")["BaseUrl"];
// Initialize Sentry
SentrySdk.Init(options =>
{
// A Sentry Data Source Name (DSN) is required.
options.Dsn = builder.Configuration["Sentry:Dsn"];
// When debug is enabled, the Sentry client will emit detailed debugging information to the console.
options.Debug = false;
// Adds request URL and headers, IP and name for users, etc.
options.SendDefaultPii = true;
// This option is recommended. It enables Sentry's "Release Health" feature.
options.AutoSessionTracking = true;
// Enabling this option is recommended for client applications only. It ensures all threads use the same global scope.
options.IsGlobalModeEnabled = false;
// Example sample rate for your transactions: captures 10% of transactions
options.TracesSampleRate = 0.1;
});
// Add service discovery for Aspire
builder.Services.AddServiceDiscovery();
@@ -41,9 +64,6 @@ builder.Services.AddHealthChecks()
.AddUrlGroup(new Uri($"{web3ProxyUrl}/health"), name: "web3proxy", tags: ["api"]);
builder.WebHost.UseUrls("http://localhost:5001");
builder.Configuration.SetBasePath(AppContext.BaseDirectory);
builder.Configuration.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
.AddJsonFile($"appsettings.{builder.Environment.EnvironmentName}.json");
builder.Host.UseSerilog((hostBuilder, loggerConfiguration) =>
{
@@ -161,7 +181,11 @@ app.UseSwaggerUI(c =>
app.UseCors("CorsPolicy");
app.UseMiddleware(typeof(GlobalErrorHandlingMiddleware));
// Add Sentry diagnostics middleware (now using shared version from Core)
app.UseSentryDiagnostics();
// Using shared GlobalErrorHandlingMiddleware from Core project
app.UseMiddleware<GlobalErrorHandlingMiddleware>();
app.UseHttpsRedirection();

View File

@@ -23,6 +23,15 @@
"Web3Proxy": {
"BaseUrl": "http://localhost:4111"
},
"Sentry": {
"Dsn": "https://8fdb299b69df4f9d9b709c8d4a556608@bugcenter.apps.managing.live/2",
"MinimumEventLevel": "Error",
"SendDefaultPii": true,
"MaxBreadcrumbs": 50,
"SampleRate": 1.0,
"TracesSampleRate": 0.2,
"Debug": false
},
"Discord": {
"BotActivity": "with jobs",
"HandleUserAction": true,

View File

@@ -0,0 +1,74 @@
using Microsoft.AspNetCore.Mvc;
using Sentry;
using System;
namespace Managing.Api.Controllers
{
[ApiController]
[Route("api/[controller]")]
public class SentryTestController : ControllerBase
{
private readonly ILogger<SentryTestController> _logger;
public SentryTestController(ILogger<SentryTestController> logger)
{
_logger = logger;
}
[HttpGet("test-exception")]
public IActionResult TestException()
{
try
{
throw new Exception($"Test exception from SentryTestController - {DateTime.Now}");
}
catch (Exception ex)
{
// Add breadcrumbs for context
SentrySdk.AddBreadcrumb("About to capture test exception", "test");
// Add context to the error
SentrySdk.ConfigureScope(scope =>
{
scope.SetTag("test_type", "manual_exception");
scope.SetExtra("timestamp", DateTime.Now);
});
// Log to both Serilog and Sentry
_logger.LogError(ex, "Test exception captured in SentryTestController");
// Explicitly capture exception
SentrySdk.CaptureException(ex);
return Ok(new
{
message = "Exception manually captured and sent to Sentry",
exceptionMessage = ex.Message,
timestamp = DateTime.Now
});
}
}
[HttpGet("throw-exception")]
public IActionResult ThrowException()
{
_logger.LogInformation("About to throw an uncaught exception");
// This should be automatically captured by Sentry middleware
throw new InvalidOperationException($"Uncaught exception from ThrowException endpoint - {DateTime.Now}");
}
[HttpGet("test-message")]
public IActionResult TestMessage()
{
// Send a simple message to Sentry
SentrySdk.CaptureMessage("Test message from Managing API", SentryLevel.Info);
return Ok(new
{
message = "Test message sent to Sentry",
timestamp = DateTime.Now
});
}
}
}

View File

@@ -0,0 +1,79 @@
namespace Managing.Api.Exceptions;
/// <summary>
/// Exception thrown when validation fails (maps to 400 Bad Request)
/// </summary>
public class ValidationException : Exception
{
public ValidationException(string message) : base(message)
{
}
public ValidationException(string message, Exception innerException) : base(message, innerException)
{
}
}
/// <summary>
/// Exception thrown when a resource is not found (maps to 404 Not Found)
/// </summary>
public class NotFoundException : Exception
{
public NotFoundException(string message) : base(message)
{
}
public NotFoundException(string resourceType, string identifier)
: base($"{resourceType} with identifier '{identifier}' was not found.")
{
}
}
/// <summary>
/// Exception thrown when the user does not have permission (maps to 403 Forbidden)
/// </summary>
public class ForbiddenException : Exception
{
public ForbiddenException(string message) : base(message)
{
}
public ForbiddenException() : base("You do not have permission to access this resource.")
{
}
}
/// <summary>
/// Exception thrown when there is a conflict with the current state (maps to 409 Conflict)
/// </summary>
public class ConflictException : Exception
{
public ConflictException(string message) : base(message)
{
}
}
/// <summary>
/// Exception thrown when a rate limit is exceeded (maps to 429 Too Many Requests)
/// </summary>
public class RateLimitExceededException : Exception
{
public RateLimitExceededException(string message) : base(message)
{
}
public RateLimitExceededException() : base("Rate limit exceeded. Please try again later.")
{
}
}
/// <summary>
/// Exception thrown when an external service is unavailable (maps to 503 Service Unavailable)
/// </summary>
public class ServiceUnavailableException : Exception
{
public ServiceUnavailableException(string serviceName)
: base($"The service '{serviceName}' is currently unavailable. Please try again later.")
{
}
}

View File

@@ -1,15 +1,20 @@
using System.Net;
using System.Text.Json;
using Sentry;
namespace Managing.Api.Exceptions;
public class GlobalErrorHandlingMiddleware
{
private readonly RequestDelegate _next;
public GlobalErrorHandlingMiddleware(RequestDelegate next)
private readonly ILogger<GlobalErrorHandlingMiddleware> _logger;
public GlobalErrorHandlingMiddleware(RequestDelegate next, ILogger<GlobalErrorHandlingMiddleware> logger)
{
_next = next;
_logger = logger;
}
public async Task Invoke(HttpContext context)
{
try
@@ -21,42 +26,162 @@ public class GlobalErrorHandlingMiddleware
await HandleExceptionAsync(context, ex);
}
}
private static Task HandleExceptionAsync(HttpContext context, Exception exception)
private Task HandleExceptionAsync(HttpContext context, Exception exception)
{
HttpStatusCode status;
var exceptionType = exception.GetType();
string errorMessage;
if (exceptionType == typeof(Exception))
// Determine the appropriate status code based on exception type
status = exception switch
{
status = HttpStatusCode.InternalServerError;
}
else if (exceptionType == typeof(NotImplementedException))
// 400 Bad Request
ArgumentException => HttpStatusCode.BadRequest,
ValidationException => HttpStatusCode.BadRequest,
FormatException => HttpStatusCode.BadRequest,
InvalidOperationException => HttpStatusCode.BadRequest,
// 401 Unauthorized
UnauthorizedAccessException => HttpStatusCode.Unauthorized,
// 403 Forbidden
ForbiddenException => HttpStatusCode.Forbidden,
// 404 Not Found
KeyNotFoundException => HttpStatusCode.NotFound,
FileNotFoundException => HttpStatusCode.NotFound,
DirectoryNotFoundException => HttpStatusCode.NotFound,
NotFoundException => HttpStatusCode.NotFound,
// 408 Request Timeout
TimeoutException => HttpStatusCode.RequestTimeout,
// 409 Conflict
ConflictException => HttpStatusCode.Conflict,
// 429 Too Many Requests
RateLimitExceededException => HttpStatusCode.TooManyRequests,
// 501 Not Implemented
NotImplementedException => HttpStatusCode.NotImplemented,
// 503 Service Unavailable
ServiceUnavailableException => HttpStatusCode.ServiceUnavailable,
// 500 Internal Server Error (default)
_ => HttpStatusCode.InternalServerError
};
// Log the error with appropriate severity based on status code
var isServerError = (int)status >= 500;
if (isServerError)
{
status = HttpStatusCode.NotImplemented;
}
else if (exceptionType == typeof(UnauthorizedAccessException))
{
status = HttpStatusCode.Unauthorized;
}
else if (exceptionType == typeof(ArgumentException))
{
status = HttpStatusCode.Unauthorized;
}
else if (exceptionType == typeof(KeyNotFoundException))
{
status = HttpStatusCode.Unauthorized;
_logger.LogError(exception, "Server Error: {StatusCode} on {Path}", (int)status, context.Request.Path);
}
else
{
status = HttpStatusCode.InternalServerError;
_logger.LogWarning(exception, "Client Error: {StatusCode} on {Path}", (int)status, context.Request.Path);
}
var message = exception.Message;
var stackTrace = exception.StackTrace;
var exceptionResult = JsonSerializer.Serialize(new { error = message, stackTrace });
// Capture exception in Sentry with request context
var sentryId = SentrySdk.CaptureException(exception, scope =>
{
// Add HTTP request information
scope.SetTag("http.method", context.Request.Method);
scope.SetTag("http.url", context.Request.Path);
// Add request details
scope.SetExtra("query_string", context.Request.QueryString.ToString());
// Add custom tags to help with filtering
scope.SetTag("error_type", exception.GetType().Name);
scope.SetTag("status_code", ((int)status).ToString());
scope.SetTag("host", context.Request.Host.ToString());
scope.SetTag("path", context.Request.Path.ToString());
// Add any correlation IDs if available
if (context.Request.Headers.TryGetValue("X-Correlation-ID", out var correlationId))
{
scope.SetTag("correlation_id", correlationId.ToString());
}
// Additional context based on exception type
if (exception is ValidationException)
{
scope.SetTag("error_category", "validation");
}
else if (exception is NotFoundException)
{
scope.SetTag("error_category", "not_found");
}
// Add additional context from exception data if available
foreach (var key in exception.Data.Keys)
{
if (key is string keyStr && exception.Data[key] != null)
{
scope.SetExtra(keyStr, exception.Data[key].ToString());
}
}
// Add breadcrumb for the request
scope.AddBreadcrumb(
message: $"Request to {context.Request.Path}",
category: "request",
level: BreadcrumbLevel.Info
);
});
// Use a more user-friendly error message in production
if (Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") == "Production")
{
// For 5xx errors, use a generic message
if (isServerError)
{
errorMessage = "An unexpected error occurred. Our team has been notified.";
}
else
{
// For 4xx errors, keep the original message since it's likely helpful for the user
errorMessage = exception.Message;
}
}
else
{
errorMessage = exception.Message;
}
// Create the error response
var errorResponse = new ErrorResponse
{
StatusCode = (int)status,
Message = errorMessage,
TraceId = sentryId.ToString()
};
// Only include stack trace in development environment
if (Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") != "Production")
{
errorResponse.StackTrace = exception.StackTrace;
}
var result = JsonSerializer.Serialize(errorResponse, new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
});
context.Response.ContentType = "application/json";
context.Response.StatusCode = (int)status;
return context.Response.WriteAsync(exceptionResult);
return context.Response.WriteAsync(result);
}
// Custom error response class
private class ErrorResponse
{
public int StatusCode { get; set; }
public string Message { get; set; }
public string TraceId { get; set; }
public string StackTrace { get; set; }
}
}

View File

@@ -0,0 +1,109 @@
using Sentry;
namespace Managing.Api.Exceptions;
/// <summary>
/// Utility class for capturing errors with Sentry across the application
/// </summary>
public static class SentryErrorCapture
{
/// <summary>
/// Captures an exception in Sentry with additional context
/// </summary>
/// <param name="exception">The exception to capture</param>
/// <param name="contextName">A descriptive name for where the error occurred</param>
/// <param name="extraData">Optional dictionary of additional data to include</param>
/// <returns>The Sentry event ID</returns>
public static SentryId CaptureException(Exception exception, string contextName, IDictionary<string, object> extraData = null)
{
return SentrySdk.CaptureException(exception, scope =>
{
// Add context information
scope.SetTag("context", contextName);
scope.SetTag("error_type", exception.GetType().Name);
// Add any extra data provided
if (extraData != null)
{
foreach (var kvp in extraData)
{
scope.SetExtra(kvp.Key, kvp.Value?.ToString() ?? "null");
}
}
// Add extra info from the exception's Data dictionary if available
foreach (var key in exception.Data.Keys)
{
if (key is string keyStr && exception.Data[key] != null)
{
scope.SetExtra($"exception_data_{keyStr}", exception.Data[key].ToString());
}
}
// Add a breadcrumb for context
scope.AddBreadcrumb(
message: $"Exception in {contextName}",
category: "error",
level: BreadcrumbLevel.Error
);
});
}
/// <summary>
/// Enriches an exception with additional context data before throwing
/// </summary>
/// <param name="exception">The exception to enrich</param>
/// <param name="contextData">Dictionary of context data to add</param>
/// <returns>The enriched exception for chaining</returns>
public static Exception EnrichException(Exception exception, IDictionary<string, object> contextData)
{
if (contextData != null)
{
foreach (var item in contextData)
{
exception.Data[item.Key] = item.Value;
}
}
return exception;
}
/// <summary>
/// Captures a message in Sentry with additional context
/// </summary>
/// <param name="message">The message to capture</param>
/// <param name="level">The severity level</param>
/// <param name="contextName">A descriptive name for where the message originated</param>
/// <param name="extraData">Optional dictionary of additional data to include</param>
/// <returns>The Sentry event ID</returns>
public static SentryId CaptureMessage(string message, SentryLevel level, string contextName, IDictionary<string, object> extraData = null)
{
// First capture the message with the specified level
var id = SentrySdk.CaptureMessage(message, level);
// Then add context via a scope
SentrySdk.ConfigureScope(scope =>
{
// Add context information
scope.SetTag("context", contextName);
// Add any extra data provided
if (extraData != null)
{
foreach (var kvp in extraData)
{
scope.SetExtra(kvp.Key, kvp.Value?.ToString() ?? "null");
}
}
// Add a breadcrumb for context
scope.AddBreadcrumb(
message: $"Message from {contextName}",
category: "message",
level: BreadcrumbLevel.Info
);
});
return id;
}
}

View File

@@ -18,6 +18,7 @@
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.5" />
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.20.1" />
<PackageReference Include="NSwag.AspNetCore" Version="14.0.7" />
<PackageReference Include="Sentry.AspNetCore" Version="5.5.1" />
<PackageReference Include="Serilog.AspNetCore" Version="8.0.1" />
<PackageReference Include="Serilog.Enrichers.Environment" Version="2.3.0" />
<PackageReference Include="Serilog.Exceptions" Version="8.4.0" />
@@ -34,6 +35,7 @@
<ItemGroup>
<ProjectReference Include="..\Managing.Bootstrap\Managing.Bootstrap.csproj" />
<ProjectReference Include="..\Managing.Aspire.ServiceDefaults\Managing.Aspire.ServiceDefaults.csproj" />
<ProjectReference Include="..\Managing.Core\Managing.Core.csproj" />
</ItemGroup>
<ItemGroup>

View File

@@ -0,0 +1,90 @@
using Sentry;
using System.Text;
namespace Managing.Api.Middleware
{
public class SentryDiagnosticsMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<SentryDiagnosticsMiddleware> _logger;
public SentryDiagnosticsMiddleware(RequestDelegate next, ILogger<SentryDiagnosticsMiddleware> logger)
{
_next = next;
_logger = logger;
}
public async Task InvokeAsync(HttpContext context)
{
// Only activate for the /api/sentry-diagnostics endpoint
if (context.Request.Path.StartsWithSegments("/api/sentry-diagnostics"))
{
await HandleDiagnosticsRequest(context);
return;
}
await _next(context);
}
private async Task HandleDiagnosticsRequest(HttpContext context)
{
var response = new StringBuilder();
response.AppendLine("Sentry Diagnostics Report");
response.AppendLine("========================");
response.AppendLine($"Timestamp: {DateTime.Now}");
response.AppendLine();
// Check if Sentry is initialized
response.AppendLine("## Sentry SDK Status");
response.AppendLine($"Sentry Enabled: {SentrySdk.IsEnabled}");
response.AppendLine($"Application Environment: {Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT")}");
response.AppendLine();
// Send a test event
response.AppendLine("## Test Event");
try
{
var id = SentrySdk.CaptureMessage($"Diagnostics test from {context.Request.Host} at {DateTime.Now}", SentryLevel.Info);
response.AppendLine($"Test Event ID: {id}");
response.AppendLine("Test event was sent to Sentry. Check your Sentry dashboard to confirm it was received.");
// Try to send an exception too
try
{
throw new Exception("Test exception from diagnostics middleware");
}
catch (Exception ex)
{
var exceptionId = SentrySdk.CaptureException(ex);
response.AppendLine($"Test Exception ID: {exceptionId}");
}
}
catch (Exception ex)
{
response.AppendLine($"Error sending test event: {ex.Message}");
response.AppendLine(ex.StackTrace);
}
response.AppendLine();
response.AppendLine("## Connectivity Check");
response.AppendLine("If events are not appearing in Sentry, check the following:");
response.AppendLine("1. Verify your DSN is correct in appsettings.json");
response.AppendLine("2. Ensure your network allows outbound HTTPS connections to sentry.apps.managing.live");
response.AppendLine("3. Check Sentry server logs for any ingestion issues");
response.AppendLine("4. Verify your Sentry project is correctly configured to receive events");
// Return the diagnostic information
context.Response.ContentType = "text/plain";
await context.Response.WriteAsync(response.ToString());
}
}
// Extension method used to add the middleware to the HTTP request pipeline.
public static class SentryDiagnosticsMiddlewareExtensions
{
public static IApplicationBuilder UseSentryDiagnostics(this IApplicationBuilder builder)
{
return builder.UseMiddleware<SentryDiagnosticsMiddleware>();
}
}
}

View File

@@ -1,12 +1,12 @@
using System.Text;
using System.Text.Json.Serialization;
using Managing.Api.Authorization;
using Managing.Api.Exceptions;
using Managing.Api.Filters;
using Managing.Api.Workers;
using Managing.Application.Hubs;
using Managing.Bootstrap;
using Managing.Common;
using Managing.Core.Middleawares;
using Managing.Infrastructure.Databases.InfluxDb.Models;
using Managing.Infrastructure.Databases.MongoDb.Configurations;
using Managing.Infrastructure.Evm.Models.Privy;
@@ -26,10 +26,46 @@ using Microsoft.Extensions.Hosting;
using HealthChecks.UI.Client;
using OpenApiSecurityRequirement = Microsoft.OpenApi.Models.OpenApiSecurityRequirement;
using OpenApiSecurityScheme = NSwag.OpenApiSecurityScheme;
using Sentry;
// Builder
var builder = WebApplication.CreateBuilder(args);
builder.Configuration.SetBasePath(AppContext.BaseDirectory);
builder.Configuration.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
.AddJsonFile($"appsettings.{builder.Environment.EnvironmentName}.json")
.AddJsonFile($"config.{builder.Environment.EnvironmentName}.json",
optional: true, reloadOnChange: true);
builder.Configuration.AddEnvironmentVariables();
builder.Configuration.AddUserSecrets<Program>();
SentrySdk.Init(options =>
{
// A Sentry Data Source Name (DSN) is required.
// See https://docs.sentry.io/concepts/key-terms/dsn-explainer/
// You can set it in the SENTRY_DSN environment variable, or you can set it in code here.
options.Dsn = builder.Configuration["Sentry:Dsn"];
// When debug is enabled, the Sentry client will emit detailed debugging information to the console.
// This might be helpful, or might interfere with the normal operation of your application.
// We enable it here for demonstration purposes when first trying Sentry.
// You shouldn't do this in your applications unless you're troubleshooting issues with Sentry.
options.Debug = false;
// Adds request URL and headers, IP and name for users, etc.
options.SendDefaultPii = true;
// This option is recommended. It enables Sentry's "Release Health" feature.
options.AutoSessionTracking = true;
// Enabling this option is recommended for client applications only. It ensures all threads use the same global scope.
options.IsGlobalModeEnabled = false;
// Example sample rate for your transactions: captures 10% of transactions
options.TracesSampleRate = 0.1;
});
// Add Service Defaults - using extension methods directly
builder.Services.AddServiceDiscovery();
builder.Services.AddHealthChecks()
@@ -45,15 +81,6 @@ builder.Services.AddHealthChecks()
.AddUrlGroup(new Uri($"{influxUrl}/health"), name: "influxdb", tags: ["database"])
.AddUrlGroup(new Uri($"{web3ProxyUrl}/health"), name: "web3proxy", tags: ["api"]);
builder.Configuration.SetBasePath(AppContext.BaseDirectory);
builder.Configuration.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
.AddJsonFile($"appsettings.{builder.Environment.EnvironmentName}.json")
.AddJsonFile($"config.{builder.Environment.EnvironmentName}.json",
optional: true, reloadOnChange: true);
builder.Configuration.AddEnvironmentVariables();
builder.Configuration.AddUserSecrets<Program>();
builder.Host.UseSerilog((hostBuilder, loggerConfiguration) =>
{
var envName = builder.Environment.EnvironmentName.ToLower().Replace(".", "-");
@@ -178,7 +205,11 @@ app.UseSwaggerUI(c =>
app.UseCors("CorsPolicy");
app.UseMiddleware(typeof(GlobalErrorHandlingMiddleware));
// Add Sentry diagnostics middleware (now using shared version)
app.UseSentryDiagnostics();
// Using shared GlobalErrorHandlingMiddleware from core project
app.UseMiddleware<GlobalErrorHandlingMiddleware>();
app.UseMiddleware<JwtMiddleware>();

View File

@@ -0,0 +1,90 @@
# Error Handling in Managing API
This document describes the centralized error handling approach used in the Managing API to ensure consistent error responses and logging, with Sentry integration for error monitoring.
## Architecture
The error handling architecture consists of:
1. **Global Error Handling Middleware**: Captures all unhandled exceptions and formats consistent responses
2. **Sentry Integration**: Sends detailed error information to Sentry for monitoring and analysis
3. **MediatR Pipeline**: Adds request context to exceptions before they reach the global middleware
4. **SentryErrorCapture Utility**: Provides methods for manually capturing errors with context
## Global Error Handling Middleware
The `GlobalErrorHandlingMiddleware` is registered in `Program.cs` and handles all unhandled exceptions:
```csharp
app.UseMiddleware(typeof(GlobalErrorHandlingMiddleware));
```
When an exception occurs, the middleware:
1. Determines the appropriate HTTP status code based on exception type
2. Logs the error with request details
3. Captures the exception in Sentry with appropriate context
4. Returns a standardized JSON error response to the client
## Sentry Integration
Sentry is integrated in two ways:
1. **Global Configuration**: Set up in `Program.cs` with environment-specific settings
2. **Error Capture**: In the global middleware and utility methods
The captured data includes:
- HTTP request details (path, method, query strings)
- Exception details (type, message, stack trace)
- Additional context from exception data
- Tags for better categorization and filtering
## MediatR Exception Handling
The `UnhandledExceptionBehaviour` in the MediatR pipeline:
1. Catches exceptions in request handlers
2. Logs the error with request details
3. Adds MediatR-specific context to the exception's Data dictionary
4. Rethrows the exception to be caught by the global middleware
This approach allows the global middleware to have full context about where the exception occurred without duplicating reporting to Sentry.
## Manual Error Reporting
For manually reporting errors to Sentry, use the `SentryErrorCapture` utility:
```csharp
// Capture an exception with context
SentryErrorCapture.CaptureException(ex, "MyService", new Dictionary<string, object> {
{ "userId", user.Id },
{ "operation", "ProcessImport" }
});
// Enrich an exception before throwing
throw SentryErrorCapture.EnrichException(new ValidationException("Invalid data"),
new Dictionary<string, object> {
{ "validationErrors", errors }
});
```
## Error Response Format
The standard error response format is:
```json
{
"statusCode": 400,
"message": "The error message",
"traceId": "sentry-event-id",
"stackTrace": "Only included in non-production environments"
}
```
## Best Practices
1. **Don't catch exceptions unless necessary**: Let the global middleware handle most exceptions
2. **Add context to exceptions**: Use the Data dictionary to add context that will be captured
3. **Use appropriate exception types**: Different exception types map to different HTTP status codes
4. **Avoid sensitive data**: Never include sensitive data (passwords, tokens) in error messages or context

View File

@@ -0,0 +1,98 @@
# Sentry Integration for Managing API
This document describes how Sentry is integrated into the Managing API for error monitoring and performance tracking.
## Setup
Sentry has been integrated into the Managing API using the official `Sentry.AspNetCore` package (version 5.5.1). The integration follows the recommended approach using `WebHost.UseSentry()` in Program.cs:
```csharp
// In Program.cs
builder.WebHost.UseSentry();
```
This approach automatically picks up Sentry configuration from appsettings.json and environment variables. It's the official recommended method for ASP.NET Core applications.
The integration captures:
- Unhandled exceptions
- HTTP request information
- Performance metrics
- Custom errors and events
- User information and PII (when SendDefaultPii is enabled)
## Configuration
Sentry is configured through `appsettings.json`. Here are the available settings:
```json
"Sentry": {
"Dsn": "YOUR_SENTRY_DSN",
"MinimumEventLevel": "Error",
"SendDefaultPii": false,
"MaxBreadcrumbs": 50,
"SampleRate": 1.0,
"TracesSampleRate": 0.2,
"Debug": false
}
```
### Required Settings
- `Dsn`: Your Sentry project's Data Source Name (required). This should be obtained from your Sentry project settings.
### Optional Settings
- `MinimumEventLevel`: Minimum log level that triggers an event to be sent to Sentry (default: Error)
- `SampleRate`: Percentage of errors to send to Sentry (1.0 = 100%)
- `TracesSampleRate`: Percentage of transactions to send to Sentry (0.2 = 20%)
- `Debug`: Enable debug mode for Sentry SDK (set to true in Development environment)
## Environment Configuration
Each environment can have specific Sentry settings:
- Development: Uses settings from `appsettings.Development.json` (sampling rate set to 100% for easy testing)
- Production: Uses settings from `appsettings.Production.json`
## Custom Error Tracking
You can manually track errors and events using the `SentrySdk` class:
```csharp
try
{
// Your code
}
catch (Exception ex)
{
SentrySdk.CaptureException(ex);
// Additional error handling
}
```
## Testing the Integration
A test endpoint has been added to verify Sentry integration:
```
GET /test-sentry
```
This endpoint throws and captures a test exception, confirming that events are being sent to Sentry.
## Deployment Considerations
When deploying to production:
1. Update the Sentry DSN in your production configuration
2. Consider setting appropriate sampling rates based on traffic volume
3. Ensure PII (Personally Identifiable Information) handling complies with your privacy policies
## Performance Impact
The Sentry SDK has minimal performance impact with default settings. If needed, you can adjust sampling rates to reduce the number of events sent to Sentry.
## Security
Sentry DSNs should be treated as secrets and not committed directly to source control. Use environment variables or secret management for production deployments.

View File

@@ -1,13 +1,21 @@
{
"DetailedErrors": true,
"Logging": {
"LogLevel": {
"Default": "Error",
"Default": "Information",
"System": "Error",
"Microsoft": "Warning"
"Microsoft": "Warning",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"ElasticConfiguration": {
"Uri": "http://elasticsearch:9200/"
},
"Sentry": {
"Debug": true,
"TracesSampleRate": 1.0,
"SendDefaultPii": true,
"DiagnosticLevel": "Debug"
}
}

View File

@@ -24,6 +24,9 @@
"ElasticConfiguration": {
"Uri": "http://elasticsearch:9200"
},
"Sentry": {
"Dsn": "https://698e00d7cb404b049aff3881e5a47f6b@bugcenter.apps.managing.live/1"
},
"Discord": {
"ApplicationId": "",
"PublicKey": "",

View File

@@ -27,5 +27,8 @@
"ElasticConfiguration": {
"Uri": "http://elasticsearch:9200"
},
"Sentry": {
"Dsn": "https://698e00d7cb404b049aff3881e5a47f6b@bugcenter.apps.managing.live/1"
},
"AllowedHosts": "*"
}

View File

@@ -24,6 +24,15 @@
"Web3Proxy": {
"BaseUrl": "http://localhost:4111"
},
"Sentry": {
"Dsn": "https://698e00d7cb404b049aff3881e5a47f6b@bugcenter.apps.managing.live/1",
"MinimumEventLevel": "Error",
"SendDefaultPii": true,
"MaxBreadcrumbs": 50,
"SampleRate": 1.0,
"TracesSampleRate": 0.2,
"Debug": false
},
"Serilog": {
"MinimumLevel": {
"Default": "Information",

View File

@@ -22,8 +22,15 @@ namespace Managing.Application.Shared.Behaviours
{
var requestName = typeof(TRequest).Name;
logger.LogError(ex, $"Unhandled Exception for Request {requestName} {request}");
// Log to standard logger with request details
// This will be picked up by the global middleware for Sentry
logger.LogError(ex, "Unhandled Exception for Request {RequestName} {@Request}", requestName, request);
// Add MediatR context to the exception for better tracing
ex.Data["RequestType"] = requestName;
ex.Data["RequestHandler"] = typeof(TRequest).FullName;
// Rethrow to allow global error handling middleware to handle the response
throw;
}
}

View File

@@ -0,0 +1,78 @@
namespace Managing.Core.Exceptions;
/// <summary>
/// Exception thrown when validation fails (maps to 400 Bad Request)
/// </summary>
public class ValidationException : Exception
{
public ValidationException(string message) : base(message)
{
}
public ValidationException(string message, Exception innerException) : base(message, innerException)
{
}
}
/// <summary>
/// Exception thrown when a resource is not found (maps to 404 Not Found)
/// </summary>
public class NotFoundException : Exception
{
public NotFoundException(string message) : base(message)
{
}
public NotFoundException(string resourceType, string identifier)
: base($"{resourceType} with identifier '{identifier}' was not found.")
{
}
}
/// <summary>
/// Exception thrown when the user does not have permission (maps to 403 Forbidden)
/// </summary>
public class ForbiddenException : Exception
{
public ForbiddenException(string message) : base(message)
{
}
public ForbiddenException() : base("You do not have permission to access this resource.")
{
}
}
/// <summary>
/// Exception thrown when there is a conflict with the current state (maps to 409 Conflict)
/// </summary>
public class ConflictException : Exception
{
public ConflictException(string message) : base(message)
{
}
}
/// <summary>
/// Exception thrown when a rate limit is exceeded (maps to 429 Too Many Requests)
/// </summary>
public class RateLimitExceededException : Exception
{
public RateLimitExceededException(string message) : base(message)
{
}
public RateLimitExceededException() : base("Rate limit exceeded. Please try again later.")
{
}
}
/// <summary>
/// Exception thrown when an external service is unavailable (maps to 503 Service Unavailable)
/// </summary>
public class ServiceUnavailableException : Exception
{
public ServiceUnavailableException(string message) : base(message)
{
}
}

View File

@@ -0,0 +1,106 @@
# Error Handling in Managing Application
This document describes the centralized error handling approach used in the Managing applications to ensure consistent error responses and logging, with Sentry integration for error monitoring.
## Architecture
The error handling architecture consists of:
1. **Global Error Handling Middleware**: Captures all unhandled exceptions and formats consistent responses
2. **Sentry Integration**: Sends detailed error information to Sentry for monitoring and analysis
3. **Custom Exception Types**: Provide appropriate HTTP status code mapping
4. **SentryErrorCapture Utility**: Provides methods for manually capturing errors with context
## Global Error Handling Middleware
The `GlobalErrorHandlingMiddleware` is registered in `Program.cs` for both the main API and Worker API:
```csharp
app.UseMiddleware(typeof(GlobalErrorHandlingMiddleware));
```
When an exception occurs, the middleware:
1. Determines the appropriate HTTP status code based on exception type
2. Logs the error with request details
3. Captures the exception in Sentry with appropriate context
4. Returns a standardized JSON error response to the client
## Sentry Integration
Sentry is integrated in three ways:
1. **Global Configuration**: Set up in `Program.cs` with environment-specific settings
2. **Error Capture**: In the global middleware and utility methods
3. **Diagnostic Endpoint**: The SentryDiagnosticsMiddleware provides a test endpoint at `/api/sentry-diagnostics`
The captured data includes:
- HTTP request details (path, method, query strings)
- Exception details (type, message, stack trace)
- Additional context from exception data
- Tags for better categorization and filtering
## Using Custom Exception Types
The shared exception types map to appropriate HTTP status codes:
| Exception Type | HTTP Status Code | Use Case |
|----------------|------------------|----------|
| ValidationException | 400 Bad Request | Input validation errors |
| NotFoundException | 404 Not Found | Resource does not exist |
| ForbiddenException | 403 Forbidden | Permission denied |
| ConflictException | 409 Conflict | Resource state conflict |
| RateLimitExceededException | 429 Too Many Requests | Rate limit exceeded |
| ServiceUnavailableException | 503 Service Unavailable | External service down |
Example:
```csharp
// Validation error
throw new ValidationException("The username must be at least 3 characters");
// Resource not found with context
throw new NotFoundException("User", userId);
// Permission denied
throw new ForbiddenException();
```
## Manual Error Reporting
For manually reporting errors to Sentry, use the `SentryErrorCapture` utility:
```csharp
// Capture an exception with context
SentryErrorCapture.CaptureException(ex, "MyService", new Dictionary<string, object> {
{ "userId", user.Id },
{ "operation", "ProcessImport" }
});
// Enrich an exception before throwing
throw SentryErrorCapture.EnrichException(new ValidationException("Invalid data"),
new Dictionary<string, object> {
{ "validationErrors", errors }
});
```
## Error Response Format
The standard error response format is:
```json
{
"statusCode": 400,
"message": "The error message",
"traceId": "sentry-event-id",
"stackTrace": "Only included in non-production environments"
}
```
## Best Practices
1. **Use custom exception types**: Throw the appropriate exception type for each error case
2. **Add context to exceptions**: Use the Data dictionary to add context that will be captured
3. **Don't duplicate Sentry reporting**: Let the global middleware handle Sentry integration
4. **Avoid sensitive data**: Never include sensitive data (passwords, tokens) in error messages or context
5. **Use the diagnostic endpoint**: Test Sentry connectivity using the `/api/sentry-diagnostics` endpoint

View File

@@ -0,0 +1,109 @@
using Sentry;
namespace Managing.Core.Exceptions;
/// <summary>
/// Utility class for capturing errors with Sentry across the application
/// </summary>
public static class SentryErrorCapture
{
/// <summary>
/// Captures an exception in Sentry with additional context
/// </summary>
/// <param name="exception">The exception to capture</param>
/// <param name="contextName">A descriptive name for where the error occurred</param>
/// <param name="extraData">Optional dictionary of additional data to include</param>
/// <returns>The Sentry event ID</returns>
public static SentryId CaptureException(Exception exception, string contextName, IDictionary<string, object> extraData = null)
{
return SentrySdk.CaptureException(exception, scope =>
{
// Add context information
scope.SetTag("context", contextName);
scope.SetTag("error_type", exception.GetType().Name);
// Add any extra data provided
if (extraData != null)
{
foreach (var kvp in extraData)
{
scope.SetExtra(kvp.Key, kvp.Value?.ToString() ?? "null");
}
}
// Add extra info from the exception's Data dictionary if available
foreach (var key in exception.Data.Keys)
{
if (key is string keyStr && exception.Data[key] != null)
{
scope.SetExtra($"exception_data_{keyStr}", exception.Data[key].ToString());
}
}
// Add a breadcrumb for context
scope.AddBreadcrumb(
message: $"Exception in {contextName}",
category: "error",
level: BreadcrumbLevel.Error
);
});
}
/// <summary>
/// Enriches an exception with additional context data before throwing
/// </summary>
/// <param name="exception">The exception to enrich</param>
/// <param name="contextData">Dictionary of context data to add</param>
/// <returns>The enriched exception for chaining</returns>
public static Exception EnrichException(Exception exception, IDictionary<string, object> contextData)
{
if (contextData != null)
{
foreach (var item in contextData)
{
exception.Data[item.Key] = item.Value;
}
}
return exception;
}
/// <summary>
/// Captures a message in Sentry with additional context
/// </summary>
/// <param name="message">The message to capture</param>
/// <param name="level">The severity level</param>
/// <param name="contextName">A descriptive name for where the message originated</param>
/// <param name="extraData">Optional dictionary of additional data to include</param>
/// <returns>The Sentry event ID</returns>
public static SentryId CaptureMessage(string message, SentryLevel level, string contextName, IDictionary<string, object> extraData = null)
{
// First capture the message with the specified level
var id = SentrySdk.CaptureMessage(message, level);
// Then add context via a scope
SentrySdk.ConfigureScope(scope =>
{
// Add context information
scope.SetTag("context", contextName);
// Add any extra data provided
if (extraData != null)
{
foreach (var kvp in extraData)
{
scope.SetExtra(kvp.Key, kvp.Value?.ToString() ?? "null");
}
}
// Add a breadcrumb for context
scope.AddBreadcrumb(
message: $"Message from {contextName}",
category: "message",
level: BreadcrumbLevel.Info
);
});
return id;
}
}

View File

@@ -6,14 +6,11 @@
<Platforms>AnyCPU;x64</Platforms>
</PropertyGroup>
<ItemGroup>
<Compile Remove="Exceptions\**" />
<EmbeddedResource Remove="Exceptions\**" />
<None Remove="Exceptions\**" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Http.Abstractions" Version="2.2.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.0" />
<PackageReference Include="Sentry" Version="5.5.1" />
<PackageReference Include="Sentry.AspNetCore" Version="5.5.1" />
<PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" />
</ItemGroup>

View File

@@ -1,16 +1,21 @@
using System.Net;
using System.Text.Json;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Sentry;
using Managing.Core.Exceptions;
namespace Managing.Core.Middleawares;
public class GlobalErrorHandlingMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<GlobalErrorHandlingMiddleware> _logger;
public GlobalErrorHandlingMiddleware(RequestDelegate next)
public GlobalErrorHandlingMiddleware(RequestDelegate next, ILogger<GlobalErrorHandlingMiddleware> logger)
{
_next = next;
_logger = logger;
}
public async Task Invoke(HttpContext context)
@@ -25,42 +30,161 @@ public class GlobalErrorHandlingMiddleware
}
}
private static Task HandleExceptionAsync(HttpContext context, Exception exception)
private Task HandleExceptionAsync(HttpContext context, Exception exception)
{
HttpStatusCode status;
var exceptionType = exception.GetType();
string errorMessage;
if (exceptionType == typeof(Exception))
// Determine the appropriate status code based on exception type
status = exception switch
{
status = HttpStatusCode.InternalServerError;
}
else if (exceptionType == typeof(NotImplementedException))
// 400 Bad Request
ArgumentException => HttpStatusCode.BadRequest,
ValidationException => HttpStatusCode.BadRequest,
FormatException => HttpStatusCode.BadRequest,
InvalidOperationException => HttpStatusCode.BadRequest,
// 401 Unauthorized
UnauthorizedAccessException => HttpStatusCode.Unauthorized,
// 403 Forbidden
ForbiddenException => HttpStatusCode.Forbidden,
// 404 Not Found
KeyNotFoundException => HttpStatusCode.NotFound,
FileNotFoundException => HttpStatusCode.NotFound,
DirectoryNotFoundException => HttpStatusCode.NotFound,
NotFoundException => HttpStatusCode.NotFound,
// 408 Request Timeout
TimeoutException => HttpStatusCode.RequestTimeout,
// 409 Conflict
ConflictException => HttpStatusCode.Conflict,
// 429 Too Many Requests
RateLimitExceededException => HttpStatusCode.TooManyRequests,
// 501 Not Implemented
NotImplementedException => HttpStatusCode.NotImplemented,
// 503 Service Unavailable
ServiceUnavailableException => HttpStatusCode.ServiceUnavailable,
// 500 Internal Server Error (default)
_ => HttpStatusCode.InternalServerError
};
// Log the error with appropriate severity based on status code
var isServerError = (int)status >= 500;
if (isServerError)
{
status = HttpStatusCode.NotImplemented;
}
else if (exceptionType == typeof(UnauthorizedAccessException))
{
status = HttpStatusCode.Unauthorized;
}
else if (exceptionType == typeof(ArgumentException))
{
status = HttpStatusCode.Unauthorized;
}
else if (exceptionType == typeof(KeyNotFoundException))
{
status = HttpStatusCode.Unauthorized;
_logger.LogError(exception, "Server Error: {StatusCode} on {Path}", (int)status, context.Request.Path);
}
else
{
status = HttpStatusCode.InternalServerError;
_logger.LogWarning(exception, "Client Error: {StatusCode} on {Path}", (int)status, context.Request.Path);
}
var message = exception.Message;
var stackTrace = exception.StackTrace;
var exceptionResult = JsonSerializer.Serialize(new { error = message, stackTrace });
// Capture exception in Sentry with request context
var sentryId = SentrySdk.CaptureException(exception, scope =>
{
// Add HTTP request information
scope.SetTag("http.method", context.Request.Method);
scope.SetTag("http.url", context.Request.Path);
// Add request details
scope.SetExtra("query_string", context.Request.QueryString.ToString());
// Add custom tags to help with filtering
scope.SetTag("error_type", exception.GetType().Name);
scope.SetTag("status_code", ((int)status).ToString());
scope.SetTag("host", context.Request.Host.ToString());
scope.SetTag("path", context.Request.Path.ToString());
// Add any correlation IDs if available
if (context.Request.Headers.TryGetValue("X-Correlation-ID", out var correlationId))
{
scope.SetTag("correlation_id", correlationId.ToString());
}
// Additional context based on exception type
if (exception is ValidationException)
{
scope.SetTag("error_category", "validation");
}
else if (exception is NotFoundException)
{
scope.SetTag("error_category", "not_found");
}
// Add additional context from exception data if available
foreach (var key in exception.Data.Keys)
{
if (key is string keyStr && exception.Data[key] != null)
{
scope.SetExtra(keyStr, exception.Data[key].ToString());
}
}
// Add breadcrumb for the request
scope.AddBreadcrumb(
message: $"Request to {context.Request.Path}",
category: "request",
level: BreadcrumbLevel.Info
);
});
// Use a more user-friendly error message in production
if (Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") == "Production")
{
// For 5xx errors, use a generic message
if (isServerError)
{
errorMessage = "An unexpected error occurred. Our team has been notified.";
}
else
{
// For 4xx errors, keep the original message since it's likely helpful for the user
errorMessage = exception.Message;
}
}
else
{
errorMessage = exception.Message;
}
// Create the error response
var errorResponse = new ErrorResponse
{
StatusCode = (int)status,
Message = errorMessage,
TraceId = sentryId.ToString()
};
// Only include stack trace in development environment
if (Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") != "Production")
{
errorResponse.StackTrace = exception.StackTrace;
}
var result = JsonSerializer.Serialize(errorResponse, new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
});
context.Response.ContentType = "application/json";
context.Response.StatusCode = (int)status;
return context.Response.WriteAsync(exceptionResult);
return context.Response.WriteAsync(result);
}
// Custom error response class
private class ErrorResponse
{
public int StatusCode { get; set; }
public string Message { get; set; }
public string TraceId { get; set; }
public string StackTrace { get; set; }
}
}

View File

@@ -0,0 +1,94 @@
using System.Text;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Sentry;
namespace Managing.Core.Middleawares;
public class SentryDiagnosticsMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<SentryDiagnosticsMiddleware> _logger;
public SentryDiagnosticsMiddleware(RequestDelegate next, ILogger<SentryDiagnosticsMiddleware> logger)
{
_next = next;
_logger = logger;
}
public async Task InvokeAsync(HttpContext context)
{
// Only activate for the /api/sentry-diagnostics endpoint
if (context.Request.Path.StartsWithSegments("/api/sentry-diagnostics"))
{
await HandleDiagnosticsRequest(context);
return;
}
await _next(context);
}
private async Task HandleDiagnosticsRequest(HttpContext context)
{
var response = new StringBuilder();
response.AppendLine("Sentry Diagnostics Report");
response.AppendLine("========================");
response.AppendLine($"Timestamp: {DateTime.Now}");
response.AppendLine();
// Check if Sentry is initialized
response.AppendLine("## Sentry SDK Status");
response.AppendLine($"Sentry Enabled: {SentrySdk.IsEnabled}");
response.AppendLine($"Application Environment: {Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT")}");
response.AppendLine();
// Send a test event
response.AppendLine("## Test Event");
try
{
var id = SentrySdk.CaptureMessage($"Diagnostics test from {context.Request.Host} at {DateTime.Now}",
SentryLevel.Info);
response.AppendLine($"Test Event ID: {id}");
response.AppendLine(
"Test event was sent to Sentry. Check your Sentry dashboard to confirm it was received.");
// Try to send an exception too
try
{
throw new Exception("Test exception from diagnostics middleware");
}
catch (Exception ex)
{
var exceptionId = SentrySdk.CaptureException(ex);
response.AppendLine($"Test Exception ID: {exceptionId}");
}
}
catch (Exception ex)
{
response.AppendLine($"Error sending test event: {ex.Message}");
response.AppendLine(ex.StackTrace);
}
response.AppendLine();
response.AppendLine("## Connectivity Check");
response.AppendLine("If events are not appearing in Sentry, check the following:");
response.AppendLine("1. Verify your DSN is correct in appsettings.json");
response.AppendLine("2. Ensure your network allows outbound HTTPS connections to sentry.io");
response.AppendLine("3. Check Sentry server logs for any ingestion issues");
response.AppendLine("4. Verify your Sentry project is correctly configured to receive events");
// Return the diagnostic information
context.Response.ContentType = "text/plain";
await context.Response.WriteAsync(response.ToString());
}
}
// Extension method used to add the middleware to the HTTP request pipeline.
public static class SentryDiagnosticsMiddlewareExtensions
{
public static IApplicationBuilder UseSentryDiagnostics(this IApplicationBuilder builder)
{
return builder.UseMiddleware<SentryDiagnosticsMiddleware>();
}
}

View File

@@ -29,3 +29,5 @@ PRIVY_APP_SECRET=25wwYu5AgxArU7djgvQEuioc9YSdGY3WN3r1dmXftPfH33KfGVfzopW3vqoPFjy
PRIVY_AUTHORIZATION_KEY=wallet-auth:MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQggpJ65PCo4E6NYpY867AyE6p1KxOrs8LJqHZw+t+076yhRANCAAS2EM23CtIfQRmHWTxcqb1j5yfrVePjZyBOZZ2RoPZHb9bDGLos206fTuVA3zgLVomlOoHTeYifkBASCn9Mfg3b
API_URL=http://localhost:5000
ARBITRUM_RPC_URL=https://arbitrum-one.publicnode.com
SENTRY_DSN=https://4b88eba622584ab1af8d0611960e6a2f@bugcenter.apps.managing.live/3
SENTRY_ENVIRONMENT=production

View File

@@ -0,0 +1,73 @@
# Sentry Integration for Web3Proxy
This project includes Sentry for error monitoring and logging. Sentry helps track application errors in real-time and provides detailed information about exceptions that occur during runtime.
## Configuration
Sentry is configured via environment variables in your `.env` file:
```bash
# Sentry configuration
SENTRY_DSN=your-sentry-dsn
SENTRY_ENVIRONMENT=development # or production, staging, etc.
```
## How It Works
1. Sentry is initialized during application startup in `plugins/external/sentry.ts`
2. All unhandled exceptions and errors are automatically captured and sent to Sentry
3. The Fastify instance is decorated with a `sentry` property, which gives you access to the Sentry SDK
## Testing Endpoints
The application provides several endpoints to test Sentry functionality:
- `/api/sentry-diagnostics` - Shows the Sentry configuration status and sends test events
- `/test-sentry` - Triggers and captures a handled exception
- `/test-sentry-uncaught` - Triggers an unhandled exception (useful for testing error handlers)
## Using Sentry in Your Routes
You can manually capture events and exceptions in your routes:
```typescript
// Capture a message
fastify.get('/example', async (request, reply) => {
// Log a message to Sentry
fastify.sentry.captureMessage('User visited example page');
// Continue with your route logic
return { message: 'Example page' };
});
// Capture an exception
fastify.get('/example-error', async (request, reply) => {
try {
// Some code that might fail
throw new Error('Something went wrong');
} catch (error) {
// Capture the exception
fastify.sentry.captureException(error);
// Respond to the client
return { message: 'An error occurred, but we've logged it' };
}
});
```
## Troubleshooting
If events aren't appearing in your Sentry dashboard:
1. Verify your DSN is correct in your `.env` file
2. Ensure your network allows outbound HTTPS connections to sentry.io
3. Check that the environment is correctly set
4. Visit `/api/sentry-diagnostics` to run a diagnostic test
## NPM Installation
The Sentry SDK is installed as a dependency via:
```bash
npm install @sentry/node
```

File diff suppressed because it is too large Load Diff

View File

@@ -43,6 +43,7 @@
"@fastify/type-provider-typebox": "^5.0.0",
"@fastify/under-pressure": "^9.0.1",
"@privy-io/server-auth": "^1.18.12",
"@sentry/node": "^8.55.0",
"@sinclair/typebox": "^0.34.11",
"canonicalize": "^2.0.0",
"concurrently": "^9.0.1",

View File

@@ -22,10 +22,7 @@ export default async function serviceApp (
delete opts.skipOverride // This option only serves testing purpose
// This loads all external plugins defined in plugins/external
// those should be registered first as your custom plugins might depend on them
// await fastify.register(fastifyAutoload, {
// dir: path.join(import.meta.dirname, 'plugins/external'),
// options: { ...opts }
// })
// This loads all your custom plugins defined in plugins/custom
// those should be support plugins that are reused

View File

@@ -0,0 +1,75 @@
import * as Sentry from '@sentry/node';
import fp from 'fastify-plugin';
import { FastifyPluginAsync, FastifyRequest, FastifyReply } from 'fastify';
interface SentryPluginOptions {
dsn?: string;
environment?: string;
debug?: boolean;
}
export const autoConfig = {
// Set default options for the plugin
dsn: process.env.SENTRY_DSN,
environment: process.env.SENTRY_ENVIRONMENT || 'development',
debug: false
};
const sentryPlugin: FastifyPluginAsync<SentryPluginOptions> = async (fastify, options) => {
const {
dsn = fastify.config?.SENTRY_DSN || process.env.SENTRY_DSN,
environment = fastify.config?.SENTRY_ENVIRONMENT || process.env.SENTRY_ENVIRONMENT || 'development',
debug = false
} = options;
if (!dsn) {
fastify.log.warn('Sentry DSN not provided, skipping initialization');
return;
}
// Initialize Sentry with minimal configuration
Sentry.init({
dsn,
environment,
debug
});
// Add Sentry error handler, but don't override the existing one
const originalErrorHandler = fastify.errorHandler;
fastify.setErrorHandler((error, request, reply) => {
// Capture the exception with request details
Sentry.captureException(error, {
extra: {
method: request.method,
url: request.url,
params: request.params,
query: request.query,
ip: request.ip
}
});
if (originalErrorHandler) {
return originalErrorHandler(error, request, reply);
}
reply.status(500).send({ error: 'Internal Server Error' });
});
// Add Sentry to fastify instance
fastify.decorate('sentry', Sentry);
// Log initialization success
fastify.log.info(`Sentry initialized for environment: ${environment}`);
};
// Augment FastifyInstance to include sentry
declare module 'fastify' {
interface FastifyInstance {
sentry: typeof Sentry;
}
}
export default fp(sentryPlugin, {
name: 'fastify-sentry',
fastify: '5.x'
});

View File

@@ -12,6 +12,8 @@ declare module 'fastify' {
PRIVY_APP_ID: string;
PRIVY_APP_SECRET: string;
PRIVY_AUTHORIZATION_KEY: string;
SENTRY_DSN: string;
SENTRY_ENVIRONMENT: string;
};
}
}
@@ -20,6 +22,8 @@ const schema = {
type: 'object',
required: [
'PORT',
'COOKIE_SECRET',
'COOKIE_NAME'
],
properties: {
PORT: {
@@ -30,7 +34,8 @@ const schema = {
type: 'string'
},
COOKIE_NAME: {
type: 'string'
type: 'string',
default: 'web3proxy'
},
COOKIE_SECURED: {
type: 'boolean',
@@ -39,6 +44,13 @@ const schema = {
RATE_LIMIT_MAX: {
type: 'number',
default: 100
},
SENTRY_DSN: {
type: 'string'
},
SENTRY_ENVIRONMENT: {
type: 'string',
default: 'development'
}
}
}
@@ -72,7 +84,7 @@ export const autoConfig = {
export default fp(async (fastify) => {
const schema = {
type: 'object',
required: ['PRIVY_APP_ID', 'PRIVY_APP_SECRET', 'PRIVY_AUTHORIZATION_KEY'],
required: ['PRIVY_APP_ID', 'PRIVY_APP_SECRET', 'PRIVY_AUTHORIZATION_KEY', 'COOKIE_SECRET'],
properties: {
PRIVY_APP_ID: {
type: 'string'
@@ -82,6 +94,23 @@ export default fp(async (fastify) => {
},
PRIVY_AUTHORIZATION_KEY: {
type: 'string'
},
COOKIE_SECRET: {
type: 'string'
},
COOKIE_NAME: {
type: 'string',
default: 'web3proxy'
},
COOKIE_SECURED: {
type: 'boolean',
default: false
},
SENTRY_DSN: {
type: 'string'
},
SENTRY_ENVIRONMENT: {
type: 'string'
}
}
}

View File

@@ -15,16 +15,34 @@ declare module 'fastify' {
* @see {@link https://github.com/fastify/session}
*/
export default fp(async (fastify) => {
// Get cookie secret from config or use a default for development
const cookieSecret = fastify.config?.COOKIE_SECRET || process.env.COOKIE_SECRET || 'development-secret-for-session-do-not-use-in-production'
if (!cookieSecret) {
fastify.log.warn('No COOKIE_SECRET found in config or environment. Using an insecure default secret. DO NOT USE IN PRODUCTION!')
} else if (cookieSecret === 'development-secret-for-session-do-not-use-in-production') {
fastify.log.warn('Using the default insecure cookie secret. DO NOT USE IN PRODUCTION!')
}
// Get cookie name from config or use a default
const cookieName = fastify.config?.COOKIE_NAME || process.env.COOKIE_NAME || 'web3proxy'
// Get cookie secure setting or default to false for development
const cookieSecured = fastify.config?.COOKIE_SECURED !== undefined
? fastify.config.COOKIE_SECURED
: (process.env.COOKIE_SECURED === 'true' || false)
fastify.register(fastifyCookie)
fastify.register(fastifySession, {
secret: fastify.config.COOKIE_SECRET,
cookieName: fastify.config.COOKIE_NAME,
secret: cookieSecret,
cookieName: cookieName,
cookie: {
secure: fastify.config.COOKIE_SECURED,
secure: cookieSecured,
httpOnly: true,
maxAge: 1800000
maxAge: 1800000 // 30 minutes
}
})
}, {
name: 'session'
name: 'session',
dependencies: ['env-config'] // Make sure env-config runs first
})

View File

@@ -1,6 +1,7 @@
import {FastifyPluginAsyncTypebox} from '@fastify/type-provider-typebox'
import {Type} from '@sinclair/typebox'
import { TradeDirection } from '../../../generated/ManagingApiTypes'
import { handleError } from '../../../utils/errorHandler.js'
const plugin: FastifyPluginAsyncTypebox = async (fastify) => {
// Define route to open a position
@@ -45,12 +46,7 @@ const plugin: FastifyPluginAsyncTypebox = async (fastify) => {
return result
} catch (error) {
fastify.log.error(error)
reply.status(500)
return {
success: false,
error: error instanceof Error ? error.message : 'An unknown error occurred'
}
return handleError(request, reply, error, 'gmx/open-position');
}
})
@@ -81,12 +77,7 @@ const plugin: FastifyPluginAsyncTypebox = async (fastify) => {
return result
} catch (error) {
fastify.log.error(error)
reply.status(500)
return {
success: false,
error: error instanceof Error ? error.message : 'An unknown error occurred'
}
return handleError(request, reply, error, 'gmx/cancel-orders');
}
})
@@ -120,12 +111,7 @@ const plugin: FastifyPluginAsyncTypebox = async (fastify) => {
return result;
} catch (error) {
fastify.log.error(error)
reply.status(500)
return {
success: false,
error: error instanceof Error ? error.message : 'An unknown error occurred'
}
return handleError(request, reply, error, 'gmx/close-position');
}
})
@@ -157,12 +143,7 @@ const plugin: FastifyPluginAsyncTypebox = async (fastify) => {
return result
} catch (error) {
fastify.log.error(error)
reply.status(500)
return {
success: false,
error: error instanceof Error ? error.message : 'An unknown error occurred'
}
return handleError(request, reply, error, 'gmx/trades');
}
})
@@ -192,12 +173,7 @@ const plugin: FastifyPluginAsyncTypebox = async (fastify) => {
return result
} catch (error) {
fastify.log.error(error)
reply.status(500)
return {
success: false,
error: error instanceof Error ? error.message : 'An unknown error occurred'
}
return handleError(request, reply, error, 'gmx/positions');
}
})
}

View File

@@ -1,10 +1,15 @@
import { FastifyInstance } from 'fastify'
import { handleError } from '../../utils/errorHandler.js'
export default async function (fastify: FastifyInstance) {
fastify.get('/', ({ protocol, hostname }) => {
return {
message:
`Hello ! See documentation at ${protocol}://${hostname}/documentation`
fastify.get('/', async (request, reply) => {
try {
return {
message:
`Hello ! See documentation at ${request.protocol}://${request.hostname}/documentation`
}
} catch (error) {
return handleError(request, reply, error, 'api-root');
}
})
}

View File

@@ -1,4 +1,5 @@
import {FastifyPluginAsyncTypebox, Type} from '@fastify/type-provider-typebox'
import { handleError } from '../../../utils/errorHandler.js'
const plugin: FastifyPluginAsyncTypebox = async (fastify) => {
fastify.post(
@@ -29,8 +30,12 @@ const plugin: FastifyPluginAsyncTypebox = async (fastify) => {
}
},
async function (request, reply) {
const { walletId, message, address } = request.body;
return request.signPrivyMessage(reply, walletId, message, address);
try {
const { walletId, message, address } = request.body;
return await request.signPrivyMessage(reply, walletId, message, address);
} catch (error) {
return handleError(request, reply, error, 'privy/sign-message');
}
}
)
@@ -62,8 +67,12 @@ const plugin: FastifyPluginAsyncTypebox = async (fastify) => {
}
},
async function (request, reply) {
const { address } = request.body;
return request.initAddress(reply, address);
try {
const { address } = request.body;
return await request.initAddress(reply, address);
} catch (error) {
return handleError(request, reply, error, 'privy/init-address');
}
}
)
}

View File

@@ -1,24 +1,59 @@
import {FastifyPluginAsyncTypebox, Type} from '@fastify/type-provider-typebox'
import { handleError } from '../utils/errorHandler.js'
const plugin: FastifyPluginAsyncTypebox = async (fastify) => {
fastify.get(
'/',
{
schema: {
tags: ['Home'],
description: 'Welcome endpoint that confirms the API is running',
response: {
200: Type.Object({
message: Type.String()
}),
500: Type.Object({
success: Type.Boolean(),
error: Type.String()
})
}
}
},
async function () {
return { message: 'Welcome to the official Web3 Proxy API!' }
async function (request, reply) {
try {
return { message: 'Welcome to the official Web3 Proxy API!' }
} catch (error) {
return handleError(request, reply, error, 'home-root');
}
}
)
// Add health check endpoint
fastify.get('/health', async function () {
return { status: 'ok' }
fastify.get('/health', {
schema: {
tags: ['Health'],
description: 'Health check endpoint that confirms the API is operational',
response: {
200: Type.Object({
status: Type.String(),
timestamp: Type.String(),
version: Type.String()
}),
500: Type.Object({
success: Type.Boolean(),
error: Type.String()
})
}
}
}, async function (request, reply) {
try {
return {
status: 'ok',
timestamp: new Date().toISOString(),
version: process.env.npm_package_version || '1.0.0'
}
} catch (error) {
return handleError(request, reply, error, 'health');
}
})
}

View File

@@ -0,0 +1,92 @@
import {FastifyPluginAsyncTypebox, Type} from '@fastify/type-provider-typebox'
const plugin: FastifyPluginAsyncTypebox = async (fastify) => {
// Diagnostic endpoint for Sentry
fastify.get('/api/sentry-diagnostics', {
schema: {
tags: ['Sentry'],
description: 'Sentry diagnostics endpoint - tests Sentry configuration and connectivity',
response: {
200: Type.String()
}
}
}, async (request, reply) => {
let output = 'Sentry Diagnostics Report\n';
output += '========================\n';
output += `Timestamp: ${new Date().toISOString()}\n\n`;
output += '## Sentry SDK Status\n';
output += `Sentry Enabled: true\n`;
output += `Environment: ${process.env.SENTRY_ENVIRONMENT || 'development'}\n\n`;
output += '## Test Event\n';
try {
const eventId = fastify.sentry.captureMessage(`Diagnostics test from ${request.hostname} at ${new Date().toISOString()}`);
output += `Test Event ID: ${eventId}\n`;
output += 'Test event was sent to Sentry. Check your Sentry dashboard to confirm it was received.\n\n';
try {
throw new Error('Test exception from diagnostics endpoint');
} catch (ex) {
const exceptionId = fastify.sentry.captureException(ex);
output += `Test Exception ID: ${exceptionId}\n`;
}
} catch (ex) {
output += `Error sending test event: ${ex.message}\n`;
output += ex.stack || '';
}
output += '\n## Connectivity Check\n';
output += 'If events are not appearing in Sentry, check the following:\n';
output += '1. Verify your DSN is correct in your .env file\n';
output += '2. Ensure your network allows outbound HTTPS connections to sentry.io\n';
output += '3. Check Sentry server logs for any ingestion issues\n';
output += '4. Verify your Sentry project is correctly configured to receive events\n';
reply.type('text/plain').send(output);
});
// Test endpoint with explicit capture
fastify.get('/test-sentry', {
schema: {
tags: ['Sentry'],
description: 'Tests Sentry error reporting with a handled exception',
response: {
200: Type.Object({
message: Type.String(),
timestamp: Type.String()
})
}
}
}, async (request, reply) => {
try {
throw new Error(`Test exception for Sentry - ${new Date().toISOString()}`);
} catch (ex) {
fastify.sentry.captureException(ex);
console.log(`Captured exception in Sentry: ${ex.message}`);
fastify.sentry.captureMessage('This is a test message from Web3Proxy');
return {
message: 'Error captured by Sentry',
timestamp: new Date().toISOString()
};
}
});
// Test endpoint that throws an uncaught exception
fastify.get('/test-sentry-uncaught', {
schema: {
tags: ['Sentry'],
description: 'Tests Sentry error reporting with an unhandled exception',
response: {
200: Type.Object({
message: Type.String()
})
}
}
}, async () => {
console.log('About to throw an uncaught exception for Sentry to capture');
throw new Error(`Uncaught exception test for Sentry - ${new Date().toISOString()}`);
});
}
export default plugin;

View File

@@ -0,0 +1,78 @@
# Web3Proxy Error Handling Utilities
This directory contains utilities for standardized error handling across the application.
## Error Handling
The error handling utilities provide a consistent way to:
1. Log errors
2. Capture exceptions in Sentry with proper context
3. Return standardized error responses to clients
## Usage
### Using the Error Handler Directly
You can use the `handleError` function directly in your try/catch blocks:
```typescript
import { handleError } from '../utils/errorHandler'
fastify.get('/example', async (request, reply) => {
try {
// Your route logic here
return { success: true, data: result }
} catch (error) {
return handleError(request, reply, error, 'endpoint/path')
}
})
```
### Using the Route Wrapper
For more concise code, use the `createHandler` or `withErrorHandling` functions:
```typescript
import { createHandler } from '../utils/routeWrapper'
// Method 1: Using createHandler
fastify.get('/example', createHandler('endpoint/example', async (request, reply) => {
// Your route logic here - errors are automatically caught and handled
return { success: true, data: result }
}))
// Method 2: Using withErrorHandling
const originalHandler = async (request, reply) => {
// Your route logic here
return { success: true, data: result }
}
fastify.get('/example', withErrorHandling(originalHandler, 'endpoint/example'))
```
## Custom Error Types
The error handler provides several custom error types that map to appropriate HTTP status codes:
- `ValidationError`: For request validation errors (400)
- `NotFoundError`: For resource not found errors (404)
- `UnauthorizedError`: For authentication failures (401)
- `ForbiddenError`: For permission errors (403)
Example:
```typescript
import { ValidationError } from '../utils/errorHandler'
if (!isValid) {
throw new ValidationError('Invalid input parameters')
}
```
## Best Practices
1. Always provide a meaningful endpoint string for tracking in Sentry
2. Use the appropriate error type for better error classification
3. Prefer using the route wrapper for new routes
4. Include relevant context in error messages for easier debugging

View File

@@ -0,0 +1,107 @@
import { FastifyRequest, FastifyReply } from 'fastify';
/**
* Handles errors consistently across the application.
* Logs errors, captures them in Sentry, and returns consistent error responses.
*
* @param request - The FastifyRequest object
* @param reply - The FastifyReply object
* @param error - The error that occurred
* @param endpoint - The endpoint where the error occurred (for better tracing)
* @returns A standardized error response
*/
export async function handleError(
request: FastifyRequest,
reply: FastifyReply,
error: unknown,
endpoint: string
) {
// Ensure status code is set
const statusCode = error instanceof SyntaxError || error instanceof TypeError || error instanceof RangeError
? 400
: 500;
reply.status(statusCode);
// Log the error
request.log.error(error);
// Capture exception in Sentry with relevant context
request.server.sentry.captureException(error, {
extra: {
method: request.method,
url: request.url,
routeParams: request.params,
queryParams: request.query,
bodyParams: request.body,
ip: request.ip,
endpoint: endpoint
}
});
// Return a standardized error response
return {
success: false,
error: error instanceof Error ? error.message : 'An unknown error occurred'
};
}
/**
* Custom error class for validation errors
*/
export class ValidationError extends Error {
constructor(message: string) {
super(message);
this.name = 'ValidationError';
}
}
/**
* Custom error class for not found errors
*/
export class NotFoundError extends Error {
constructor(message: string) {
super(message);
this.name = 'NotFoundError';
}
}
/**
* Custom error class for unauthorized errors
*/
export class UnauthorizedError extends Error {
constructor(message: string) {
super(message);
this.name = 'UnauthorizedError';
}
}
/**
* Custom error class for forbidden errors
*/
export class ForbiddenError extends Error {
constructor(message: string) {
super(message);
this.name = 'ForbiddenError';
}
}
/**
* Gets the appropriate status code for different error types
* @param error The error to analyze
* @returns The appropriate HTTP status code
*/
export function getStatusCodeForError(error: Error): number {
if (error instanceof ValidationError) return 400;
if (error instanceof NotFoundError) return 404;
if (error instanceof UnauthorizedError) return 401;
if (error instanceof ForbiddenError) return 403;
// Default error types
if (error instanceof SyntaxError) return 400;
if (error instanceof TypeError) return 400;
if (error instanceof RangeError) return 400;
// Default to 500 for unhandled errors
return 500;
}

View File

@@ -0,0 +1,35 @@
import { FastifyRequest, FastifyReply } from 'fastify'
import { handleError } from './errorHandler.js'
/**
* Type for route handler functions
*/
type RouteHandler<T = any> = (request: FastifyRequest, reply: FastifyReply) => Promise<T>
/**
* Wraps a route handler with error handling logic
*
* @param handler - The original route handler function
* @param endpoint - The endpoint identifier for tracking
* @returns A wrapped handler with error handling
*/
export function withErrorHandling<T = any>(handler: RouteHandler<T>, endpoint: string): RouteHandler<T> {
return async (request: FastifyRequest, reply: FastifyReply): Promise<T> => {
try {
return await handler(request, reply)
} catch (error) {
return handleError(request, reply, error, endpoint) as T
}
}
}
/**
* Creates a route handler with built-in error handling
*
* @param endpoint - The endpoint identifier for tracking
* @param handlerFn - The handler function logic
* @returns A route handler with error handling
*/
export function createHandler<T = any>(endpoint: string, handlerFn: RouteHandler<T>): RouteHandler<T> {
return withErrorHandling(handlerFn, endpoint)
}