diff --git a/Signum.Engine/Basics/ExceptionLogic.cs b/Signum.Engine/Basics/ExceptionLogic.cs index 2d16c63eab..4f2b7357ee 100644 --- a/Signum.Engine/Basics/ExceptionLogic.cs +++ b/Signum.Engine/Basics/ExceptionLogic.cs @@ -18,7 +18,7 @@ public static void Start(SchemaBuilder sb) e.Id, e.CreationDate, e.ExceptionType, - e.IsClientSide, + e.Origin, e.ExceptionMessage, e.StackTraceHash, }); diff --git a/Signum.Entities/Basics/Exception.cs b/Signum.Entities/Basics/Exception.cs index 66a5fa997a..b91d7a9c88 100644 --- a/Signum.Entities/Basics/Exception.cs +++ b/Signum.Entities/Basics/Exception.cs @@ -1,10 +1,5 @@ using System.Threading; using System.Collections.Specialized; -using Microsoft.AspNetCore.Mvc.Filters; -using Microsoft.AspNetCore.Http.Features; -using Microsoft.AspNetCore.Http.Extensions; -using System.Net; -using Microsoft.AspNetCore.Http; namespace Signum.Entities.Basics; @@ -16,36 +11,22 @@ public class ExceptionEntity : Entity #pragma warning disable CS8618 // Non-nullable field is uninitialized. public ExceptionEntity() { } - public ExceptionEntity(ClientExceptionModel clientException, HttpContext httpContext) + public ExceptionEntity(ClientErrorModel clientError) { - if (httpContext != null) - { - var req = httpContext.Request; - var connFeature = httpContext.Features.Get()!; - - this.ExceptionType = clientException.ExceptionType; - this.ExceptionMessage = clientException.TypeErrorMessage; - this.StackTrace = new BigStringEmbedded(clientException.TypeErrorStack); - this.ThreadId = -1; - this.UserAgent = Try(300, () => req.Headers["User-Agent"].FirstOrDefault()); - this.RequestUrl = Try(int.MaxValue, () => req.GetDisplayUrl()); - this.UrlReferer = Try(int.MaxValue, () => req.Headers["Referer"].ToString()); - this.UserHostAddress = Try(100, () => connFeature.RemoteIpAddress?.ToString()); - this.UserHostName = Try(100, () => connFeature.RemoteIpAddress == null ? null : Dns.GetHostEntry(connFeature.RemoteIpAddress).HostName); - - this.MachineName = System.Environment.MachineName; - this.ApplicationName = AppDomain.CurrentDomain.FriendlyName; - this.IsClientSide = true; - - this.Form = new BigStringEmbedded(); - this.QueryString = new BigStringEmbedded(); - this.Session = new BigStringEmbedded(); - this.Data = new BigStringEmbedded(); - } + this.RebindEvents(); + this.ExceptionType = "/".Combine(clientError.ErrorType, clientError.Name); + this.ExceptionMessage = clientError.Message; + this.StackTrace = new BigStringEmbedded(clientError.Stack); + this.ThreadId = -1; + + this.MachineName = System.Environment.MachineName; + this.ApplicationName = AppDomain.CurrentDomain.FriendlyName; + this.Origin = ExceptionOrigin.Frontend_React; } public ExceptionEntity(Exception ex) { + this.RebindEvents(); this.ExceptionType = ex.GetType().Name; this.ExceptionMessage = ex.Message!; this.StackTrace = new BigStringEmbedded(ex.StackTrace!); @@ -53,6 +34,7 @@ public ExceptionEntity(Exception ex) ex.Data[ExceptionDataKey] = this; this.MachineName = System.Environment.MachineName; this.ApplicationName = AppDomain.CurrentDomain.FriendlyName; + this.Origin = ExceptionOrigin.Backend_DotNet; } #pragma warning restore CS8618 // Non-nullable field is uninitialized. @@ -142,7 +124,7 @@ public BigStringEmbedded StackTrace public bool Referenced { get; set; } - public bool IsClientSide { get; set; } + public ExceptionOrigin Origin { get; set; } public override string ToString() { @@ -153,18 +135,12 @@ public static string Dump(NameValueCollection nameValueCollection) { return nameValueCollection.Cast().ToString(key => key + ": " + nameValueCollection[key], "\r\n"); } +} - private static string? Try(int size, Func getValue) - { - try - { - return getValue()?.TryStart(size); - } - catch (Exception e) - { - return (e.GetType().Name + ":" + e.Message).TryStart(size); - } - } +public enum ExceptionOrigin +{ + Backend_DotNet, + Frontend_React } @@ -225,16 +201,16 @@ public class DeleteLogsTypeOverridesEmbedded : EmbeddedEntity } } -public class ClientExceptionModel : ModelEntity +[AllowUnathenticated] +public class ClientErrorModel : ModelEntity { - public string TypeErrorMessage { get; set; } + public string ErrorType { get; set; } + + public string Message { get; set; } - public string? TypeErrorStack { get; set; } + public string? Stack { get; set; } - public string? TypeErrorName { get; set; } - - public string? ExceptionType { get; set; } + public string? Name { get; set; } } - #pragma warning restore CS8618 // Non-nullable field is uninitialized. diff --git a/Signum.Entities/Signum.Entities.csproj b/Signum.Entities/Signum.Entities.csproj index e276f400a5..204b24d81c 100644 --- a/Signum.Entities/Signum.Entities.csproj +++ b/Signum.Entities/Signum.Entities.csproj @@ -7,8 +7,6 @@ - - diff --git a/Signum.React/ApiControllers/ReflectionController.cs b/Signum.React/ApiControllers/ReflectionController.cs index a0c46f6f3a..11e01dc097 100644 --- a/Signum.React/ApiControllers/ReflectionController.cs +++ b/Signum.React/ApiControllers/ReflectionController.cs @@ -5,6 +5,8 @@ using Microsoft.AspNetCore.Http; using Signum.Engine.Maps; using Signum.Entities.Authorization; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.Http.Extensions; namespace Signum.React.ApiControllers; @@ -32,19 +34,39 @@ public ActionResult> Types() } - [HttpPost("api/registerClientError"), ValidateModelFilter, ProfilerActionSplitter] - public void ClientError([Required, FromBody] ClientExceptionModel error) + [HttpPost("api/registerClientError"), ValidateModelFilter, SignumAllowAnonymous] + public void ClientError([Required, FromBody] ClientErrorModel error) { var httpContext = this.HttpContext; - var clientException = new ExceptionEntity(error, httpContext); - clientException.Version = Schema.Current.Version.ToString(); - clientException.ApplicationName = Schema.Current.ApplicationName; - clientException.User = UserEntity.Current; + var req = httpContext.Request; + var connFeature = httpContext.Features.Get()!; - if (Database.Query().Any(e => e.ExceptionMessageHash == clientException.ExceptionMessageHash && e.CreationDate.AddSeconds(60) > clientException.CreationDate)) - return; + var clientException = new ExceptionEntity(error) + { + UserAgent = Try(300, () => req.Headers["User-Agent"].FirstOrDefault()), + RequestUrl = Try(int.MaxValue, () => req.GetDisplayUrl()), + UrlReferer = Try(int.MaxValue, () => req.Headers["Referer"].ToString()), + UserHostAddress = Try(100, () => connFeature.RemoteIpAddress?.ToString()), + UserHostName = Try(100, () => connFeature.RemoteIpAddress == null ? null : Dns.GetHostEntry(connFeature.RemoteIpAddress).HostName), + + Version = Schema.Current.Version.ToString(), + ApplicationName = Schema.Current.ApplicationName, + User = UserEntity.Current, + }; clientException.Save(); } + + private static string? Try(int size, Func getValue) + { + try + { + return getValue()?.TryStart(size); + } + catch (Exception e) + { + return (e.GetType().Name + ":" + e.Message).TryStart(size); + } + } } diff --git a/Signum.React/Scripts/Exceptions/Exception.tsx b/Signum.React/Scripts/Exceptions/Exception.tsx index 6e4ba1660c..5d6c2ed140 100644 --- a/Signum.React/Scripts/Exceptions/Exception.tsx +++ b/Signum.React/Scripts/Exceptions/Exception.tsx @@ -27,7 +27,7 @@ export default function Exception(p: { ctx: TypeContext }) { f.userHostAddress)} /> f.userHostName)} /> f.userAgent)} valueLineType="TextArea" /> - f.isClientSide)} /> + f.origin)} /> f.requestUrl)} /> diff --git a/Signum.React/Scripts/Modals/ErrorModal.tsx b/Signum.React/Scripts/Modals/ErrorModal.tsx index 30e7d78212..15f6801616 100644 --- a/Signum.React/Scripts/Modals/ErrorModal.tsx +++ b/Signum.React/Scripts/Modals/ErrorModal.tsx @@ -3,7 +3,7 @@ import * as Modals from '../Modals'; import { Dic } from '../Globals'; import { ajaxPost, ExternalServiceError, ServiceError, ValidationError } from '../Services'; import { JavascriptMessage, FrameMessage, ConnectionMessage } from '../Signum.Entities' -import { ClientExceptionModel, ExceptionEntity } from '../Signum.Entities.Basics' +import { ClientErrorModel, ExceptionEntity } from '../Signum.Entities.Basics' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import "./Modals.css" import { newLite } from '../Reflection'; @@ -103,19 +103,36 @@ export default function ErrorModal(p: ErrorModalProps) { +var lastError: { model: ClientErrorModel, date: Date } | undefined; + function logError(error: Error) { if (error instanceof ServiceError || error instanceof ValidationError) return; - var errorModel: ClientExceptionModel = ClientExceptionModel.New({ - - exceptionType : (error as Object).constructor.name, - typeErrorMessage : error.message ?? error.toString(), - typeErrorStack : error.stack ?? null, - typeErrorName : error.name, + var errorModel = ClientErrorModel.New({ + errorType: (error as Object).constructor.name, + message: error.message ?? error.toString(), + stack: error.stack ?? null, + name: error.name, }); + var date = new Date(); + + if (lastError != null) { + if ( + lastError.model.errorType == errorModel.errorType && + lastError.model.message == errorModel.message && + lastError.model.stack == errorModel.stack && + lastError.model.errorType == errorModel.errorType && + ((date.valueOf() - lastError.date.valueOf()) / 1000) < 10 + ) { + return; + } + } + + lastError = { model: errorModel, date: date }; + ajaxPost({ url: "~/api/registerClientError" }, errorModel); } @@ -123,8 +140,8 @@ ErrorModal.register = () => { window.onunhandledrejection = p => { var error = p.reason; + logError(error); if (Modals.isStarted()) { - logError(error); ErrorModal.showErrorModal(error); } else @@ -134,7 +151,10 @@ ErrorModal.register = () => { var oldOnError = window.onerror; window.onerror = (message: Event | string, filename?: string, lineno?: number, colno?: number, error?: Error) => { - if (Modals.isStarted()) + if (error != null) + logError(error); + + if (Modals.isStarted()) ErrorModal.showErrorModal(error); else if (oldOnError != null) { if (error instanceof ServiceError) diff --git a/Signum.React/Scripts/Signum.Entities.Basics.ts b/Signum.React/Scripts/Signum.Entities.Basics.ts index 21551a9ce2..360123987d 100644 --- a/Signum.React/Scripts/Signum.Entities.Basics.ts +++ b/Signum.React/Scripts/Signum.Entities.Basics.ts @@ -12,13 +12,13 @@ export interface BigStringEmbedded extends Entities.EmbeddedEntity { text: string | null; } -export const ClientExceptionModel = new Type("ClientExceptionModel"); -export interface ClientExceptionModel extends Entities.ModelEntity { - Type: "ClientExceptionModel"; - typeErrorMessage: string; - typeErrorStack: string | null; - typeErrorName: string | null; - exceptionType: string | null; +export const ClientErrorModel = new Type("ClientErrorModel"); +export interface ClientErrorModel extends Entities.ModelEntity { + Type: "ClientErrorModel"; + errorType: string; + message: string; + stack: string | null; + name: string | null; } export const DeleteLogParametersEmbedded = new Type("DeleteLogParametersEmbedded"); @@ -66,9 +66,14 @@ export interface ExceptionEntity extends Entities.Entity { data: BigStringEmbedded; hResult: number; referenced: boolean; - isClientSide: boolean; + origin: ExceptionOrigin; } +export const ExceptionOrigin = new EnumType("ExceptionOrigin"); +export type ExceptionOrigin = + "Backend_DotNet" | + "Frontend_React"; + export interface IUserEntity extends Entities.Entity { }