From 42a4cafd8d298766c55a5101cd3b66b934df6f12 Mon Sep 17 00:00:00 2001 From: Oda <102867384+CryptoOda@users.noreply.github.com> Date: Tue, 22 Apr 2025 20:49:02 +0200 Subject: [PATCH 1/2] Add Sentry (#19) * add sentry * add sentry * better log web3proxy * Add managing and worker on sentry * better log web3proxy --- .../Managing.Api.Workers.csproj | 2 + .../Middleware/SentryDiagnosticsMiddleware.cs | 90 ++ src/Managing.Api.Workers/Program.cs | 44 +- src/Managing.Api.Workers/appsettings.json | 9 + .../Controllers/SentryTestController.cs | 74 ++ .../Exceptions/CustomExceptions.cs | 79 ++ .../GlobalErrorHandlingMiddleware.cs | 179 +++- .../Exceptions/SentryErrorCapture.cs | 109 +++ src/Managing.Api/Managing.Api.csproj | 2 + .../Middleware/SentryDiagnosticsMiddleware.cs | 90 ++ src/Managing.Api/Program.cs | 59 +- src/Managing.Api/README-ERROR-HANDLING.md | 90 ++ src/Managing.Api/README-SENTRY.md | 98 ++ src/Managing.Api/appsettings.Development.json | 12 +- src/Managing.Api/appsettings.Oda.json | 3 + src/Managing.Api/appsettings.Prod.json | 3 + src/Managing.Api/appsettings.json | 9 + .../Behaviours/UnhandledExceptionBehaviour.cs | 9 +- .../Exceptions/CustomExceptions.cs | 78 ++ src/Managing.Core/Exceptions/README.md | 106 ++ .../Exceptions/SentryErrorCapture.cs | 109 +++ src/Managing.Core/Managing.Core.csproj | 9 +- .../GlobalErrorHandlingMiddleware.cs | 186 +++- .../SentryDiagnosticsMiddleware.cs | 94 ++ src/Managing.Web3Proxy/.env | 4 +- src/Managing.Web3Proxy/SENTRY.md | 73 ++ src/Managing.Web3Proxy/package-lock.json | 922 +++++++++++++++++- src/Managing.Web3Proxy/package.json | 1 + src/Managing.Web3Proxy/src/app.ts | 5 +- .../src/plugins/custom/sentry.ts | 75 ++ .../src/plugins/external/env.ts | 33 +- .../src/plugins/external/session.ts | 28 +- .../src/routes/api/gmx/index.ts | 36 +- .../src/routes/api/index.ts | 13 +- .../src/routes/api/privy/index.ts | 17 +- src/Managing.Web3Proxy/src/routes/home.ts | 43 +- src/Managing.Web3Proxy/src/routes/sentry.ts | 92 ++ src/Managing.Web3Proxy/src/utils/README.md | 78 ++ .../src/utils/errorHandler.ts | 107 ++ .../src/utils/routeWrapper.ts | 35 + 40 files changed, 2959 insertions(+), 146 deletions(-) create mode 100644 src/Managing.Api.Workers/Middleware/SentryDiagnosticsMiddleware.cs create mode 100644 src/Managing.Api/Controllers/SentryTestController.cs create mode 100644 src/Managing.Api/Exceptions/CustomExceptions.cs create mode 100644 src/Managing.Api/Exceptions/SentryErrorCapture.cs create mode 100644 src/Managing.Api/Middleware/SentryDiagnosticsMiddleware.cs create mode 100644 src/Managing.Api/README-ERROR-HANDLING.md create mode 100644 src/Managing.Api/README-SENTRY.md create mode 100644 src/Managing.Core/Exceptions/CustomExceptions.cs create mode 100644 src/Managing.Core/Exceptions/README.md create mode 100644 src/Managing.Core/Exceptions/SentryErrorCapture.cs create mode 100644 src/Managing.Core/Middleawares/SentryDiagnosticsMiddleware.cs create mode 100644 src/Managing.Web3Proxy/SENTRY.md create mode 100644 src/Managing.Web3Proxy/src/plugins/custom/sentry.ts create mode 100644 src/Managing.Web3Proxy/src/routes/sentry.ts create mode 100644 src/Managing.Web3Proxy/src/utils/README.md create mode 100644 src/Managing.Web3Proxy/src/utils/errorHandler.ts create mode 100644 src/Managing.Web3Proxy/src/utils/routeWrapper.ts 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/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.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.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..a7237ee 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" @@ -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" @@ -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, @@ -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 From ec442c4dffa5ebe330b1cac910ba99c591c1b573 Mon Sep 17 00:00:00 2001 From: Crypto Od Date: Tue, 22 Apr 2025 21:17:34 +0200 Subject: [PATCH 2/2] Fix a bit bots --- src/Managing.Application/Bots/TradingBot.cs | 11 ++++++++--- .../Trading/OpenPositionCommandHandler.cs | 12 ++++-------- .../Services/Web3ProxyService.cs | 2 +- src/Managing.Web3Proxy/package-lock.json | 8 ++++---- 4 files changed, 17 insertions(+), 16 deletions(-) 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/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.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/package-lock.json b/src/Managing.Web3Proxy/package-lock.json index a7237ee..5465cff 100644 --- a/src/Managing.Web3Proxy/package-lock.json +++ b/src/Managing.Web3Proxy/package-lock.json @@ -4340,7 +4340,7 @@ }, "node_modules/get-tsconfig": { "version": "4.10.0", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "resolve-pkg-maps": "^1.0.0" @@ -6598,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" @@ -7526,7 +7526,7 @@ }, "node_modules/tsx": { "version": "4.19.3", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "esbuild": "~0.25.0", @@ -7646,7 +7646,7 @@ }, "node_modules/typescript": { "version": "5.8.2", - "devOptional": true, + "dev": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc",