diff --git a/src/Managing.Api.Workers/Managing.Api.Workers.csproj b/src/Managing.Api.Workers/Managing.Api.Workers.csproj
index 5037374..b0ad5c3 100644
--- a/src/Managing.Api.Workers/Managing.Api.Workers.csproj
+++ b/src/Managing.Api.Workers/Managing.Api.Workers.csproj
@@ -17,6 +17,7 @@
+
@@ -33,6 +34,7 @@
+
diff --git a/src/Managing.Api.Workers/Middleware/SentryDiagnosticsMiddleware.cs b/src/Managing.Api.Workers/Middleware/SentryDiagnosticsMiddleware.cs
new file mode 100644
index 0000000..2d5a53a
--- /dev/null
+++ b/src/Managing.Api.Workers/Middleware/SentryDiagnosticsMiddleware.cs
@@ -0,0 +1,90 @@
+using Sentry;
+using System.Text;
+
+namespace Managing.Api.Workers.Middleware
+{
+ public class SentryDiagnosticsMiddleware
+ {
+ private readonly RequestDelegate _next;
+ private readonly ILogger _logger;
+
+ public SentryDiagnosticsMiddleware(RequestDelegate next, ILogger 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();
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Managing.Api.Workers/Program.cs b/src/Managing.Api.Workers/Program.cs
index 469faa5..f882208 100644
--- a/src/Managing.Api.Workers/Program.cs
+++ b/src/Managing.Api.Workers/Program.cs
@@ -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();
app.UseHttpsRedirection();
@@ -173,12 +197,12 @@ app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
endpoints.MapHub("/positionhub");
-
- endpoints.MapHealthChecks("/health", new HealthCheckOptions
+
+ endpoints.MapHealthChecks("/health", new HealthCheckOptions
{
ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse
});
-
+
endpoints.MapHealthChecks("/alive", new HealthCheckOptions
{
Predicate = r => r.Tags.Contains("live"),
diff --git a/src/Managing.Api.Workers/appsettings.json b/src/Managing.Api.Workers/appsettings.json
index 779fe22..170c050 100644
--- a/src/Managing.Api.Workers/appsettings.json
+++ b/src/Managing.Api.Workers/appsettings.json
@@ -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,
diff --git a/src/Managing.Api/Controllers/SentryTestController.cs b/src/Managing.Api/Controllers/SentryTestController.cs
new file mode 100644
index 0000000..13517f2
--- /dev/null
+++ b/src/Managing.Api/Controllers/SentryTestController.cs
@@ -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 _logger;
+
+ public SentryTestController(ILogger 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
+ });
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Managing.Api/Exceptions/CustomExceptions.cs b/src/Managing.Api/Exceptions/CustomExceptions.cs
new file mode 100644
index 0000000..119edb9
--- /dev/null
+++ b/src/Managing.Api/Exceptions/CustomExceptions.cs
@@ -0,0 +1,79 @@
+namespace Managing.Api.Exceptions;
+
+///
+/// Exception thrown when validation fails (maps to 400 Bad Request)
+///
+public class ValidationException : Exception
+{
+ public ValidationException(string message) : base(message)
+ {
+ }
+
+ public ValidationException(string message, Exception innerException) : base(message, innerException)
+ {
+ }
+}
+
+///
+/// Exception thrown when a resource is not found (maps to 404 Not Found)
+///
+public class NotFoundException : Exception
+{
+ public NotFoundException(string message) : base(message)
+ {
+ }
+
+ public NotFoundException(string resourceType, string identifier)
+ : base($"{resourceType} with identifier '{identifier}' was not found.")
+ {
+ }
+}
+
+///
+/// Exception thrown when the user does not have permission (maps to 403 Forbidden)
+///
+public class ForbiddenException : Exception
+{
+ public ForbiddenException(string message) : base(message)
+ {
+ }
+
+ public ForbiddenException() : base("You do not have permission to access this resource.")
+ {
+ }
+}
+
+///
+/// Exception thrown when there is a conflict with the current state (maps to 409 Conflict)
+///
+public class ConflictException : Exception
+{
+ public ConflictException(string message) : base(message)
+ {
+ }
+}
+
+///
+/// Exception thrown when a rate limit is exceeded (maps to 429 Too Many Requests)
+///
+public class RateLimitExceededException : Exception
+{
+ public RateLimitExceededException(string message) : base(message)
+ {
+ }
+
+ public RateLimitExceededException() : base("Rate limit exceeded. Please try again later.")
+ {
+ }
+}
+
+///
+/// Exception thrown when an external service is unavailable (maps to 503 Service Unavailable)
+///
+public class ServiceUnavailableException : Exception
+{
+ public ServiceUnavailableException(string serviceName)
+ : base($"The service '{serviceName}' is currently unavailable. Please try again later.")
+ {
+ }
+}
\ No newline at end of file
diff --git a/src/Managing.Api/Exceptions/GlobalErrorHandlingMiddleware.cs b/src/Managing.Api/Exceptions/GlobalErrorHandlingMiddleware.cs
index 84308bb..fd832d1 100644
--- a/src/Managing.Api/Exceptions/GlobalErrorHandlingMiddleware.cs
+++ b/src/Managing.Api/Exceptions/GlobalErrorHandlingMiddleware.cs
@@ -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 _logger;
+
+ public GlobalErrorHandlingMiddleware(RequestDelegate next, ILogger 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; }
}
}
diff --git a/src/Managing.Api/Exceptions/SentryErrorCapture.cs b/src/Managing.Api/Exceptions/SentryErrorCapture.cs
new file mode 100644
index 0000000..9c603db
--- /dev/null
+++ b/src/Managing.Api/Exceptions/SentryErrorCapture.cs
@@ -0,0 +1,109 @@
+using Sentry;
+
+namespace Managing.Api.Exceptions;
+
+///
+/// Utility class for capturing errors with Sentry across the application
+///
+public static class SentryErrorCapture
+{
+ ///
+ /// Captures an exception in Sentry with additional context
+ ///
+ /// The exception to capture
+ /// A descriptive name for where the error occurred
+ /// Optional dictionary of additional data to include
+ /// The Sentry event ID
+ public static SentryId CaptureException(Exception exception, string contextName, IDictionary 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
+ );
+ });
+ }
+
+ ///
+ /// Enriches an exception with additional context data before throwing
+ ///
+ /// The exception to enrich
+ /// Dictionary of context data to add
+ /// The enriched exception for chaining
+ public static Exception EnrichException(Exception exception, IDictionary contextData)
+ {
+ if (contextData != null)
+ {
+ foreach (var item in contextData)
+ {
+ exception.Data[item.Key] = item.Value;
+ }
+ }
+
+ return exception;
+ }
+
+ ///
+ /// Captures a message in Sentry with additional context
+ ///
+ /// The message to capture
+ /// The severity level
+ /// A descriptive name for where the message originated
+ /// Optional dictionary of additional data to include
+ /// The Sentry event ID
+ public static SentryId CaptureMessage(string message, SentryLevel level, string contextName, IDictionary 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;
+ }
+}
\ No newline at end of file
diff --git a/src/Managing.Api/Managing.Api.csproj b/src/Managing.Api/Managing.Api.csproj
index 182137d..5ba484f 100644
--- a/src/Managing.Api/Managing.Api.csproj
+++ b/src/Managing.Api/Managing.Api.csproj
@@ -18,6 +18,7 @@
+
@@ -34,6 +35,7 @@
+
diff --git a/src/Managing.Api/Middleware/SentryDiagnosticsMiddleware.cs b/src/Managing.Api/Middleware/SentryDiagnosticsMiddleware.cs
new file mode 100644
index 0000000..eee21b5
--- /dev/null
+++ b/src/Managing.Api/Middleware/SentryDiagnosticsMiddleware.cs
@@ -0,0 +1,90 @@
+using Sentry;
+using System.Text;
+
+namespace Managing.Api.Middleware
+{
+ public class SentryDiagnosticsMiddleware
+ {
+ private readonly RequestDelegate _next;
+ private readonly ILogger _logger;
+
+ public SentryDiagnosticsMiddleware(RequestDelegate next, ILogger 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();
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Managing.Api/Program.cs b/src/Managing.Api/Program.cs
index ad3cde5..b0a1a58 100644
--- a/src/Managing.Api/Program.cs
+++ b/src/Managing.Api/Program.cs
@@ -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();
+
+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();
-
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();
app.UseMiddleware();
@@ -196,12 +227,12 @@ app.UseEndpoints(endpoints =>
endpoints.MapHub("/bothub");
endpoints.MapHub("/backtesthub");
endpoints.MapHub("/candlehub");
-
- endpoints.MapHealthChecks("/health", new HealthCheckOptions
+
+ endpoints.MapHealthChecks("/health", new HealthCheckOptions
{
ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse
});
-
+
endpoints.MapHealthChecks("/alive", new HealthCheckOptions
{
Predicate = r => r.Tags.Contains("live"),
diff --git a/src/Managing.Api/README-ERROR-HANDLING.md b/src/Managing.Api/README-ERROR-HANDLING.md
new file mode 100644
index 0000000..3cd9b04
--- /dev/null
+++ b/src/Managing.Api/README-ERROR-HANDLING.md
@@ -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 {
+ { "userId", user.Id },
+ { "operation", "ProcessImport" }
+});
+
+// Enrich an exception before throwing
+throw SentryErrorCapture.EnrichException(new ValidationException("Invalid data"),
+ new Dictionary {
+ { "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
\ No newline at end of file
diff --git a/src/Managing.Api/README-SENTRY.md b/src/Managing.Api/README-SENTRY.md
new file mode 100644
index 0000000..ab973de
--- /dev/null
+++ b/src/Managing.Api/README-SENTRY.md
@@ -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.
\ No newline at end of file
diff --git a/src/Managing.Api/appsettings.Development.json b/src/Managing.Api/appsettings.Development.json
index c566931..2433dc4 100644
--- a/src/Managing.Api/appsettings.Development.json
+++ b/src/Managing.Api/appsettings.Development.json
@@ -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"
}
}
\ No newline at end of file
diff --git a/src/Managing.Api/appsettings.Oda.json b/src/Managing.Api/appsettings.Oda.json
index 12661e9..cf0af0d 100644
--- a/src/Managing.Api/appsettings.Oda.json
+++ b/src/Managing.Api/appsettings.Oda.json
@@ -24,6 +24,9 @@
"ElasticConfiguration": {
"Uri": "http://elasticsearch:9200"
},
+ "Sentry": {
+ "Dsn": "https://698e00d7cb404b049aff3881e5a47f6b@bugcenter.apps.managing.live/1"
+ },
"Discord": {
"ApplicationId": "",
"PublicKey": "",
diff --git a/src/Managing.Api/appsettings.Prod.json b/src/Managing.Api/appsettings.Prod.json
index ab0e66a..9cfb836 100644
--- a/src/Managing.Api/appsettings.Prod.json
+++ b/src/Managing.Api/appsettings.Prod.json
@@ -27,5 +27,8 @@
"ElasticConfiguration": {
"Uri": "http://elasticsearch:9200"
},
+ "Sentry": {
+ "Dsn": "https://698e00d7cb404b049aff3881e5a47f6b@bugcenter.apps.managing.live/1"
+ },
"AllowedHosts": "*"
}
\ No newline at end of file
diff --git a/src/Managing.Api/appsettings.json b/src/Managing.Api/appsettings.json
index afd77b0..1f8c47b 100644
--- a/src/Managing.Api/appsettings.json
+++ b/src/Managing.Api/appsettings.json
@@ -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",
diff --git a/src/Managing.Application/Bots/TradingBot.cs b/src/Managing.Application/Bots/TradingBot.cs
index 8dd4d85..7d9983b 100644
--- a/src/Managing.Application/Bots/TradingBot.cs
+++ b/src/Managing.Application/Bots/TradingBot.cs
@@ -510,12 +510,14 @@ public class TradingBot : Bot, ITradingBot
var position = await new OpenPositionCommandHandler(ExchangeService, AccountService, TradingService)
.Handle(command);
+
if (position != null)
{
+ position.SignalIdentifier = signal.Identifier;
+ Positions.Add(position);
+
if (position.Open.Status != TradeStatus.Cancelled)
{
- position.SignalIdentifier = signal.Identifier;
- Positions.Add(position);
SetSignalStatus(signal.Identifier, SignalStatus.PositionOpen);
if (!IsForBacktest)
@@ -534,8 +536,11 @@ public class TradingBot : Bot, ITradingBot
}
catch (Exception ex)
{
+ // Keep signal open for debug purpose
+ //SetSignalStatus(signal.Identifier, SignalStatus.Expired);
SetSignalStatus(signal.Identifier, SignalStatus.Expired);
- await LogWarning($"Cannot open trade : {ex.Message}");
+
+ await LogWarning($"Cannot open trade : {ex.Message}, stackTrace : {ex.StackTrace}");
}
}
}
diff --git a/src/Managing.Application/Shared/Behaviours/UnhandledExceptionBehaviour.cs b/src/Managing.Application/Shared/Behaviours/UnhandledExceptionBehaviour.cs
index dc07235..10a2302 100644
--- a/src/Managing.Application/Shared/Behaviours/UnhandledExceptionBehaviour.cs
+++ b/src/Managing.Application/Shared/Behaviours/UnhandledExceptionBehaviour.cs
@@ -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;
}
}
diff --git a/src/Managing.Application/Trading/OpenPositionCommandHandler.cs b/src/Managing.Application/Trading/OpenPositionCommandHandler.cs
index 3a91ad8..df3f625 100644
--- a/src/Managing.Application/Trading/OpenPositionCommandHandler.cs
+++ b/src/Managing.Application/Trading/OpenPositionCommandHandler.cs
@@ -42,11 +42,7 @@ namespace Managing.Application.Trading
? request.Price.Value
: exchangeService.GetPrice(account, request.Ticker, DateTime.Now);
var quantity = balanceAtRisk / price;
- var fee = request.IsForPaperTrading
- ? request.Fee.GetValueOrDefault()
- : tradingService.GetFee(account, request.IsForPaperTrading);
-
- var expectedStatus = GetExpectedStatus(request);
+ // var expectedStatus = GetExpectedStatus(request);
// position.Open = TradingPolicies.OpenPosition(expectedStatus).Execute(async () => { });
var openPrice = request.IsForPaperTrading || request.Price.HasValue
@@ -70,7 +66,7 @@ namespace Managing.Application.Trading
stopLossPrice: stopLossPrice, // Pass determined SL price
takeProfitPrice: takeProfitPrice); // Pass determined TP price
- trade.Fee = TradingHelpers.GetFeeAmount(fee, openPrice * quantity, account.Exchange);
+ //trade.Fee = TradingHelpers.GetFeeAmount(fee, openPrice * quantity, account.Exchange);
position.Open = trade;
var closeDirection = request.Direction == TradeDirection.Long
@@ -88,8 +84,8 @@ namespace Managing.Application.Trading
request.Date,
TradeStatus.Requested);
- position.StopLoss.Fee = TradingHelpers.GetFeeAmount(fee,
- position.StopLoss.Price * position.StopLoss.Quantity, account.Exchange);
+ // position.StopLoss.Fee = TradingHelpers.GetFeeAmount(fee,
+ // position.StopLoss.Price * position.StopLoss.Quantity, account.Exchange);
// Take profit - Use the determined price
position.TakeProfit1 = exchangeService.BuildEmptyTrade(
diff --git a/src/Managing.Core/Exceptions/CustomExceptions.cs b/src/Managing.Core/Exceptions/CustomExceptions.cs
new file mode 100644
index 0000000..d914046
--- /dev/null
+++ b/src/Managing.Core/Exceptions/CustomExceptions.cs
@@ -0,0 +1,78 @@
+namespace Managing.Core.Exceptions;
+
+///
+/// Exception thrown when validation fails (maps to 400 Bad Request)
+///
+public class ValidationException : Exception
+{
+ public ValidationException(string message) : base(message)
+ {
+ }
+
+ public ValidationException(string message, Exception innerException) : base(message, innerException)
+ {
+ }
+}
+
+///
+/// Exception thrown when a resource is not found (maps to 404 Not Found)
+///
+public class NotFoundException : Exception
+{
+ public NotFoundException(string message) : base(message)
+ {
+ }
+
+ public NotFoundException(string resourceType, string identifier)
+ : base($"{resourceType} with identifier '{identifier}' was not found.")
+ {
+ }
+}
+
+///
+/// Exception thrown when the user does not have permission (maps to 403 Forbidden)
+///
+public class ForbiddenException : Exception
+{
+ public ForbiddenException(string message) : base(message)
+ {
+ }
+
+ public ForbiddenException() : base("You do not have permission to access this resource.")
+ {
+ }
+}
+
+///
+/// Exception thrown when there is a conflict with the current state (maps to 409 Conflict)
+///
+public class ConflictException : Exception
+{
+ public ConflictException(string message) : base(message)
+ {
+ }
+}
+
+///
+/// Exception thrown when a rate limit is exceeded (maps to 429 Too Many Requests)
+///
+public class RateLimitExceededException : Exception
+{
+ public RateLimitExceededException(string message) : base(message)
+ {
+ }
+
+ public RateLimitExceededException() : base("Rate limit exceeded. Please try again later.")
+ {
+ }
+}
+
+///
+/// Exception thrown when an external service is unavailable (maps to 503 Service Unavailable)
+///
+public class ServiceUnavailableException : Exception
+{
+ public ServiceUnavailableException(string message) : base(message)
+ {
+ }
+}
\ No newline at end of file
diff --git a/src/Managing.Core/Exceptions/README.md b/src/Managing.Core/Exceptions/README.md
new file mode 100644
index 0000000..be3a474
--- /dev/null
+++ b/src/Managing.Core/Exceptions/README.md
@@ -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 {
+ { "userId", user.Id },
+ { "operation", "ProcessImport" }
+});
+
+// Enrich an exception before throwing
+throw SentryErrorCapture.EnrichException(new ValidationException("Invalid data"),
+ new Dictionary {
+ { "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
\ No newline at end of file
diff --git a/src/Managing.Core/Exceptions/SentryErrorCapture.cs b/src/Managing.Core/Exceptions/SentryErrorCapture.cs
new file mode 100644
index 0000000..0c13abc
--- /dev/null
+++ b/src/Managing.Core/Exceptions/SentryErrorCapture.cs
@@ -0,0 +1,109 @@
+using Sentry;
+
+namespace Managing.Core.Exceptions;
+
+///
+/// Utility class for capturing errors with Sentry across the application
+///
+public static class SentryErrorCapture
+{
+ ///
+ /// Captures an exception in Sentry with additional context
+ ///
+ /// The exception to capture
+ /// A descriptive name for where the error occurred
+ /// Optional dictionary of additional data to include
+ /// The Sentry event ID
+ public static SentryId CaptureException(Exception exception, string contextName, IDictionary 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
+ );
+ });
+ }
+
+ ///
+ /// Enriches an exception with additional context data before throwing
+ ///
+ /// The exception to enrich
+ /// Dictionary of context data to add
+ /// The enriched exception for chaining
+ public static Exception EnrichException(Exception exception, IDictionary contextData)
+ {
+ if (contextData != null)
+ {
+ foreach (var item in contextData)
+ {
+ exception.Data[item.Key] = item.Value;
+ }
+ }
+
+ return exception;
+ }
+
+ ///
+ /// Captures a message in Sentry with additional context
+ ///
+ /// The message to capture
+ /// The severity level
+ /// A descriptive name for where the message originated
+ /// Optional dictionary of additional data to include
+ /// The Sentry event ID
+ public static SentryId CaptureMessage(string message, SentryLevel level, string contextName, IDictionary 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;
+ }
+}
\ No newline at end of file
diff --git a/src/Managing.Core/Managing.Core.csproj b/src/Managing.Core/Managing.Core.csproj
index 57170d6..6926c45 100644
--- a/src/Managing.Core/Managing.Core.csproj
+++ b/src/Managing.Core/Managing.Core.csproj
@@ -6,14 +6,11 @@
AnyCPU;x64
-
-
-
-
-
-
+
+
+
diff --git a/src/Managing.Core/Middleawares/GlobalErrorHandlingMiddleware.cs b/src/Managing.Core/Middleawares/GlobalErrorHandlingMiddleware.cs
index 4ce8564..0402906 100644
--- a/src/Managing.Core/Middleawares/GlobalErrorHandlingMiddleware.cs
+++ b/src/Managing.Core/Middleawares/GlobalErrorHandlingMiddleware.cs
@@ -1,18 +1,23 @@
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;
-
- public GlobalErrorHandlingMiddleware(RequestDelegate next)
+ private readonly ILogger _logger;
+
+ public GlobalErrorHandlingMiddleware(RequestDelegate next, ILogger logger)
{
_next = next;
+ _logger = logger;
}
-
+
public async Task Invoke(HttpContext context)
{
try
@@ -24,43 +29,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();
-
- if (exceptionType == typeof(Exception))
+ string errorMessage;
+
+ // 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; }
}
}
\ No newline at end of file
diff --git a/src/Managing.Core/Middleawares/SentryDiagnosticsMiddleware.cs b/src/Managing.Core/Middleawares/SentryDiagnosticsMiddleware.cs
new file mode 100644
index 0000000..c7bfd80
--- /dev/null
+++ b/src/Managing.Core/Middleawares/SentryDiagnosticsMiddleware.cs
@@ -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 _logger;
+
+ public SentryDiagnosticsMiddleware(RequestDelegate next, ILogger 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();
+ }
+}
\ No newline at end of file
diff --git a/src/Managing.Infrastructure.Web3/Services/Web3ProxyService.cs b/src/Managing.Infrastructure.Web3/Services/Web3ProxyService.cs
index 35c40af..9d9e8af 100644
--- a/src/Managing.Infrastructure.Web3/Services/Web3ProxyService.cs
+++ b/src/Managing.Infrastructure.Web3/Services/Web3ProxyService.cs
@@ -92,7 +92,7 @@ namespace Managing.Infrastructure.Evm.Services
endpoint = $"/{endpoint}";
}
- var url = $"{_settings.BaseUrl}gmx{endpoint}";
+ var url = $"{_settings.BaseUrl}/api/gmx{endpoint}";
try
{
diff --git a/src/Managing.Web3Proxy/.env b/src/Managing.Web3Proxy/.env
index 576d1bc..b3eae70 100644
--- a/src/Managing.Web3Proxy/.env
+++ b/src/Managing.Web3Proxy/.env
@@ -28,4 +28,6 @@ PRIVY_APP_ID=cm7u09v0u002zrkuf2yjjr58p
PRIVY_APP_SECRET=25wwYu5AgxArU7djgvQEuioc9YSdGY3WN3r1dmXftPfH33KfGVfzopW3vqoPFjy1b8wS2gkDDZ9iQ8yxSo9Vi4iN
PRIVY_AUTHORIZATION_KEY=wallet-auth:MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQggpJ65PCo4E6NYpY867AyE6p1KxOrs8LJqHZw+t+076yhRANCAAS2EM23CtIfQRmHWTxcqb1j5yfrVePjZyBOZZ2RoPZHb9bDGLos206fTuVA3zgLVomlOoHTeYifkBASCn9Mfg3b
API_URL=http://localhost:5000
-ARBITRUM_RPC_URL=https://arbitrum-one.publicnode.com
\ No newline at end of file
+ARBITRUM_RPC_URL=https://arbitrum-one.publicnode.com
+SENTRY_DSN=https://4b88eba622584ab1af8d0611960e6a2f@bugcenter.apps.managing.live/3
+SENTRY_ENVIRONMENT=production
\ No newline at end of file
diff --git a/src/Managing.Web3Proxy/SENTRY.md b/src/Managing.Web3Proxy/SENTRY.md
new file mode 100644
index 0000000..bd20e0b
--- /dev/null
+++ b/src/Managing.Web3Proxy/SENTRY.md
@@ -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
+```
\ No newline at end of file
diff --git a/src/Managing.Web3Proxy/package-lock.json b/src/Managing.Web3Proxy/package-lock.json
index 7ef26d8..5465cff 100644
--- a/src/Managing.Web3Proxy/package-lock.json
+++ b/src/Managing.Web3Proxy/package-lock.json
@@ -24,6 +24,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",
@@ -804,6 +805,626 @@
"node": ">=12.4.0"
}
},
+ "node_modules/@opentelemetry/api": {
+ "version": "1.9.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz",
+ "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=8.0.0"
+ }
+ },
+ "node_modules/@opentelemetry/api-logs": {
+ "version": "0.57.2",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.57.2.tgz",
+ "integrity": "sha512-uIX52NnTM0iBh84MShlpouI7UKqkZ7MrUszTmaypHBu4r7NofznSnQRfJ+uUeDtQDj6w8eFGg5KBLDAwAPz1+A==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/api": "^1.3.0"
+ },
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/@opentelemetry/context-async-hooks": {
+ "version": "1.30.1",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/context-async-hooks/-/context-async-hooks-1.30.1.tgz",
+ "integrity": "sha512-s5vvxXPVdjqS3kTLKMeBMvop9hbWkwzBpu+mUO2M7sZtlkyDJGwFe33wRKnbaYDo8ExRVBIIdwIGrqpxHuKttA==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=14"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": ">=1.0.0 <1.10.0"
+ }
+ },
+ "node_modules/@opentelemetry/core": {
+ "version": "1.30.1",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.30.1.tgz",
+ "integrity": "sha512-OOCM2C/QIURhJMuKaekP3TRBxBKxG/TWWA0TL2J6nXUtDnuCtccy49LUJF8xPFXMX+0LMcxFpCo8M9cGY1W6rQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/semantic-conventions": "1.28.0"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": ">=1.0.0 <1.10.0"
+ }
+ },
+ "node_modules/@opentelemetry/core/node_modules/@opentelemetry/semantic-conventions": {
+ "version": "1.28.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.28.0.tgz",
+ "integrity": "sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/@opentelemetry/instrumentation": {
+ "version": "0.57.2",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.57.2.tgz",
+ "integrity": "sha512-BdBGhQBh8IjZ2oIIX6F2/Q3LKm/FDDKi6ccYKcBTeilh6SNdNKveDOLk73BkSJjQLJk6qe4Yh+hHw1UPhCDdrg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/api-logs": "0.57.2",
+ "@types/shimmer": "^1.2.0",
+ "import-in-the-middle": "^1.8.1",
+ "require-in-the-middle": "^7.1.1",
+ "semver": "^7.5.2",
+ "shimmer": "^1.2.1"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": "^1.3.0"
+ }
+ },
+ "node_modules/@opentelemetry/instrumentation-amqplib": {
+ "version": "0.46.1",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-amqplib/-/instrumentation-amqplib-0.46.1.tgz",
+ "integrity": "sha512-AyXVnlCf/xV3K/rNumzKxZqsULyITJH6OVLiW6730JPRqWA7Zc9bvYoVNpN6iOpTU8CasH34SU/ksVJmObFibQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/core": "^1.8.0",
+ "@opentelemetry/instrumentation": "^0.57.1",
+ "@opentelemetry/semantic-conventions": "^1.27.0"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": "^1.3.0"
+ }
+ },
+ "node_modules/@opentelemetry/instrumentation-connect": {
+ "version": "0.43.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-connect/-/instrumentation-connect-0.43.0.tgz",
+ "integrity": "sha512-Q57JGpH6T4dkYHo9tKXONgLtxzsh1ZEW5M9A/OwKrZFyEpLqWgjhcZ3hIuVvDlhb426iDF1f9FPToV/mi5rpeA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/core": "^1.8.0",
+ "@opentelemetry/instrumentation": "^0.57.0",
+ "@opentelemetry/semantic-conventions": "^1.27.0",
+ "@types/connect": "3.4.36"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": "^1.3.0"
+ }
+ },
+ "node_modules/@opentelemetry/instrumentation-connect/node_modules/@types/connect": {
+ "version": "3.4.36",
+ "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.36.tgz",
+ "integrity": "sha512-P63Zd/JUGq+PdrM1lv0Wv5SBYeA2+CORvbrXbngriYY0jzLUWfQMQQxOhjONEz/wlHOAxOdY7CY65rgQdTjq2w==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@opentelemetry/instrumentation-dataloader": {
+ "version": "0.16.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-dataloader/-/instrumentation-dataloader-0.16.0.tgz",
+ "integrity": "sha512-88+qCHZC02up8PwKHk0UQKLLqGGURzS3hFQBZC7PnGwReuoKjHXS1o29H58S+QkXJpkTr2GACbx8j6mUoGjNPA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/instrumentation": "^0.57.0"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": "^1.3.0"
+ }
+ },
+ "node_modules/@opentelemetry/instrumentation-express": {
+ "version": "0.47.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-express/-/instrumentation-express-0.47.0.tgz",
+ "integrity": "sha512-XFWVx6k0XlU8lu6cBlCa29ONtVt6ADEjmxtyAyeF2+rifk8uBJbk1La0yIVfI0DoKURGbaEDTNelaXG9l/lNNQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/core": "^1.8.0",
+ "@opentelemetry/instrumentation": "^0.57.0",
+ "@opentelemetry/semantic-conventions": "^1.27.0"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": "^1.3.0"
+ }
+ },
+ "node_modules/@opentelemetry/instrumentation-fastify": {
+ "version": "0.44.1",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-fastify/-/instrumentation-fastify-0.44.1.tgz",
+ "integrity": "sha512-RoVeMGKcNttNfXMSl6W4fsYoCAYP1vi6ZAWIGhBY+o7R9Y0afA7f9JJL0j8LHbyb0P0QhSYk+6O56OwI2k4iRQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/core": "^1.8.0",
+ "@opentelemetry/instrumentation": "^0.57.0",
+ "@opentelemetry/semantic-conventions": "^1.27.0"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": "^1.3.0"
+ }
+ },
+ "node_modules/@opentelemetry/instrumentation-fs": {
+ "version": "0.19.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-fs/-/instrumentation-fs-0.19.0.tgz",
+ "integrity": "sha512-JGwmHhBkRT2G/BYNV1aGI+bBjJu4fJUD/5/Jat0EWZa2ftrLV3YE8z84Fiij/wK32oMZ88eS8DI4ecLGZhpqsQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/core": "^1.8.0",
+ "@opentelemetry/instrumentation": "^0.57.0"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": "^1.3.0"
+ }
+ },
+ "node_modules/@opentelemetry/instrumentation-generic-pool": {
+ "version": "0.43.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-generic-pool/-/instrumentation-generic-pool-0.43.0.tgz",
+ "integrity": "sha512-at8GceTtNxD1NfFKGAuwtqM41ot/TpcLh+YsGe4dhf7gvv1HW/ZWdq6nfRtS6UjIvZJOokViqLPJ3GVtZItAnQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/instrumentation": "^0.57.0"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": "^1.3.0"
+ }
+ },
+ "node_modules/@opentelemetry/instrumentation-graphql": {
+ "version": "0.47.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-graphql/-/instrumentation-graphql-0.47.0.tgz",
+ "integrity": "sha512-Cc8SMf+nLqp0fi8oAnooNEfwZWFnzMiBHCGmDFYqmgjPylyLmi83b+NiTns/rKGwlErpW0AGPt0sMpkbNlzn8w==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/instrumentation": "^0.57.0"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": "^1.3.0"
+ }
+ },
+ "node_modules/@opentelemetry/instrumentation-hapi": {
+ "version": "0.45.1",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-hapi/-/instrumentation-hapi-0.45.1.tgz",
+ "integrity": "sha512-VH6mU3YqAKTePPfUPwfq4/xr049774qWtfTuJqVHoVspCLiT3bW+fCQ1toZxt6cxRPYASoYaBsMA3CWo8B8rcw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/core": "^1.8.0",
+ "@opentelemetry/instrumentation": "^0.57.0",
+ "@opentelemetry/semantic-conventions": "^1.27.0"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": "^1.3.0"
+ }
+ },
+ "node_modules/@opentelemetry/instrumentation-http": {
+ "version": "0.57.1",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-http/-/instrumentation-http-0.57.1.tgz",
+ "integrity": "sha512-ThLmzAQDs7b/tdKI3BV2+yawuF09jF111OFsovqT1Qj3D8vjwKBwhi/rDE5xethwn4tSXtZcJ9hBsVAlWFQZ7g==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/core": "1.30.1",
+ "@opentelemetry/instrumentation": "0.57.1",
+ "@opentelemetry/semantic-conventions": "1.28.0",
+ "forwarded-parse": "2.1.2",
+ "semver": "^7.5.2"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": "^1.3.0"
+ }
+ },
+ "node_modules/@opentelemetry/instrumentation-http/node_modules/@opentelemetry/api-logs": {
+ "version": "0.57.1",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.57.1.tgz",
+ "integrity": "sha512-I4PHczeujhQAQv6ZBzqHYEUiggZL4IdSMixtVD3EYqbdrjujE7kRfI5QohjlPoJm8BvenoW5YaTMWRrbpot6tg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/api": "^1.3.0"
+ },
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/@opentelemetry/instrumentation-http/node_modules/@opentelemetry/instrumentation": {
+ "version": "0.57.1",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.57.1.tgz",
+ "integrity": "sha512-SgHEKXoVxOjc20ZYusPG3Fh+RLIZTSa4x8QtD3NfgAUDyqdFFS9W1F2ZVbZkqDCdyMcQG02Ok4duUGLHJXHgbA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/api-logs": "0.57.1",
+ "@types/shimmer": "^1.2.0",
+ "import-in-the-middle": "^1.8.1",
+ "require-in-the-middle": "^7.1.1",
+ "semver": "^7.5.2",
+ "shimmer": "^1.2.1"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": "^1.3.0"
+ }
+ },
+ "node_modules/@opentelemetry/instrumentation-http/node_modules/@opentelemetry/semantic-conventions": {
+ "version": "1.28.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.28.0.tgz",
+ "integrity": "sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/@opentelemetry/instrumentation-ioredis": {
+ "version": "0.47.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-ioredis/-/instrumentation-ioredis-0.47.0.tgz",
+ "integrity": "sha512-4HqP9IBC8e7pW9p90P3q4ox0XlbLGme65YTrA3UTLvqvo4Z6b0puqZQP203YFu8m9rE/luLfaG7/xrwwqMUpJw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/instrumentation": "^0.57.0",
+ "@opentelemetry/redis-common": "^0.36.2",
+ "@opentelemetry/semantic-conventions": "^1.27.0"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": "^1.3.0"
+ }
+ },
+ "node_modules/@opentelemetry/instrumentation-kafkajs": {
+ "version": "0.7.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-kafkajs/-/instrumentation-kafkajs-0.7.0.tgz",
+ "integrity": "sha512-LB+3xiNzc034zHfCtgs4ITWhq6Xvdo8bsq7amR058jZlf2aXXDrN9SV4si4z2ya9QX4tz6r4eZJwDkXOp14/AQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/instrumentation": "^0.57.0",
+ "@opentelemetry/semantic-conventions": "^1.27.0"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": "^1.3.0"
+ }
+ },
+ "node_modules/@opentelemetry/instrumentation-knex": {
+ "version": "0.44.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-knex/-/instrumentation-knex-0.44.0.tgz",
+ "integrity": "sha512-SlT0+bLA0Lg3VthGje+bSZatlGHw/vwgQywx0R/5u9QC59FddTQSPJeWNw29M6f8ScORMeUOOTwihlQAn4GkJQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/instrumentation": "^0.57.0",
+ "@opentelemetry/semantic-conventions": "^1.27.0"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": "^1.3.0"
+ }
+ },
+ "node_modules/@opentelemetry/instrumentation-koa": {
+ "version": "0.47.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-koa/-/instrumentation-koa-0.47.0.tgz",
+ "integrity": "sha512-HFdvqf2+w8sWOuwtEXayGzdZ2vWpCKEQv5F7+2DSA74Te/Cv4rvb2E5So5/lh+ok4/RAIPuvCbCb/SHQFzMmbw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/core": "^1.8.0",
+ "@opentelemetry/instrumentation": "^0.57.0",
+ "@opentelemetry/semantic-conventions": "^1.27.0"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": "^1.3.0"
+ }
+ },
+ "node_modules/@opentelemetry/instrumentation-lru-memoizer": {
+ "version": "0.44.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-lru-memoizer/-/instrumentation-lru-memoizer-0.44.0.tgz",
+ "integrity": "sha512-Tn7emHAlvYDFik3vGU0mdwvWJDwtITtkJ+5eT2cUquct6nIs+H8M47sqMJkCpyPe5QIBJoTOHxmc6mj9lz6zDw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/instrumentation": "^0.57.0"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": "^1.3.0"
+ }
+ },
+ "node_modules/@opentelemetry/instrumentation-mongodb": {
+ "version": "0.51.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-mongodb/-/instrumentation-mongodb-0.51.0.tgz",
+ "integrity": "sha512-cMKASxCX4aFxesoj3WK8uoQ0YUrRvnfxaO72QWI2xLu5ZtgX/QvdGBlU3Ehdond5eb74c2s1cqRQUIptBnKz1g==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/instrumentation": "^0.57.0",
+ "@opentelemetry/semantic-conventions": "^1.27.0"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": "^1.3.0"
+ }
+ },
+ "node_modules/@opentelemetry/instrumentation-mongoose": {
+ "version": "0.46.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-mongoose/-/instrumentation-mongoose-0.46.0.tgz",
+ "integrity": "sha512-mtVv6UeaaSaWTeZtLo4cx4P5/ING2obSqfWGItIFSunQBrYROfhuVe7wdIrFUs2RH1tn2YYpAJyMaRe/bnTTIQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/core": "^1.8.0",
+ "@opentelemetry/instrumentation": "^0.57.0",
+ "@opentelemetry/semantic-conventions": "^1.27.0"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": "^1.3.0"
+ }
+ },
+ "node_modules/@opentelemetry/instrumentation-mysql": {
+ "version": "0.45.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-mysql/-/instrumentation-mysql-0.45.0.tgz",
+ "integrity": "sha512-tWWyymgwYcTwZ4t8/rLDfPYbOTF3oYB8SxnYMtIQ1zEf5uDm90Ku3i6U/vhaMyfHNlIHvDhvJh+qx5Nc4Z3Acg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/instrumentation": "^0.57.0",
+ "@opentelemetry/semantic-conventions": "^1.27.0",
+ "@types/mysql": "2.15.26"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": "^1.3.0"
+ }
+ },
+ "node_modules/@opentelemetry/instrumentation-mysql2": {
+ "version": "0.45.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-mysql2/-/instrumentation-mysql2-0.45.0.tgz",
+ "integrity": "sha512-qLslv/EPuLj0IXFvcE3b0EqhWI8LKmrgRPIa4gUd8DllbBpqJAvLNJSv3cC6vWwovpbSI3bagNO/3Q2SuXv2xA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/instrumentation": "^0.57.0",
+ "@opentelemetry/semantic-conventions": "^1.27.0",
+ "@opentelemetry/sql-common": "^0.40.1"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": "^1.3.0"
+ }
+ },
+ "node_modules/@opentelemetry/instrumentation-nestjs-core": {
+ "version": "0.44.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-nestjs-core/-/instrumentation-nestjs-core-0.44.0.tgz",
+ "integrity": "sha512-t16pQ7A4WYu1yyQJZhRKIfUNvl5PAaF2pEteLvgJb/BWdd1oNuU1rOYt4S825kMy+0q4ngiX281Ss9qiwHfxFQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/instrumentation": "^0.57.0",
+ "@opentelemetry/semantic-conventions": "^1.27.0"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": "^1.3.0"
+ }
+ },
+ "node_modules/@opentelemetry/instrumentation-pg": {
+ "version": "0.50.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-pg/-/instrumentation-pg-0.50.0.tgz",
+ "integrity": "sha512-TtLxDdYZmBhFswm8UIsrDjh/HFBeDXd4BLmE8h2MxirNHewLJ0VS9UUddKKEverb5Sm2qFVjqRjcU+8Iw4FJ3w==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/core": "^1.26.0",
+ "@opentelemetry/instrumentation": "^0.57.0",
+ "@opentelemetry/semantic-conventions": "1.27.0",
+ "@opentelemetry/sql-common": "^0.40.1",
+ "@types/pg": "8.6.1",
+ "@types/pg-pool": "2.0.6"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": "^1.3.0"
+ }
+ },
+ "node_modules/@opentelemetry/instrumentation-pg/node_modules/@opentelemetry/semantic-conventions": {
+ "version": "1.27.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.27.0.tgz",
+ "integrity": "sha512-sAay1RrB+ONOem0OZanAR1ZI/k7yDpnOQSQmTMuGImUQb2y8EbSaCJ94FQluM74xoU03vlb2d2U90hZluL6nQg==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/@opentelemetry/instrumentation-redis-4": {
+ "version": "0.46.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-redis-4/-/instrumentation-redis-4-0.46.0.tgz",
+ "integrity": "sha512-aTUWbzbFMFeRODn3720TZO0tsh/49T8H3h8vVnVKJ+yE36AeW38Uj/8zykQ/9nO8Vrtjr5yKuX3uMiG/W8FKNw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/instrumentation": "^0.57.0",
+ "@opentelemetry/redis-common": "^0.36.2",
+ "@opentelemetry/semantic-conventions": "^1.27.0"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": "^1.3.0"
+ }
+ },
+ "node_modules/@opentelemetry/instrumentation-tedious": {
+ "version": "0.18.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-tedious/-/instrumentation-tedious-0.18.0.tgz",
+ "integrity": "sha512-9zhjDpUDOtD+coeADnYEJQ0IeLVCj7w/hqzIutdp5NqS1VqTAanaEfsEcSypyvYv5DX3YOsTUoF+nr2wDXPETA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/instrumentation": "^0.57.0",
+ "@opentelemetry/semantic-conventions": "^1.27.0",
+ "@types/tedious": "^4.0.14"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": "^1.3.0"
+ }
+ },
+ "node_modules/@opentelemetry/instrumentation-undici": {
+ "version": "0.10.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-undici/-/instrumentation-undici-0.10.0.tgz",
+ "integrity": "sha512-vm+V255NGw9gaSsPD6CP0oGo8L55BffBc8KnxqsMuc6XiAD1L8SFNzsW0RHhxJFqy9CJaJh+YiJ5EHXuZ5rZBw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/core": "^1.8.0",
+ "@opentelemetry/instrumentation": "^0.57.0"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": "^1.7.0"
+ }
+ },
+ "node_modules/@opentelemetry/redis-common": {
+ "version": "0.36.2",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/redis-common/-/redis-common-0.36.2.tgz",
+ "integrity": "sha512-faYX1N0gpLhej/6nyp6bgRjzAKXn5GOEMYY7YhciSfCoITAktLUtQ36d24QEWNA1/WA1y6qQunCe0OhHRkVl9g==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/@opentelemetry/resources": {
+ "version": "1.30.1",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-1.30.1.tgz",
+ "integrity": "sha512-5UxZqiAgLYGFjS4s9qm5mBVo433u+dSPUFWVWXmLAD4wB65oMCoXaJP1KJa9DIYYMeHu3z4BZcStG3LC593cWA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/core": "1.30.1",
+ "@opentelemetry/semantic-conventions": "1.28.0"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": ">=1.0.0 <1.10.0"
+ }
+ },
+ "node_modules/@opentelemetry/resources/node_modules/@opentelemetry/semantic-conventions": {
+ "version": "1.28.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.28.0.tgz",
+ "integrity": "sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/@opentelemetry/sdk-trace-base": {
+ "version": "1.30.1",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-1.30.1.tgz",
+ "integrity": "sha512-jVPgBbH1gCy2Lb7X0AVQ8XAfgg0pJ4nvl8/IiQA6nxOsPvS+0zMJaFSs2ltXe0J6C8dqjcnpyqINDJmU30+uOg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/core": "1.30.1",
+ "@opentelemetry/resources": "1.30.1",
+ "@opentelemetry/semantic-conventions": "1.28.0"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": ">=1.0.0 <1.10.0"
+ }
+ },
+ "node_modules/@opentelemetry/sdk-trace-base/node_modules/@opentelemetry/semantic-conventions": {
+ "version": "1.28.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.28.0.tgz",
+ "integrity": "sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/@opentelemetry/semantic-conventions": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.32.0.tgz",
+ "integrity": "sha512-s0OpmpQFSfMrmedAn9Lhg4KWJELHCU6uU9dtIJ28N8UGhf9Y55im5X8fEzwhwDwiSqN+ZPSNrDJF7ivf/AuRPQ==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/@opentelemetry/sql-common": {
+ "version": "0.40.1",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/sql-common/-/sql-common-0.40.1.tgz",
+ "integrity": "sha512-nSDlnHSqzC3pXn/wZEZVLuAuJ1MYMXPBwtv2qAbCa3847SaHItdE7SzUq/Jtb0KZmh1zfAbNi3AAMjztTT4Ugg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/core": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": "^1.1.0"
+ }
+ },
"node_modules/@pkgjs/parseargs": {
"version": "0.11.0",
"dev": true,
@@ -813,6 +1434,49 @@
"node": ">=14"
}
},
+ "node_modules/@prisma/instrumentation": {
+ "version": "5.22.0",
+ "resolved": "https://registry.npmjs.org/@prisma/instrumentation/-/instrumentation-5.22.0.tgz",
+ "integrity": "sha512-LxccF392NN37ISGxIurUljZSh1YWnphO34V5a0+T7FVQG2u9bhAXRTJpgmQ3483woVhkraQZFF7cbRrpbw/F4Q==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/api": "^1.8",
+ "@opentelemetry/instrumentation": "^0.49 || ^0.50 || ^0.51 || ^0.52.0 || ^0.53.0",
+ "@opentelemetry/sdk-trace-base": "^1.22"
+ }
+ },
+ "node_modules/@prisma/instrumentation/node_modules/@opentelemetry/api-logs": {
+ "version": "0.53.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.53.0.tgz",
+ "integrity": "sha512-8HArjKx+RaAI8uEIgcORbZIPklyh1YLjPSBus8hjRmvLi6DeFzgOcdZ7KwPabKj8mXF8dX0hyfAyGfycz0DbFw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/api": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/@prisma/instrumentation/node_modules/@opentelemetry/instrumentation": {
+ "version": "0.53.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.53.0.tgz",
+ "integrity": "sha512-DMwg0hy4wzf7K73JJtl95m/e0boSoWhH07rfvHvYzQtBD3Bmv0Wc1x733vyZBqmFm8OjJD0/pfiUg1W3JjFX0A==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/api-logs": "0.53.0",
+ "@types/shimmer": "^1.2.0",
+ "import-in-the-middle": "^1.8.1",
+ "require-in-the-middle": "^7.1.1",
+ "semver": "^7.5.2",
+ "shimmer": "^1.2.1"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": "^1.3.0"
+ }
+ },
"node_modules/@privy-io/server-auth": {
"version": "1.18.12",
"license": "Apache-2.0",
@@ -879,6 +1543,81 @@
"url": "https://paulmillr.com/funding/"
}
},
+ "node_modules/@sentry/core": {
+ "version": "8.55.0",
+ "resolved": "https://registry.npmjs.org/@sentry/core/-/core-8.55.0.tgz",
+ "integrity": "sha512-6g7jpbefjHYs821Z+EBJ8r4Z7LT5h80YSWRJaylGS4nW5W5Z2KXzpdnyFarv37O7QjauzVC2E+PABmpkw5/JGA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.18"
+ }
+ },
+ "node_modules/@sentry/node": {
+ "version": "8.55.0",
+ "resolved": "https://registry.npmjs.org/@sentry/node/-/node-8.55.0.tgz",
+ "integrity": "sha512-h10LJLDTRAzYgay60Oy7moMookqqSZSviCWkkmHZyaDn+4WURnPp5SKhhfrzPRQcXKrweiOwDSHBgn1tweDssg==",
+ "license": "MIT",
+ "dependencies": {
+ "@opentelemetry/api": "^1.9.0",
+ "@opentelemetry/context-async-hooks": "^1.30.1",
+ "@opentelemetry/core": "^1.30.1",
+ "@opentelemetry/instrumentation": "^0.57.1",
+ "@opentelemetry/instrumentation-amqplib": "^0.46.0",
+ "@opentelemetry/instrumentation-connect": "0.43.0",
+ "@opentelemetry/instrumentation-dataloader": "0.16.0",
+ "@opentelemetry/instrumentation-express": "0.47.0",
+ "@opentelemetry/instrumentation-fastify": "0.44.1",
+ "@opentelemetry/instrumentation-fs": "0.19.0",
+ "@opentelemetry/instrumentation-generic-pool": "0.43.0",
+ "@opentelemetry/instrumentation-graphql": "0.47.0",
+ "@opentelemetry/instrumentation-hapi": "0.45.1",
+ "@opentelemetry/instrumentation-http": "0.57.1",
+ "@opentelemetry/instrumentation-ioredis": "0.47.0",
+ "@opentelemetry/instrumentation-kafkajs": "0.7.0",
+ "@opentelemetry/instrumentation-knex": "0.44.0",
+ "@opentelemetry/instrumentation-koa": "0.47.0",
+ "@opentelemetry/instrumentation-lru-memoizer": "0.44.0",
+ "@opentelemetry/instrumentation-mongodb": "0.51.0",
+ "@opentelemetry/instrumentation-mongoose": "0.46.0",
+ "@opentelemetry/instrumentation-mysql": "0.45.0",
+ "@opentelemetry/instrumentation-mysql2": "0.45.0",
+ "@opentelemetry/instrumentation-nestjs-core": "0.44.0",
+ "@opentelemetry/instrumentation-pg": "0.50.0",
+ "@opentelemetry/instrumentation-redis-4": "0.46.0",
+ "@opentelemetry/instrumentation-tedious": "0.18.0",
+ "@opentelemetry/instrumentation-undici": "0.10.0",
+ "@opentelemetry/resources": "^1.30.1",
+ "@opentelemetry/sdk-trace-base": "^1.30.1",
+ "@opentelemetry/semantic-conventions": "^1.28.0",
+ "@prisma/instrumentation": "5.22.0",
+ "@sentry/core": "8.55.0",
+ "@sentry/opentelemetry": "8.55.0",
+ "import-in-the-middle": "^1.11.2"
+ },
+ "engines": {
+ "node": ">=14.18"
+ }
+ },
+ "node_modules/@sentry/opentelemetry": {
+ "version": "8.55.0",
+ "resolved": "https://registry.npmjs.org/@sentry/opentelemetry/-/opentelemetry-8.55.0.tgz",
+ "integrity": "sha512-UvatdmSr3Xf+4PLBzJNLZ2JjG1yAPWGe/VrJlJAqyTJ2gKeTzgXJJw8rp4pbvNZO8NaTGEYhhO+scLUj0UtLAQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@sentry/core": "8.55.0"
+ },
+ "engines": {
+ "node": ">=14.18"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": "^1.9.0",
+ "@opentelemetry/context-async-hooks": "^1.30.1",
+ "@opentelemetry/core": "^1.30.1",
+ "@opentelemetry/instrumentation": "^0.57.1",
+ "@opentelemetry/sdk-trace-base": "^1.30.1",
+ "@opentelemetry/semantic-conventions": "^1.28.0"
+ }
+ },
"node_modules/@sinclair/typebox": {
"version": "0.34.30",
"license": "MIT"
@@ -969,6 +1708,15 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@types/mysql": {
+ "version": "2.15.26",
+ "resolved": "https://registry.npmjs.org/@types/mysql/-/mysql-2.15.26.tgz",
+ "integrity": "sha512-DSLCOXhkvfS5WNNPbfn2KdICAmk8lLc+/PNvnPnF7gOdMZCxopXduqv0OQ13y/yA/zXTSikZZqVgybUxOEg6YQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
"node_modules/@types/node": {
"version": "22.13.10",
"license": "MIT",
@@ -976,6 +1724,41 @@
"undici-types": "~6.20.0"
}
},
+ "node_modules/@types/pg": {
+ "version": "8.6.1",
+ "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.6.1.tgz",
+ "integrity": "sha512-1Kc4oAGzAl7uqUStZCDvaLFqZrW9qWSjXOmBfdgyBP5La7Us6Mg4GBvRlSoaZMhQF/zSj1C8CtKMBkoiT8eL8w==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*",
+ "pg-protocol": "*",
+ "pg-types": "^2.2.0"
+ }
+ },
+ "node_modules/@types/pg-pool": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/@types/pg-pool/-/pg-pool-2.0.6.tgz",
+ "integrity": "sha512-TaAUE5rq2VQYxab5Ts7WZhKNmuN78Q6PiFonTDdpbx8a1H0M1vhy3rhiMjl+e2iHmogyMw7jZF4FrE6eJUy5HQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/pg": "*"
+ }
+ },
+ "node_modules/@types/shimmer": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/@types/shimmer/-/shimmer-1.2.0.tgz",
+ "integrity": "sha512-UE7oxhQLLd9gub6JKIAhDq06T0F6FnztwMNRvYgjeQSBeMc1ZG/tA47EwfduvkuQS8apbkM/lpLpWsaCeYsXVg==",
+ "license": "MIT"
+ },
+ "node_modules/@types/tedious": {
+ "version": "4.0.14",
+ "resolved": "https://registry.npmjs.org/@types/tedious/-/tedious-4.0.14.tgz",
+ "integrity": "sha512-KHPsfX/FoVbUGbyYvk1q9MMQHLPeRZhRJZdO45Q4YjvFkv4hMNghCWTvy7rdKessBsmtz4euWCWAB6/tVpI1Iw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
"node_modules/@types/uuid": {
"version": "8.3.4",
"license": "MIT"
@@ -1300,7 +2083,6 @@
},
"node_modules/acorn": {
"version": "8.14.1",
- "dev": true,
"license": "MIT",
"bin": {
"acorn": "bin/acorn"
@@ -1309,6 +2091,15 @@
"node": ">=0.4.0"
}
},
+ "node_modules/acorn-import-attributes": {
+ "version": "1.9.5",
+ "resolved": "https://registry.npmjs.org/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz",
+ "integrity": "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==",
+ "license": "MIT",
+ "peerDependencies": {
+ "acorn": "^8"
+ }
+ },
"node_modules/acorn-jsx": {
"version": "5.3.2",
"dev": true,
@@ -1840,6 +2631,12 @@
"url": "https://paulmillr.com/funding/"
}
},
+ "node_modules/cjs-module-lexer": {
+ "version": "1.4.3",
+ "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz",
+ "integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==",
+ "license": "MIT"
+ },
"node_modules/cliui": {
"version": "8.0.1",
"license": "ISC",
@@ -3403,6 +4200,12 @@
"node": ">= 0.6"
}
},
+ "node_modules/forwarded-parse": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/forwarded-parse/-/forwarded-parse-2.1.2.tgz",
+ "integrity": "sha512-alTFZZQDKMporBH77856pXgzhEzaUVmLCDk+egLgIgHst3Tpndzz8MnKe+GzRJRfvVdn69HhpW7cmXzvtLvJAw==",
+ "license": "MIT"
+ },
"node_modules/fs.realpath": {
"version": "1.0.0",
"license": "ISC"
@@ -3537,7 +4340,7 @@
},
"node_modules/get-tsconfig": {
"version": "4.10.0",
- "devOptional": true,
+ "dev": true,
"license": "MIT",
"dependencies": {
"resolve-pkg-maps": "^1.0.0"
@@ -3858,6 +4661,18 @@
"node": ">=4"
}
},
+ "node_modules/import-in-the-middle": {
+ "version": "1.13.1",
+ "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-1.13.1.tgz",
+ "integrity": "sha512-k2V9wNm9B+ysuelDTHjI9d5KPc4l8zAZTGqj+pcynvWkypZd857ryzN8jNC7Pg2YZXNMJcHRPpaDyCBbNyVRpA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "acorn": "^8.14.0",
+ "acorn-import-attributes": "^1.9.5",
+ "cjs-module-lexer": "^1.2.2",
+ "module-details-from-path": "^1.0.3"
+ }
+ },
"node_modules/imurmurhash": {
"version": "0.1.4",
"dev": true,
@@ -4845,6 +5660,12 @@
"obliterator": "^2.0.4"
}
},
+ "node_modules/module-details-from-path": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/module-details-from-path/-/module-details-from-path-1.0.3.tgz",
+ "integrity": "sha512-ySViT69/76t8VhE1xXHK6Ch4NcDd26gx0MzKXLO+F7NOtnqH68d9zF94nT8ZWSxXh8ELOERsnJO/sWt1xZYw5A==",
+ "license": "MIT"
+ },
"node_modules/ms": {
"version": "2.1.3",
"license": "MIT"
@@ -5255,6 +6076,37 @@
"version": "2.6.2",
"license": "MIT"
},
+ "node_modules/pg-int8": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz",
+ "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=4.0.0"
+ }
+ },
+ "node_modules/pg-protocol": {
+ "version": "1.8.0",
+ "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.8.0.tgz",
+ "integrity": "sha512-jvuYlEkL03NRvOoyoRktBK7+qU5kOvlAwvmrH8sr3wbLrOdVWsRxQfz8mMy9sZFsqJ1hEWNfdWKI4SAmoL+j7g==",
+ "license": "MIT"
+ },
+ "node_modules/pg-types": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz",
+ "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==",
+ "license": "MIT",
+ "dependencies": {
+ "pg-int8": "1.0.1",
+ "postgres-array": "~2.0.0",
+ "postgres-bytea": "~1.0.0",
+ "postgres-date": "~1.0.4",
+ "postgres-interval": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
"node_modules/picocolors": {
"version": "1.1.1",
"license": "ISC"
@@ -5458,6 +6310,45 @@
"node": ">=20.0.0"
}
},
+ "node_modules/postgres-array": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz",
+ "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/postgres-bytea": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz",
+ "integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/postgres-date": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz",
+ "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/postgres-interval": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz",
+ "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==",
+ "license": "MIT",
+ "dependencies": {
+ "xtend": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/prelude-ls": {
"version": "1.2.1",
"dev": true,
@@ -5662,6 +6553,20 @@
"node": ">=0.10.0"
}
},
+ "node_modules/require-in-the-middle": {
+ "version": "7.5.2",
+ "resolved": "https://registry.npmjs.org/require-in-the-middle/-/require-in-the-middle-7.5.2.tgz",
+ "integrity": "sha512-gAZ+kLqBdHarXB64XpAe2VCjB7rIRv+mU8tfRWziHRJ5umKsIHN2tLLv6EtMw7WCdP19S0ERVMldNvxYCHnhSQ==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^4.3.5",
+ "module-details-from-path": "^1.0.3",
+ "resolve": "^1.22.8"
+ },
+ "engines": {
+ "node": ">=8.6.0"
+ }
+ },
"node_modules/requires-port": {
"version": "1.0.0",
"license": "MIT"
@@ -5693,7 +6598,7 @@
},
"node_modules/resolve-pkg-maps": {
"version": "1.0.0",
- "devOptional": true,
+ "dev": true,
"license": "MIT",
"funding": {
"url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
@@ -6011,6 +6916,12 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/shimmer": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/shimmer/-/shimmer-1.2.1.tgz",
+ "integrity": "sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw==",
+ "license": "BSD-2-Clause"
+ },
"node_modules/side-channel": {
"version": "1.1.0",
"dev": true,
@@ -6615,7 +7526,7 @@
},
"node_modules/tsx": {
"version": "4.19.3",
- "devOptional": true,
+ "dev": true,
"license": "MIT",
"dependencies": {
"esbuild": "~0.25.0",
@@ -6735,7 +7646,7 @@
},
"node_modules/typescript": {
"version": "5.8.2",
- "devOptional": true,
+ "dev": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
@@ -7268,6 +8179,15 @@
}
}
},
+ "node_modules/xtend": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
+ "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.4"
+ }
+ },
"node_modules/y18n": {
"version": "5.0.8",
"license": "ISC",
diff --git a/src/Managing.Web3Proxy/package.json b/src/Managing.Web3Proxy/package.json
index b6ebf23..a993710 100644
--- a/src/Managing.Web3Proxy/package.json
+++ b/src/Managing.Web3Proxy/package.json
@@ -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",
diff --git a/src/Managing.Web3Proxy/src/app.ts b/src/Managing.Web3Proxy/src/app.ts
index a6311ee..c7ddd82 100644
--- a/src/Managing.Web3Proxy/src/app.ts
+++ b/src/Managing.Web3Proxy/src/app.ts
@@ -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
diff --git a/src/Managing.Web3Proxy/src/plugins/custom/sentry.ts b/src/Managing.Web3Proxy/src/plugins/custom/sentry.ts
new file mode 100644
index 0000000..45dd841
--- /dev/null
+++ b/src/Managing.Web3Proxy/src/plugins/custom/sentry.ts
@@ -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 = 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'
+});
\ No newline at end of file
diff --git a/src/Managing.Web3Proxy/src/plugins/external/env.ts b/src/Managing.Web3Proxy/src/plugins/external/env.ts
index 3650349..d4f18ae 100644
--- a/src/Managing.Web3Proxy/src/plugins/external/env.ts
+++ b/src/Managing.Web3Proxy/src/plugins/external/env.ts
@@ -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'
}
}
}
diff --git a/src/Managing.Web3Proxy/src/plugins/external/session.ts b/src/Managing.Web3Proxy/src/plugins/external/session.ts
index d435ed0..0907062 100644
--- a/src/Managing.Web3Proxy/src/plugins/external/session.ts
+++ b/src/Managing.Web3Proxy/src/plugins/external/session.ts
@@ -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
})
diff --git a/src/Managing.Web3Proxy/src/routes/api/gmx/index.ts b/src/Managing.Web3Proxy/src/routes/api/gmx/index.ts
index 6416003..a1c7467 100644
--- a/src/Managing.Web3Proxy/src/routes/api/gmx/index.ts
+++ b/src/Managing.Web3Proxy/src/routes/api/gmx/index.ts
@@ -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');
}
})
}
diff --git a/src/Managing.Web3Proxy/src/routes/api/index.ts b/src/Managing.Web3Proxy/src/routes/api/index.ts
index 30a81ec..80156ec 100644
--- a/src/Managing.Web3Proxy/src/routes/api/index.ts
+++ b/src/Managing.Web3Proxy/src/routes/api/index.ts
@@ -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');
}
})
}
diff --git a/src/Managing.Web3Proxy/src/routes/api/privy/index.ts b/src/Managing.Web3Proxy/src/routes/api/privy/index.ts
index e18ac5f..ba46bba 100644
--- a/src/Managing.Web3Proxy/src/routes/api/privy/index.ts
+++ b/src/Managing.Web3Proxy/src/routes/api/privy/index.ts
@@ -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');
+ }
}
)
}
diff --git a/src/Managing.Web3Proxy/src/routes/home.ts b/src/Managing.Web3Proxy/src/routes/home.ts
index 9e9f873..f47968b 100644
--- a/src/Managing.Web3Proxy/src/routes/home.ts
+++ b/src/Managing.Web3Proxy/src/routes/home.ts
@@ -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');
+ }
})
}
diff --git a/src/Managing.Web3Proxy/src/routes/sentry.ts b/src/Managing.Web3Proxy/src/routes/sentry.ts
new file mode 100644
index 0000000..f4c976e
--- /dev/null
+++ b/src/Managing.Web3Proxy/src/routes/sentry.ts
@@ -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;
\ No newline at end of file
diff --git a/src/Managing.Web3Proxy/src/utils/README.md b/src/Managing.Web3Proxy/src/utils/README.md
new file mode 100644
index 0000000..cf3e6d9
--- /dev/null
+++ b/src/Managing.Web3Proxy/src/utils/README.md
@@ -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
\ No newline at end of file
diff --git a/src/Managing.Web3Proxy/src/utils/errorHandler.ts b/src/Managing.Web3Proxy/src/utils/errorHandler.ts
new file mode 100644
index 0000000..1bb413f
--- /dev/null
+++ b/src/Managing.Web3Proxy/src/utils/errorHandler.ts
@@ -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;
+}
\ No newline at end of file
diff --git a/src/Managing.Web3Proxy/src/utils/routeWrapper.ts b/src/Managing.Web3Proxy/src/utils/routeWrapper.ts
new file mode 100644
index 0000000..dad2e14
--- /dev/null
+++ b/src/Managing.Web3Proxy/src/utils/routeWrapper.ts
@@ -0,0 +1,35 @@
+import { FastifyRequest, FastifyReply } from 'fastify'
+import { handleError } from './errorHandler.js'
+
+/**
+ * Type for route handler functions
+ */
+type RouteHandler = (request: FastifyRequest, reply: FastifyReply) => Promise
+
+/**
+ * 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(handler: RouteHandler, endpoint: string): RouteHandler {
+ return async (request: FastifyRequest, reply: FastifyReply): Promise => {
+ 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(endpoint: string, handlerFn: RouteHandler): RouteHandler {
+ return withErrorHandling(handlerFn, endpoint)
+}
\ No newline at end of file