diff --git a/Mindscape.Raygun4Net.AspNetCore.Tests/ExampleRaygunAspNetCoreClientProvider.cs b/Mindscape.Raygun4Net.AspNetCore.Tests/ExampleRaygunAspNetCoreClientProvider.cs deleted file mode 100644 index 285a2be05..000000000 --- a/Mindscape.Raygun4Net.AspNetCore.Tests/ExampleRaygunAspNetCoreClientProvider.cs +++ /dev/null @@ -1,30 +0,0 @@ -using Microsoft.AspNetCore.Http; -using System.Collections.Generic; - -namespace Mindscape.Raygun4Net.AspNetCore.Tests -{ - public class ExampleRaygunAspNetCoreClientProvider : DefaultRaygunAspNetCoreClientProvider - { - public override RaygunClient GetClient(RaygunSettings settings, HttpContext context) - { - var client = base.GetClient(settings, context); - - var email = "bob@raygun.com"; - - client.UserInfo = new RaygunIdentifierMessage(email) - { - IsAnonymous = false, - Email = email, - FullName = "Bob" - }; - - client.SendingMessage += (_, args) => - { - args.Message.Details.Tags ??= new List(); - args.Message.Details.Tags.Add("new tag"); - }; - - return client; - } - } -} \ No newline at end of file diff --git a/Mindscape.Raygun4Net.AspNetCore.Tests/Program.cs b/Mindscape.Raygun4Net.AspNetCore.Tests/Program.cs index 4ad5cc8e5..e8257c15e 100644 --- a/Mindscape.Raygun4Net.AspNetCore.Tests/Program.cs +++ b/Mindscape.Raygun4Net.AspNetCore.Tests/Program.cs @@ -9,11 +9,7 @@ builder.Services.AddRazorPages(); builder.Services.AddMvc(); -builder.Services.AddRaygun(builder.Configuration, new RaygunMiddlewareSettings -{ - // adds an optional example of over riding the client provider - ClientProvider = new ExampleRaygunAspNetCoreClientProvider() -}); +builder.Services.AddRaygun(builder.Configuration); // because we're using a library that uses Raygun, we need to initialize that too RaygunClientFactory.Initialize(builder.Configuration["RaygunSettings:ApiKey"]); diff --git a/Mindscape.Raygun4Net.AspNetCore/ApplicationBuilderExtensions.cs b/Mindscape.Raygun4Net.AspNetCore/ApplicationBuilderExtensions.cs new file mode 100644 index 000000000..ee658bb56 --- /dev/null +++ b/Mindscape.Raygun4Net.AspNetCore/ApplicationBuilderExtensions.cs @@ -0,0 +1,32 @@ +#nullable enable + +using System; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Mindscape.Raygun4Net.AspNetCore; + +public static class ApplicationBuilderExtensions +{ + public static IApplicationBuilder UseRaygun(this IApplicationBuilder app) + { + return app.UseMiddleware(); + } + + public static IServiceCollection AddRaygun(this IServiceCollection services, IConfiguration? configuration = null, Action? configure = null) + { + // Fetch settings from configuration or use default settings + var settings = configuration?.GetSection("RaygunSettings").Get() ?? new RaygunSettings(); + + // Override settings with user-provided settings + configure?.Invoke(settings); + + services.TryAddSingleton(settings); + services.TryAddSingleton(s => new RaygunClient(s.GetService())); + services.AddHttpContextAccessor(); + + return services; + } +} \ No newline at end of file diff --git a/Mindscape.Raygun4Net.AspNetCore/Builders/RaygunAspNetCoreRequestMessageBuilder.cs b/Mindscape.Raygun4Net.AspNetCore/Builders/RaygunAspNetCoreRequestMessageBuilder.cs index 6117913e5..fba8ff794 100644 --- a/Mindscape.Raygun4Net.AspNetCore/Builders/RaygunAspNetCoreRequestMessageBuilder.cs +++ b/Mindscape.Raygun4Net.AspNetCore/Builders/RaygunAspNetCoreRequestMessageBuilder.cs @@ -1,4 +1,6 @@ -using System; +#nullable enable + +using System; using System.Collections; using System.Collections.Generic; using System.Globalization; @@ -16,8 +18,13 @@ public class RaygunAspNetCoreRequestMessageBuilder { private const int MAX_RAW_DATA_LENGTH = 4096; // bytes - public static async Task Build(HttpContext context, RaygunRequestMessageOptions options) + public static async Task Build(HttpContext? context, RaygunRequestMessageOptions? options) { + if (context == null) + { + return new RaygunRequestMessage(); + } + var request = context.Request; options = options ?? new RaygunRequestMessageOptions(); diff --git a/Mindscape.Raygun4Net.AspNetCore/Builders/RaygunAspNetCoreResponseMessageBuilder.cs b/Mindscape.Raygun4Net.AspNetCore/Builders/RaygunAspNetCoreResponseMessageBuilder.cs index c4f096e92..15a693e18 100644 --- a/Mindscape.Raygun4Net.AspNetCore/Builders/RaygunAspNetCoreResponseMessageBuilder.cs +++ b/Mindscape.Raygun4Net.AspNetCore/Builders/RaygunAspNetCoreResponseMessageBuilder.cs @@ -1,12 +1,19 @@ -using Microsoft.AspNetCore.Http; +#nullable enable + +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; namespace Mindscape.Raygun4Net.AspNetCore.Builders { public class RaygunAspNetCoreResponseMessageBuilder { - public static RaygunResponseMessage Build(HttpContext context) + public static RaygunResponseMessage Build(HttpContext? context) { + if (context == null) + { + return new RaygunResponseMessage(); + } + var httpResponseFeature = context.Features.Get(); return new RaygunResponseMessage { diff --git a/Mindscape.Raygun4Net.AspNetCore/HttpRequestExtensions.cs b/Mindscape.Raygun4Net.AspNetCore/HttpRequestExtensions.cs new file mode 100644 index 000000000..71d9bf537 --- /dev/null +++ b/Mindscape.Raygun4Net.AspNetCore/HttpRequestExtensions.cs @@ -0,0 +1,31 @@ +using System.Net; +using Microsoft.AspNetCore.Http; + +namespace Mindscape.Raygun4Net.AspNetCore; + +internal static class HttpRequestExtensions +{ + /// + /// Returns true if the IP address of the request originator was 127.0.0.1 or if the IP address of the request was the same as the server's IP address. + /// + /// + /// Credit to Filip W for the initial implementation of this method. + /// See http://www.strathweb.com/2016/04/request-islocal-in-asp-net-core/ + /// + public static bool IsLocal(this HttpRequest req) + { + var connection = req.HttpContext.Connection; + if (connection.RemoteIpAddress != null) + { + return (connection.LocalIpAddress != null && connection.RemoteIpAddress.Equals(connection.LocalIpAddress)) || IPAddress.IsLoopback(connection.RemoteIpAddress); + } + + // for in memory TestServer or when dealing with default connection info + if (connection.RemoteIpAddress == null && connection.LocalIpAddress == null) + { + return true; + } + + return false; + } +} \ No newline at end of file diff --git a/Mindscape.Raygun4Net.AspNetCore/RaygunAspNetMiddleware.cs b/Mindscape.Raygun4Net.AspNetCore/RaygunAspNetMiddleware.cs deleted file mode 100644 index d0179643e..000000000 --- a/Mindscape.Raygun4Net.AspNetCore/RaygunAspNetMiddleware.cs +++ /dev/null @@ -1,153 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Globalization; -using System.IO; -using System.Net; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Options; - -namespace Mindscape.Raygun4Net.AspNetCore -{ - public class RaygunAspNetMiddleware - { - private readonly RequestDelegate _next; - private readonly RaygunMiddlewareSettings _middlewareSettings; - private readonly RaygunSettings _settings; - - internal const string UnhandledExceptionTag = "UnhandledException"; - - public RaygunAspNetMiddleware(RequestDelegate next, IOptions settings, RaygunMiddlewareSettings middlewareSettings) - { - _next = next; - _middlewareSettings = middlewareSettings; - - _settings = _middlewareSettings.ClientProvider.GetRaygunSettings(settings.Value ?? new RaygunSettings()); - } - - public async Task Invoke(HttpContext httpContext) - { - MemoryStream buffer = null; - Stream originalRequestBody = null; - - if (_settings.ReplaceUnseekableRequestStreams) - { - try - { - var contentType = httpContext.Request.ContentType; - //ignore conditions - var streamIsNull = httpContext.Request.Body == Stream.Null; - var streamIsRewindable = httpContext.Request.Body.CanSeek; - var isFormUrlEncoded = contentType != null && CultureInfo.InvariantCulture.CompareInfo.IndexOf(contentType, "application/x-www-form-urlencoded", CompareOptions.IgnoreCase) >= 0; - var isTextHtml = contentType != null && CultureInfo.InvariantCulture.CompareInfo.IndexOf(contentType, "text/html", CompareOptions.IgnoreCase) >= 0; - var isHttpGet = httpContext.Request.Method == "GET"; //should be no request body to be concerned with - - //if any of the ignore conditions apply, don't modify the Body Stream - if (!(streamIsNull || isFormUrlEncoded || streamIsRewindable || isTextHtml || isHttpGet)) - { - //copy, rewind and replace the stream - buffer = new MemoryStream(); - originalRequestBody = httpContext.Request.Body; - - await originalRequestBody.CopyToAsync(buffer); - buffer.Seek(0, SeekOrigin.Begin); - - httpContext.Request.Body = buffer; - } - } - catch (Exception e) - { - Debug.WriteLine(string.Format("Error replacing request stream {0}", e.Message)); - - if (_settings.ThrowOnError) - { - throw; - } - } - } - - try - { - await _next.Invoke(httpContext); - } - catch (Exception e) - { - if (_settings.ExcludeErrorsFromLocal && httpContext.Request.IsLocal()) - { - throw; - } - - var client = _middlewareSettings.ClientProvider.GetClient(_settings, httpContext); - await client.SendInBackground(e, new List{UnhandledExceptionTag}); - throw; - } - finally - { - buffer?.Dispose(); - if (originalRequestBody != null) - { - httpContext.Request.Body = originalRequestBody; - } - } - } - } - - public static class ApplicationBuilderExtensions - { - public static IApplicationBuilder UseRaygun(this IApplicationBuilder app) - { - return app.UseMiddleware(); - } - - public static IServiceCollection AddRaygun(this IServiceCollection services, IConfiguration configuration) - { - services.Configure(configuration.GetSection("RaygunSettings")); - - services.AddTransient(_ => new DefaultRaygunAspNetCoreClientProvider()); - services.AddSingleton(); - - return services; - } - - public static IServiceCollection AddRaygun(this IServiceCollection services, IConfiguration configuration, RaygunMiddlewareSettings middlewareSettings) - { - services.Configure(configuration.GetSection("RaygunSettings")); - - services.AddTransient(_ => middlewareSettings.ClientProvider ?? new DefaultRaygunAspNetCoreClientProvider()); - services.AddTransient(_ => middlewareSettings); - - return services; - } - } - - internal static class HttpRequestExtensions - { - /// - /// Returns true if the IP address of the request originator was 127.0.0.1 or if the IP address of the request was the same as the server's IP address. - /// - /// - /// Credit to Filip W for the initial implementation of this method. - /// See http://www.strathweb.com/2016/04/request-islocal-in-asp-net-core/ - /// - public static bool IsLocal(this HttpRequest req) - { - var connection = req.HttpContext.Connection; - if (connection.RemoteIpAddress != null) - { - return (connection.LocalIpAddress != null && connection.RemoteIpAddress.Equals(connection.LocalIpAddress)) || IPAddress.IsLoopback(connection.RemoteIpAddress); - } - - // for in memory TestServer or when dealing with default connection info - if (connection.RemoteIpAddress == null && connection.LocalIpAddress == null) - { - return true; - } - - return false; - } - } -} \ No newline at end of file diff --git a/Mindscape.Raygun4Net.AspNetCore/RaygunClient.cs b/Mindscape.Raygun4Net.AspNetCore/RaygunClient.cs index 25107db10..1473017cb 100644 --- a/Mindscape.Raygun4Net.AspNetCore/RaygunClient.cs +++ b/Mindscape.Raygun4Net.AspNetCore/RaygunClient.cs @@ -1,316 +1,270 @@ using System; -using System.Collections; -using System.Collections.Generic; using System.Linq; using System.Net.Http; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Http; -using Mindscape.Raygun4Net.AspNetCore.Builders; using Mindscape.Raygun4Net.Filters; -namespace Mindscape.Raygun4Net.AspNetCore +namespace Mindscape.Raygun4Net.AspNetCore; + +public class RaygunClient : RaygunClientBase { - public class RaygunClient : RaygunClientBase + private readonly RaygunRequestMessageOptions _requestMessageOptions = new(); + + public RaygunClient(string apiKey) + : this(new RaygunSettings {ApiKey = apiKey}) { - protected readonly RaygunRequestMessageOptions _requestMessageOptions = new(); - - private readonly ThreadLocal _currentHttpContext = new(() => null); - private readonly ThreadLocal _currentRequestMessage = new(() => null); - private readonly ThreadLocal _currentResponseMessage = new(() => null); - - public RaygunClient(string apiKey) - : this(new RaygunSettings {ApiKey = apiKey}) - { - } + } - public RaygunClient(RaygunSettings settings, HttpContext context = null, HttpClient httpClient = null) + public RaygunClient(RaygunSettings settings, HttpClient httpClient = null) : base(settings, httpClient) + { + if (settings.IgnoreSensitiveFieldNames != null) { - if (settings.IgnoreSensitiveFieldNames != null) - { - var ignoredNames = settings.IgnoreSensitiveFieldNames; - IgnoreSensitiveFieldNames(ignoredNames); - } - - if (settings.IgnoreQueryParameterNames != null) - { - var ignoredNames = settings.IgnoreQueryParameterNames; - IgnoreQueryParameterNames(ignoredNames); - } - - if (settings.IgnoreFormFieldNames != null) - { - var ignoredNames = settings.IgnoreFormFieldNames; - IgnoreFormFieldNames(ignoredNames); - } - - if (settings.IgnoreHeaderNames != null) - { - var ignoredNames = settings.IgnoreHeaderNames; - IgnoreHeaderNames(ignoredNames); - } - - if (settings.IgnoreCookieNames != null) - { - var ignoredNames = settings.IgnoreCookieNames; - IgnoreCookieNames(ignoredNames); - } - - if (settings.IgnoreServerVariableNames != null) - { - var ignoredNames = settings.IgnoreServerVariableNames; - IgnoreServerVariableNames(ignoredNames); - } - - if (!string.IsNullOrEmpty(settings.ApplicationVersion)) - { - ApplicationVersion = settings.ApplicationVersion; - } - - IsRawDataIgnored = settings.IsRawDataIgnored; - IsRawDataIgnoredWhenFilteringFailed = settings.IsRawDataIgnoredWhenFilteringFailed; - - UseXmlRawDataFilter = settings.UseXmlRawDataFilter; - UseKeyValuePairRawDataFilter = settings.UseKeyValuePairRawDataFilter; - - if (context != null) - { - SetCurrentContext(context); - } - } - - RaygunSettings GetSettings() - { - return (RaygunSettings) _settings; + var ignoredNames = settings.IgnoreSensitiveFieldNames; + IgnoreSensitiveFieldNames(ignoredNames); } - /// - /// Adds a list of keys to remove from the following sections of the - /// - /// - /// - /// - /// - /// - /// Keys to be stripped from the . - public void IgnoreSensitiveFieldNames(params string[] names) + if (settings.IgnoreQueryParameterNames != null) { - _requestMessageOptions.AddSensitiveFieldNames(names); + var ignoredNames = settings.IgnoreQueryParameterNames; + IgnoreQueryParameterNames(ignoredNames); } - /// - /// Adds a list of keys to remove from the property of the - /// - /// Keys to be stripped from the - public void IgnoreQueryParameterNames(params string[] names) + if (settings.IgnoreFormFieldNames != null) { - _requestMessageOptions.AddQueryParameterNames(names); + var ignoredNames = settings.IgnoreFormFieldNames; + IgnoreFormFieldNames(ignoredNames); } - /// - /// Adds a list of keys to ignore when attaching the Form data of an HTTP POST request. This allows - /// you to remove sensitive data from the transmitted copy of the Form on the HttpRequest by specifying the keys you want removed. - /// This method is only effective in a web context. - /// - /// Keys to be stripped from the copy of the Form NameValueCollection when sending to Raygun. - public void IgnoreFormFieldNames(params string[] names) + if (settings.IgnoreHeaderNames != null) { - _requestMessageOptions.AddFormFieldNames(names); + var ignoredNames = settings.IgnoreHeaderNames; + IgnoreHeaderNames(ignoredNames); } - /// - /// Adds a list of keys to ignore when attaching the headers of an HTTP POST request. This allows - /// you to remove sensitive data from the transmitted copy of the Headers on the HttpRequest by specifying the keys you want removed. - /// This method is only effective in a web context. - /// - /// Keys to be stripped from the copy of the Headers NameValueCollection when sending to Raygun. - public void IgnoreHeaderNames(params string[] names) + if (settings.IgnoreCookieNames != null) { - _requestMessageOptions.AddHeaderNames(names); + var ignoredNames = settings.IgnoreCookieNames; + IgnoreCookieNames(ignoredNames); } - /// - /// Adds a list of keys to ignore when attaching the cookies of an HTTP POST request. This allows - /// you to remove sensitive data from the transmitted copy of the Cookies on the HttpRequest by specifying the keys you want removed. - /// This method is only effective in a web context. - /// - /// Keys to be stripped from the copy of the Cookies NameValueCollection when sending to Raygun. - public void IgnoreCookieNames(params string[] names) + if (settings.IgnoreServerVariableNames != null) { - _requestMessageOptions.AddCookieNames(names); + var ignoredNames = settings.IgnoreServerVariableNames; + IgnoreServerVariableNames(ignoredNames); } - /// - /// Adds a list of keys to ignore when attaching the server variables of an HTTP POST request. This allows - /// you to remove sensitive data from the transmitted copy of the ServerVariables on the HttpRequest by specifying the keys you want removed. - /// This method is only effective in a web context. - /// - /// Keys to be stripped from the copy of the ServerVariables NameValueCollection when sending to Raygun. - public void IgnoreServerVariableNames(params string[] names) + if (!string.IsNullOrEmpty(settings.ApplicationVersion)) { - _requestMessageOptions.AddServerVariableNames(names); + ApplicationVersion = settings.ApplicationVersion; } - /// - /// Specifies whether or not RawData from web requests is ignored when sending reports to Raygun. - /// The default is false which means RawData will be sent to Raygun. - /// - public bool IsRawDataIgnored - { - get { return _requestMessageOptions.IsRawDataIgnored; } - set - { - _requestMessageOptions.IsRawDataIgnored = value; - } - } + IsRawDataIgnored = settings.IsRawDataIgnored; + IsRawDataIgnoredWhenFilteringFailed = settings.IsRawDataIgnoredWhenFilteringFailed; - /// - /// Specifies whether or not RawData from web requests is ignored when sensitive values are seen and unable to be removed due to failing to parse the contents. - /// The default is false which means RawData will not be ignored when filtering fails. - /// - public bool IsRawDataIgnoredWhenFilteringFailed - { - get { return _requestMessageOptions.IsRawDataIgnoredWhenFilteringFailed; } - set { _requestMessageOptions.IsRawDataIgnoredWhenFilteringFailed = value; } - } + UseXmlRawDataFilter = settings.UseXmlRawDataFilter; + UseKeyValuePairRawDataFilter = settings.UseKeyValuePairRawDataFilter; + } - /// - /// Specifies whether or not RawData from web requests is filtered of sensitive values using an XML parser. - /// - /// true if use xml raw data filter; otherwise, false. - public bool UseXmlRawDataFilter - { - get { return _requestMessageOptions.UseXmlRawDataFilter; } - set { _requestMessageOptions.UseXmlRawDataFilter = value; } - } + private Lazy Settings => new(() => (RaygunSettings) _settings); + + /// + /// Adds a list of keys to remove from the following sections of the + /// + /// + /// + /// + /// + /// + /// Keys to be stripped from the . + public void IgnoreSensitiveFieldNames(params string[] names) + { + _requestMessageOptions.AddSensitiveFieldNames(names); + } - /// - /// Specifies whether or not RawData from web requests is filtered of sensitive values using an KeyValuePair parser. - /// - /// true if use key pair raw data filter; otherwise, false. - public bool UseKeyValuePairRawDataFilter - { - get { return _requestMessageOptions.UseKeyValuePairRawDataFilter; } - set { _requestMessageOptions.UseKeyValuePairRawDataFilter = value; } - } + /// + /// Adds a list of keys to remove from the property of the + /// + /// Keys to be stripped from the + public void IgnoreQueryParameterNames(params string[] names) + { + _requestMessageOptions.AddQueryParameterNames(names); + } - /// - /// Add an implementation to be used when capturing the raw data - /// of a HTTP request. This filter will be passed the request raw data and is expected to remove - /// or replace values whose keys are found in the list supplied to the Filter method. - /// - /// Custom raw data filter implementation. - public void AddRawDataFilter(IRaygunDataFilter filter) - { - _requestMessageOptions.AddRawDataFilter(filter); - } + /// + /// Adds a list of keys to ignore when attaching the Form data of an HTTP POST request. This allows + /// you to remove sensitive data from the transmitted copy of the Form on the HttpRequest by specifying the keys you want removed. + /// This method is only effective in a web context. + /// + /// Keys to be stripped from the copy of the Form NameValueCollection when sending to Raygun. + public void IgnoreFormFieldNames(params string[] names) + { + _requestMessageOptions.AddFormFieldNames(names); + } - protected override bool CanSend(RaygunMessage message) - { - if (message?.Details?.Response == null) - { - return true; - } + /// + /// Adds a list of keys to ignore when attaching the headers of an HTTP POST request. This allows + /// you to remove sensitive data from the transmitted copy of the Headers on the HttpRequest by specifying the keys you want removed. + /// This method is only effective in a web context. + /// + /// Keys to be stripped from the copy of the Headers NameValueCollection when sending to Raygun. + public void IgnoreHeaderNames(params string[] names) + { + _requestMessageOptions.AddHeaderNames(names); + } - RaygunSettings settings = GetSettings(); - if (settings.ExcludedStatusCodes == null) - { - return true; - } + /// + /// Adds a list of keys to ignore when attaching the cookies of an HTTP POST request. This allows + /// you to remove sensitive data from the transmitted copy of the Cookies on the HttpRequest by specifying the keys you want removed. + /// This method is only effective in a web context. + /// + /// Keys to be stripped from the copy of the Cookies NameValueCollection when sending to Raygun. + public void IgnoreCookieNames(params string[] names) + { + _requestMessageOptions.AddCookieNames(names); + } - return !settings.ExcludedStatusCodes.Contains(message.Details.Response.StatusCode); - } + /// + /// Adds a list of keys to ignore when attaching the server variables of an HTTP POST request. This allows + /// you to remove sensitive data from the transmitted copy of the ServerVariables on the HttpRequest by specifying the keys you want removed. + /// This method is only effective in a web context. + /// + /// Keys to be stripped from the copy of the ServerVariables NameValueCollection when sending to Raygun. + public void IgnoreServerVariableNames(params string[] names) + { + _requestMessageOptions.AddServerVariableNames(names); + } - /// - public override async Task SendAsync(Exception exception, IList tags, IDictionary userCustomData, RaygunIdentifierMessage userInfo = null) + /// + /// Specifies whether or not RawData from web requests is ignored when sending reports to Raygun. + /// The default is false which means RawData will be sent to Raygun. + /// + public bool IsRawDataIgnored + { + get { return _requestMessageOptions.IsRawDataIgnored; } + set { - if (CanSend(exception)) - { - RaygunRequestMessage currentRequestMessage = await BuildRequestMessage(); - RaygunResponseMessage currentResponseMessage = BuildResponseMessage(); - - _currentHttpContext.Value = null; - - _currentRequestMessage.Value = currentRequestMessage; - _currentResponseMessage.Value = currentResponseMessage; - - await StripAndSend(exception, tags, userCustomData, null); - FlagAsSent(exception); - } + _requestMessageOptions.IsRawDataIgnored = value; } + } - /// - /// Asynchronously transmits an exception to Raygun. - /// - /// The exception to deliver. - /// A list of strings associated with the message. - /// A key-value collection of custom data that will be added to the payload. - /// Information about the user including the identity string. - public override async Task SendInBackground(Exception exception, IList tags = null, IDictionary userCustomData = null, RaygunIdentifierMessage userInfo = null) - { - if (CanSend(exception)) - { - // We need to process the Request on the current thread, - // otherwise it will be disposed while we are using it on the other thread. - // BuildRequestMessage relies on ReadFormAsync so we need to await it to ensure it's processed before continuing. - RaygunRequestMessage currentRequestMessage = await BuildRequestMessage(); - RaygunResponseMessage currentResponseMessage = BuildResponseMessage(); - - _currentHttpContext.Value = null; - - // Do not await here as we want this to execute in the background and allow the request to finish. - _ = Task.Run(async () => - { - _currentRequestMessage.Value = currentRequestMessage; - _currentResponseMessage.Value = currentResponseMessage; + /// + /// Specifies whether or not RawData from web requests is ignored when sensitive values are seen and unable to be removed due to failing to parse the contents. + /// The default is false which means RawData will not be ignored when filtering fails. + /// + public bool IsRawDataIgnoredWhenFilteringFailed + { + get { return _requestMessageOptions.IsRawDataIgnoredWhenFilteringFailed; } + set { _requestMessageOptions.IsRawDataIgnoredWhenFilteringFailed = value; } + } - await StripAndSend(exception, tags, userCustomData, userInfo); - }); + /// + /// Specifies whether or not RawData from web requests is filtered of sensitive values using an XML parser. + /// + /// true if use xml raw data filter; otherwise, false. + public bool UseXmlRawDataFilter + { + get { return _requestMessageOptions.UseXmlRawDataFilter; } + set { _requestMessageOptions.UseXmlRawDataFilter = value; } + } - FlagAsSent(exception); - } - } + /// + /// Specifies whether or not RawData from web requests is filtered of sensitive values using an KeyValuePair parser. + /// + /// true if use key pair raw data filter; otherwise, false. + public bool UseKeyValuePairRawDataFilter + { + get => _requestMessageOptions.UseKeyValuePairRawDataFilter; + set => _requestMessageOptions.UseKeyValuePairRawDataFilter = value; + } - private async Task BuildRequestMessage() - { - return _currentHttpContext.Value != null ? await RaygunAspNetCoreRequestMessageBuilder.Build(_currentHttpContext.Value, _requestMessageOptions) : null; - } + /// + /// Add an implementation to be used when capturing the raw data + /// of a HTTP request. This filter will be passed the request raw data and is expected to remove + /// or replace values whose keys are found in the list supplied to the Filter method. + /// + /// Custom raw data filter implementation. + public void AddRawDataFilter(IRaygunDataFilter filter) + { + _requestMessageOptions.AddRawDataFilter(filter); + } - private RaygunResponseMessage BuildResponseMessage() + protected override bool CanSend(RaygunMessage message) + { + if (message?.Details?.Response == null) { - return _currentHttpContext.Value != null ? RaygunAspNetCoreResponseMessageBuilder.Build(_currentHttpContext.Value) : null; + return true; } - public RaygunClient SetCurrentContext(HttpContext request) + var settings = Settings.Value; + if (settings.ExcludedStatusCodes == null) { - _currentHttpContext.Value = request; - return this; + return true; } - protected override async Task BuildMessage(Exception exception, IList tags, IDictionary userCustomData, RaygunIdentifierMessage userInfoMessage) - { - var message = RaygunMessageBuilder.New(GetSettings()) - .SetResponseDetails(_currentResponseMessage.Value) - .SetRequestDetails(_currentRequestMessage.Value) - .SetEnvironmentDetails() - .SetMachineName(Environment.MachineName) - .SetExceptionDetails(exception) - .SetClientDetails() - .SetVersion(ApplicationVersion) - .SetTags(tags) - .SetUserCustomData(userCustomData) - .SetUser(userInfoMessage ?? UserInfo ?? (!String.IsNullOrEmpty(User) ? new RaygunIdentifierMessage(User) : null)) - .Build(); - - var customGroupingKey = await OnCustomGroupingKey(exception, message); - if (string.IsNullOrEmpty(customGroupingKey) == false) - { - message.Details.GroupingKey = customGroupingKey; - } - - return message; - } + return !settings.ExcludedStatusCodes.Contains(message.Details.Response.StatusCode); } -} + ///// + // public override async Task SendAsync(Exception exception, IList tags, IDictionary userCustomData, RaygunIdentifierMessage userInfo = null) + // { + // if (CanSend(exception)) + // { + // RaygunRequestMessage currentRequestMessage = await BuildRequestMessage(); + // RaygunResponseMessage currentResponseMessage = BuildResponseMessage(); + // + // //_currentHttpContext.Value = null; + // + // _currentRequestMessage.Value = currentRequestMessage; + // _currentResponseMessage.Value = currentResponseMessage; + // + // await StripAndSend(exception, tags, userCustomData, null); + // FlagAsSent(exception); + // } + // } + + // /// + // /// Asynchronously transmits an exception to Raygun. + // /// + // /// The exception to deliver. + // /// A list of strings associated with the message. + // /// A key-value collection of custom data that will be added to the payload. + // /// Information about the user including the identity string. + // public override async Task SendInBackground(Exception exception, IList tags = null, IDictionary userCustomData = null, RaygunIdentifierMessage userInfo = null) + // { + // if (CanSend(exception)) + // { + // // We need to process the Request on the current thread, + // // otherwise it will be disposed while we are using it on the other thread. + // // BuildRequestMessage relies on ReadFormAsync so we need to await it to ensure it's processed before continuing. + // var currentRequestMessage = await BuildRequestMessage(); + // var currentResponseMessage = BuildResponseMessage(); + // + // var exceptions = StripWrapperExceptions(exception); + // + // foreach (var ex in exceptions) + // { + // if (!_backgroundMessageProcessor.Enqueue(async () => await BuildMessage(ex, tags, userCustomData, userInfo, + // builder => + // { + // builder.SetResponseDetails(currentResponseMessage); + // builder.SetRequestDetails(currentRequestMessage); + // }))) + // { + // Debug.WriteLine("Could not add message to background queue. Dropping exception: {0}", ex); + // } + // } + // + // FlagAsSent(exception); + // } + // } + + // internal async Task BuildRequestMessage() + // { + // return _httpContextAccessor?.HttpContext != null ? await RaygunAspNetCoreRequestMessageBuilder.Build(_httpContextAccessor?.HttpContext, _requestMessageOptions) : null; + // } + // + // internal RaygunResponseMessage BuildResponseMessage() + // { + // return _httpContextAccessor?.HttpContext != null ? RaygunAspNetCoreResponseMessageBuilder.Build(_httpContextAccessor?.HttpContext) : null; + // } +} \ No newline at end of file diff --git a/Mindscape.Raygun4Net.AspNetCore/RaygunClientExtensions.cs b/Mindscape.Raygun4Net.AspNetCore/RaygunClientExtensions.cs new file mode 100644 index 000000000..2c9dff297 --- /dev/null +++ b/Mindscape.Raygun4Net.AspNetCore/RaygunClientExtensions.cs @@ -0,0 +1,50 @@ +#nullable enable + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Mindscape.Raygun4Net.AspNetCore.Builders; + +namespace Mindscape.Raygun4Net.AspNetCore; + +public static class RaygunClientExtensions +{ + /// + /// Asynchronously transmits an exception to Raygun. + /// + /// The exception to deliver. + /// A Banana. + /// A list of strings associated with the message. + /// A Carrot. + public static async Task SendInBackground(this RaygunClient client, Exception exception, IList tags, HttpContext? context) + { + if (client.CanSend(exception)) + { + // We need to process the Request on the current thread, + // otherwise it will be disposed while we are using it on the other thread. + // BuildRequestMessage relies on ReadFormAsync so we need to await it to ensure it's processed before continuing. + var currentRequestMessage = await RaygunAspNetCoreRequestMessageBuilder.Build(context, new RaygunRequestMessageOptions()); + var currentResponseMessage = RaygunAspNetCoreResponseMessageBuilder.Build(context); + + var exceptions = client.StripWrapperExceptions(exception); + + foreach (var ex in exceptions) + { + var msg = await client.BuildMessage(ex, tags, null, null, + builder => + { + builder.SetResponseDetails(currentResponseMessage); + builder.SetRequestDetails(currentRequestMessage); + }); + if (!client.Enqueue(msg)) + { + Debug.WriteLine("Could not add message to background queue. Dropping exception: {0}", ex); + } + } + + client.FlagAsSent(exception); + } + } +} \ No newline at end of file diff --git a/Mindscape.Raygun4Net.AspNetCore/RaygunClientProvider.cs b/Mindscape.Raygun4Net.AspNetCore/RaygunClientProvider.cs deleted file mode 100644 index 6d2016fbb..000000000 --- a/Mindscape.Raygun4Net.AspNetCore/RaygunClientProvider.cs +++ /dev/null @@ -1,29 +0,0 @@ -using Microsoft.AspNetCore.Http; - -namespace Mindscape.Raygun4Net.AspNetCore -{ - public interface IRaygunAspNetCoreClientProvider - { - RaygunClient GetClient(RaygunSettings settings); - RaygunClient GetClient(RaygunSettings settings, HttpContext context); - RaygunSettings GetRaygunSettings(RaygunSettings baseSettings); - } - - public class DefaultRaygunAspNetCoreClientProvider : IRaygunAspNetCoreClientProvider - { - public virtual RaygunClient GetClient(RaygunSettings settings) - { - return GetClient(settings, null); - } - - public virtual RaygunClient GetClient(RaygunSettings settings, HttpContext context) - { - return new RaygunClient(settings, context); - } - - public virtual RaygunSettings GetRaygunSettings(RaygunSettings baseSettings) - { - return baseSettings; - } - } -} \ No newline at end of file diff --git a/Mindscape.Raygun4Net.AspNetCore/RaygunMiddleware.cs b/Mindscape.Raygun4Net.AspNetCore/RaygunMiddleware.cs new file mode 100644 index 000000000..9ff6f6dd7 --- /dev/null +++ b/Mindscape.Raygun4Net.AspNetCore/RaygunMiddleware.cs @@ -0,0 +1,53 @@ +#nullable enable + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Options; + +namespace Mindscape.Raygun4Net.AspNetCore; + +public class RaygunMiddleware +{ + private readonly RequestDelegate _next; + private readonly RaygunSettings _settings; + private readonly RaygunClient _client; + private readonly IHttpContextAccessor _httpContextAccessor; + + private const string UnhandledExceptionTag = "UnhandledException"; + + public RaygunMiddleware(RequestDelegate next, + IOptions settings, + RaygunClient raygunClient, + IHttpContextAccessor httpContextAccessor) + { + _next = next; + _settings = settings.Value ?? new RaygunSettings(); + _client = raygunClient; + _httpContextAccessor = httpContextAccessor; + } + + public async Task Invoke(HttpContext httpContext) + { + httpContext.Request.EnableBuffering(); + + try + { + // Let the request get invoked as normal + await _next.Invoke(httpContext); + } + catch (Exception e) + { + // If an exception was captured but we exclude the capture in local then just throw the exception + if (_settings.ExcludeErrorsFromLocal && httpContext.Request.IsLocal()) + { + throw; + } + + // Capture the exception and send it to Raygun + await _client.SendInBackground(e, new List { UnhandledExceptionTag }, _httpContextAccessor.HttpContext); + throw; + } + } +} \ No newline at end of file diff --git a/Mindscape.Raygun4Net.AspNetCore/RaygunMiddlewareSettings.cs b/Mindscape.Raygun4Net.AspNetCore/RaygunMiddlewareSettings.cs deleted file mode 100644 index 3b0ed08d1..000000000 --- a/Mindscape.Raygun4Net.AspNetCore/RaygunMiddlewareSettings.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace Mindscape.Raygun4Net.AspNetCore -{ - public class RaygunMiddlewareSettings - { - public IRaygunAspNetCoreClientProvider ClientProvider { get; set; } - - public RaygunMiddlewareSettings() - { - ClientProvider = new DefaultRaygunAspNetCoreClientProvider(); - } - } -} \ No newline at end of file diff --git a/Mindscape.Raygun4Net.AspNetCore/RaygunRequestMessageOptions.cs b/Mindscape.Raygun4Net.AspNetCore/RaygunRequestMessageOptions.cs index b51ff2f8b..2136d4a76 100644 --- a/Mindscape.Raygun4Net.AspNetCore/RaygunRequestMessageOptions.cs +++ b/Mindscape.Raygun4Net.AspNetCore/RaygunRequestMessageOptions.cs @@ -5,18 +5,14 @@ namespace Mindscape.Raygun4Net.AspNetCore { public class RaygunRequestMessageOptions { - private readonly List _ignoredSensitiveFieldNames = new List(); - private readonly List _ignoredQueryParameterNames = new List(); - private readonly List _ignoredFormFieldNames = new List(); - private readonly List _ignoreHeaderNames = new List(); - private readonly List _ignoreCookieNames = new List(); - private readonly List _ignoreServerVariableNames = new List(); - private bool _isRawDataIgnored; - private bool _isRawDataIgnoredWhenFilteringFailed; - private bool _useXmlRawDataFilter; - private bool _useKeyValuePairRawDataFilter; - - private List _rawDataFilters = new List(); + private readonly List _ignoredSensitiveFieldNames = new(); + private readonly List _ignoredQueryParameterNames = new(); + private readonly List _ignoredFormFieldNames = new(); + private readonly List _ignoreHeaderNames = new(); + private readonly List _ignoreCookieNames = new(); + private readonly List _ignoreServerVariableNames = new(); + + private readonly List _rawDataFilters = new(); public RaygunRequestMessageOptions() { } @@ -37,32 +33,13 @@ public RaygunRequestMessageOptions(IEnumerable sensitiveFieldNames, // RawData - public bool IsRawDataIgnored - { - get { return _isRawDataIgnored; } - set - { - _isRawDataIgnored = value; - } - } + public bool IsRawDataIgnored { get; set; } = false; - public bool IsRawDataIgnoredWhenFilteringFailed - { - get { return _isRawDataIgnoredWhenFilteringFailed; } - set { _isRawDataIgnoredWhenFilteringFailed = value; } - } + public bool IsRawDataIgnoredWhenFilteringFailed { get; set; } = false; - public bool UseXmlRawDataFilter - { - get { return _useXmlRawDataFilter; } - set { _useXmlRawDataFilter = value; } - } + public bool UseXmlRawDataFilter { get; set; } = false; - public bool UseKeyValuePairRawDataFilter - { - get { return _useKeyValuePairRawDataFilter; } - set { _useKeyValuePairRawDataFilter = value; } - } + public bool UseKeyValuePairRawDataFilter { get; set; } = false; public void AddRawDataFilter(IRaygunDataFilter filter) { diff --git a/Mindscape.Raygun4Net.Mvc.Tests/Mindscape.Raygun4Net.Mvc.Tests.csproj b/Mindscape.Raygun4Net.Mvc.Tests/Mindscape.Raygun4Net.Mvc.Tests.csproj index c1d904f81..4a4a5b185 100644 --- a/Mindscape.Raygun4Net.Mvc.Tests/Mindscape.Raygun4Net.Mvc.Tests.csproj +++ b/Mindscape.Raygun4Net.Mvc.Tests/Mindscape.Raygun4Net.Mvc.Tests.csproj @@ -21,7 +21,7 @@ - + diff --git a/Mindscape.Raygun4Net.Mvc/Mindscape.Raygun4Net.Mvc.csproj b/Mindscape.Raygun4Net.Mvc/Mindscape.Raygun4Net.Mvc.csproj index 17b8481b1..73715a650 100644 --- a/Mindscape.Raygun4Net.Mvc/Mindscape.Raygun4Net.Mvc.csproj +++ b/Mindscape.Raygun4Net.Mvc/Mindscape.Raygun4Net.Mvc.csproj @@ -24,7 +24,7 @@ - + @@ -37,6 +37,6 @@ - + \ No newline at end of file diff --git a/Mindscape.Raygun4Net.NetCore.Common/Builders/RaygunMessageBuilder.cs b/Mindscape.Raygun4Net.NetCore.Common/Builders/RaygunMessageBuilder.cs index 82db17ff3..5070042fe 100644 --- a/Mindscape.Raygun4Net.NetCore.Common/Builders/RaygunMessageBuilder.cs +++ b/Mindscape.Raygun4Net.NetCore.Common/Builders/RaygunMessageBuilder.cs @@ -112,5 +112,11 @@ public IRaygunMessageBuilder SetResponseDetails(RaygunResponseMessage message) _raygunMessage.Details.Response = message; return this; } + + public IRaygunMessageBuilder Customise(Action customiseMessage) + { + customiseMessage?.Invoke(this); + return this; + } } } \ No newline at end of file diff --git a/Mindscape.Raygun4Net.NetCore.Common/Mindscape.Raygun4Net.NetCore.Common.csproj b/Mindscape.Raygun4Net.NetCore.Common/Mindscape.Raygun4Net.NetCore.Common.csproj index 5a05e5347..590cfd481 100644 --- a/Mindscape.Raygun4Net.NetCore.Common/Mindscape.Raygun4Net.NetCore.Common.csproj +++ b/Mindscape.Raygun4Net.NetCore.Common/Mindscape.Raygun4Net.NetCore.Common.csproj @@ -24,10 +24,12 @@ + + diff --git a/Mindscape.Raygun4Net.NetCore.Common/RaygunClientBase.cs b/Mindscape.Raygun4Net.NetCore.Common/RaygunClientBase.cs index 1fc2f999d..1bfb94958 100644 --- a/Mindscape.Raygun4Net.NetCore.Common/RaygunClientBase.cs +++ b/Mindscape.Raygun4Net.NetCore.Common/RaygunClientBase.cs @@ -18,7 +18,7 @@ public abstract class RaygunClientBase /// /// If no HttpClient is provided to the constructor, this will be used. /// - private static readonly HttpClient DefaultClient = new HttpClient + private static readonly HttpClient DefaultClient = new () { // The default timeout is 100 seconds for the HttpClient, Timeout = TimeSpan.FromSeconds(30) @@ -30,7 +30,7 @@ public abstract class RaygunClientBase private readonly HttpClient _client; private readonly string _apiKey; - private readonly List _wrapperExceptions = new List(); + private readonly List _wrapperExceptions = new(); private bool _handlingRecursiveErrorSending; private bool _handlingRecursiveGrouping; @@ -149,19 +149,24 @@ public void RemoveWrapperExceptions(params Type[] wrapperExceptions) } } - protected virtual bool CanSend(Exception exception) + internal bool CanSend(Exception exception) { return exception?.Data == null || !exception.Data.Contains(SentKey) || false.Equals(exception.Data[SentKey]); } - protected void FlagAsSent(Exception exception) + internal bool Enqueue(RaygunMessage msg) + { + return _backgroundMessageProcessor.Enqueue(msg); + } + + internal void FlagAsSent(Exception exception) { if (exception?.Data != null) { try { - Type[] genericTypes = exception.Data.GetType().GetTypeInfo().GenericTypeArguments; + var genericTypes = exception.Data.GetType().GetTypeInfo().GenericTypeArguments; if (genericTypes.Length == 0 || genericTypes[0].GetTypeInfo().IsAssignableFrom(typeof(string))) { @@ -178,7 +183,7 @@ protected void FlagAsSent(Exception exception) // Returns true if the message can be sent, false if the sending is canceled. protected bool OnSendingMessage(RaygunMessage raygunMessage) { - bool result = true; + var result = true; if (!_handlingRecursiveErrorSending) { @@ -335,7 +340,7 @@ public virtual async Task SendAsync(Exception exception, IList tags, IDi /// A list of strings associated with the message. /// A key-value collection of custom data that will be added to the payload. /// Information about the user including the identity string. - public virtual async Task SendInBackground(Exception exception, IList tags = null, IDictionary userCustomData = null, RaygunIdentifierMessage userInfo = null) + public virtual Task SendInBackground(Exception exception, IList tags = null, IDictionary userCustomData = null, RaygunIdentifierMessage userInfo = null) { if (CanSend(exception)) { @@ -350,6 +355,8 @@ public virtual async Task SendInBackground(Exception exception, IList ta FlagAsSent(exception); } + + return Task.CompletedTask; } /// @@ -367,24 +374,25 @@ public Task SendInBackground(RaygunMessage raygunMessage) return Task.CompletedTask; } - internal void FlagExceptionAsSent(Exception exception) - { - FlagAsSent(exception); - } - - protected virtual async Task BuildMessage(Exception exception, IList tags, - IDictionary userCustomData, RaygunIdentifierMessage userInfo) + internal async Task BuildMessage(Exception exception, + IList tags, + IDictionary userCustomData, + RaygunIdentifierMessage userInfo, + Action customiseMessage = null) { + var thing = userInfo ?? UserInfo ?? (!string.IsNullOrEmpty(User) ? new RaygunIdentifierMessage(User) : null); + var message = RaygunMessageBuilder.New(_settings) - .SetEnvironmentDetails() - .SetMachineName(Environment.MachineName) - .SetExceptionDetails(exception) - .SetClientDetails() - .SetVersion(ApplicationVersion) - .SetTags(tags) - .SetUserCustomData(userCustomData) - .SetUser(userInfo ?? UserInfo ?? (!String.IsNullOrEmpty(User) ? new RaygunIdentifierMessage(User) : null)) - .Build(); + .Customise(customiseMessage) + .SetEnvironmentDetails() + .SetMachineName(Environment.MachineName) + .SetExceptionDetails(exception) + .SetClientDetails() + .SetVersion(ApplicationVersion) + .SetTags(tags) + .SetUserCustomData(userCustomData) + .SetUser(thing) + .Build(); var customGroupingKey = await OnCustomGroupingKey(exception, message).ConfigureAwait(false); @@ -399,24 +407,24 @@ protected virtual async Task BuildMessage(Exception exception, IL protected async Task StripAndSend(Exception exception, IList tags, IDictionary userCustomData, RaygunIdentifierMessage userInfo) { - foreach (Exception e in StripWrapperExceptions(exception)) + foreach (var e in StripWrapperExceptions(exception)) { await Send(await BuildMessage(e, tags, userCustomData, userInfo).ConfigureAwait(false)).ConfigureAwait(false); } } - protected IEnumerable StripWrapperExceptions(Exception exception) + internal IEnumerable StripWrapperExceptions(Exception exception) { if (exception != null && _wrapperExceptions.Any(wrapperException => exception.GetType() == wrapperException && exception.InnerException != null)) { - AggregateException aggregate = exception as AggregateException; + var aggregate = exception as AggregateException; if (aggregate != null) { - foreach (Exception e in aggregate.InnerExceptions) + foreach (var e in aggregate.InnerExceptions) { - foreach (Exception ex in StripWrapperExceptions(e)) + foreach (var ex in StripWrapperExceptions(e)) { yield return ex; } @@ -424,7 +432,7 @@ protected IEnumerable StripWrapperExceptions(Exception exception) } else { - foreach (Exception e in StripWrapperExceptions(exception.InnerException)) + foreach (var e in StripWrapperExceptions(exception.InnerException)) { yield return e; } @@ -459,7 +467,7 @@ public async Task Send(RaygunMessage raygunMessage, CancellationToken cancellati return; } - bool canSend = OnSendingMessage(raygunMessage) && CanSend(raygunMessage); + var canSend = OnSendingMessage(raygunMessage) && CanSend(raygunMessage); if (!canSend) { diff --git a/Mindscape.Raygun4Net.NetCore.Common/RaygunSettings.cs b/Mindscape.Raygun4Net.NetCore.Common/RaygunSettings.cs index fb48bc799..8aec952a4 100644 --- a/Mindscape.Raygun4Net.NetCore.Common/RaygunSettings.cs +++ b/Mindscape.Raygun4Net.NetCore.Common/RaygunSettings.cs @@ -4,7 +4,7 @@ namespace Mindscape.Raygun4Net { public abstract class RaygunSettingsBase { - internal const string DefaultApiEndPoint = "https://api.raygun.com/entries"; + private const string DefaultApiEndPoint = "https://api.raygun.com/entries"; private const string RaygunMessageQueueMaxVariable = "RAYGUN_MESSAGE_QUEUE_MAX"; public RaygunSettingsBase() diff --git a/Mindscape.Raygun4Net.NetCore.Tests/RaygunClientIntegrationTests.cs b/Mindscape.Raygun4Net.NetCore.Tests/RaygunClientIntegrationTests.cs index 68e21755b..5e28ebd19 100644 --- a/Mindscape.Raygun4Net.NetCore.Tests/RaygunClientIntegrationTests.cs +++ b/Mindscape.Raygun4Net.NetCore.Tests/RaygunClientIntegrationTests.cs @@ -11,8 +11,8 @@ namespace Mindscape.Raygun4Net.NetCore.Tests [TestFixture] public class RaygunClientIntegrationTests { - private HttpClient httpClient = null!; - private MockHttpHandler mockHttp = null!; + private HttpClient _httpClient = null!; + private MockHttpHandler _mockHttp = null!; public class BananaClient : RaygunClient { @@ -24,13 +24,13 @@ public BananaClient(RaygunSettings settings, HttpClient httpClient) : base(setti [SetUp] public void Init() { - mockHttp = new MockHttpHandler(); + _mockHttp = new MockHttpHandler(); } [Test] public async Task SendInBackground_ShouldNotBlock() { - mockHttp.When(match => match.Method(HttpMethod.Post) + _mockHttp.When(match => match.Method(HttpMethod.Post) .RequestUri("https://api.raygun.com/entries")) .Respond(x => { @@ -39,7 +39,7 @@ public async Task SendInBackground_ShouldNotBlock() x.StatusCode(HttpStatusCode.Accepted); }).Verifiable(); - httpClient = new HttpClient(mockHttp); + _httpClient = new HttpClient(_mockHttp); // This test runs using a mocked http client so we don't need a real API key, if you want to run this // and have the data sent to Raygun, remove the `httpClient` parameter from the RaygunClient constructor @@ -47,7 +47,7 @@ public async Task SendInBackground_ShouldNotBlock() var client = new BananaClient(new RaygunSettings { ApiKey = "banana" - }, httpClient); + }, _httpClient); var stopwatch = new Stopwatch(); @@ -76,14 +76,14 @@ public async Task SendInBackground_ShouldNotBlock() await Task.Delay(1000); // Verify that the request was sent 50 times - await mockHttp.VerifyAsync(match => match.Method(HttpMethod.Post) + await _mockHttp.VerifyAsync(match => match.Method(HttpMethod.Post) .RequestUri("https://api.raygun.com/entries"), IsSent.Exactly(50)); } [Test] public async Task SendInBackground_ShouldFail_WhenMaxTasksIsZero() { - mockHttp.When(match => match.Method(HttpMethod.Post) + _mockHttp.When(match => match.Method(HttpMethod.Post) .RequestUri("https://api.raygun.com/entries")) .Respond(x => { @@ -92,7 +92,7 @@ public async Task SendInBackground_ShouldFail_WhenMaxTasksIsZero() x.StatusCode(HttpStatusCode.Accepted); }).Verifiable(); - httpClient = new HttpClient(mockHttp); + _httpClient = new HttpClient(_mockHttp); // This test runs using a mocked http client so we don't need a real API key, if you want to run this // and have the data sent to Raygun, remove the `httpClient` parameter from the RaygunClient constructor @@ -101,7 +101,7 @@ public async Task SendInBackground_ShouldFail_WhenMaxTasksIsZero() { ApiKey = "banana", BackgroundMessageWorkerCount = 0 - }, httpClient); + }, _httpClient); var stopwatch = new Stopwatch(); @@ -130,7 +130,7 @@ public async Task SendInBackground_ShouldFail_WhenMaxTasksIsZero() await Task.Delay(1000); // Verify that the request wasn't sent because there was no worker - await mockHttp.VerifyAsync(match => match.Method(HttpMethod.Post) + await _mockHttp.VerifyAsync(match => match.Method(HttpMethod.Post) .RequestUri("https://api.raygun.com/entries"), IsSent.Exactly(0)); } @@ -140,7 +140,7 @@ public async Task SendInBackground_WithLowQueueMax_DoesNotSendAllRequests() { Environment.SetEnvironmentVariable("RAYGUN_MESSAGE_QUEUE_MAX", "10"); - mockHttp.When(match => match.Method(HttpMethod.Post) + _mockHttp.When(match => match.Method(HttpMethod.Post) .RequestUri("https://api.raygun.com/entries")) .Respond(x => { @@ -149,7 +149,7 @@ public async Task SendInBackground_WithLowQueueMax_DoesNotSendAllRequests() x.StatusCode(HttpStatusCode.Accepted); }).Verifiable(); - httpClient = new HttpClient(mockHttp); + _httpClient = new HttpClient(_mockHttp); // This test runs using a mocked http client so we don't need a real API key, if you want to run this // and have the data sent to Raygun, remove the `httpClient` parameter from the RaygunClient constructor @@ -157,7 +157,7 @@ public async Task SendInBackground_WithLowQueueMax_DoesNotSendAllRequests() var client = new BananaClient(new RaygunSettings { ApiKey = "banana" - }, httpClient); + }, _httpClient); var stopwatch = new Stopwatch(); @@ -186,7 +186,7 @@ public async Task SendInBackground_WithLowQueueMax_DoesNotSendAllRequests() await Task.Delay(1000); // Verify that the request was sent 50 times - await mockHttp.VerifyAsync(match => match.Method(HttpMethod.Post) + await _mockHttp.VerifyAsync(match => match.Method(HttpMethod.Post) .RequestUri("https://api.raygun.com/entries"), IsSent.AtMost(49)); Environment.SetEnvironmentVariable("RAYGUN_MESSAGE_QUEUE_MAX", null); diff --git a/Mindscape.Raygun4Net.NetCore.Tests/ThrottledBackgroundMessageProcessorTests.cs b/Mindscape.Raygun4Net.NetCore.Tests/ThrottledBackgroundMessageProcessorTests.cs index ff35ef611..94fde70f8 100644 --- a/Mindscape.Raygun4Net.NetCore.Tests/ThrottledBackgroundMessageProcessorTests.cs +++ b/Mindscape.Raygun4Net.NetCore.Tests/ThrottledBackgroundMessageProcessorTests.cs @@ -1,5 +1,6 @@ using System; using System.Threading; +using System.Threading.Tasks; using NUnit.Framework; namespace Mindscape.Raygun4Net.NetCore.Tests @@ -10,7 +11,7 @@ public class ThrottledBackgroundMessageProcessorTests [Test] public void ThrottledBackgroundMessageProcessor_WithQueueSpace_AcceptsMessages() { - var cut = new ThrottledBackgroundMessageProcessor(1, 0, async (m, t) => { }); + var cut = new ThrottledBackgroundMessageProcessor(1, 0, (m, t) => { return Task.CompletedTask; }); var enqueued = cut.Enqueue(new RaygunMessage()); Assert.That(enqueued, Is.True); @@ -19,7 +20,7 @@ public void ThrottledBackgroundMessageProcessor_WithQueueSpace_AcceptsMessages() [Test] public void ThrottledBackgroundMessageProcessor_WithFullQueue_DropsMessages() { - var cut = new ThrottledBackgroundMessageProcessor(1, 0, async (m, t) => { }); + var cut = new ThrottledBackgroundMessageProcessor(1, 0, (m, t) => { return Task.CompletedTask; }); cut.Enqueue(new RaygunMessage()); var second = cut.Enqueue(new RaygunMessage()); @@ -32,7 +33,11 @@ public void ThrottledBackgroundMessageProcessor_WithFullQueue_DropsMessages() public void ThrottledBackgroundMessageProcessor_WithNoWorkers_DoesNotProcessMessages() { var processed = false; - var cut = new ThrottledBackgroundMessageProcessor(1, 0, async (m, t) => { processed = true; }); + var cut = new ThrottledBackgroundMessageProcessor(1, 0, (m, t) => + { + processed = true; + return Task.CompletedTask; + }); cut.Enqueue(new RaygunMessage()); @@ -47,10 +52,11 @@ public void ThrottledBackgroundMessageProcessor_WithAtLeastOneWorker_DoesProcess { var processed = false; var resetEventSlim = new ManualResetEventSlim(); - var cut = new ThrottledBackgroundMessageProcessor(1, 1, async (m, t) => + var cut = new ThrottledBackgroundMessageProcessor(1, 1, (m, t) => { processed = true; resetEventSlim.Set(); + return Task.CompletedTask; }); cut.Enqueue(new RaygunMessage()); @@ -66,14 +72,13 @@ public void ThrottledBackgroundMessageProcessor_WithAtLeastOneWorker_DoesProcess [Test] public void ThrottledBackgroundMessageProcessor_CallingDisposeTwice_DoesNotExplode() { - var cut = new ThrottledBackgroundMessageProcessor(1, 0, async (m, t) => { }); - + var cut = new ThrottledBackgroundMessageProcessor(1, 0, (m, t) => { return Task.CompletedTask; }); + Assert.DoesNotThrow(() => { cut.Dispose(); cut.Dispose(); }); - } [Test] @@ -82,8 +87,8 @@ public void ThrottledBackgroundMessageProcessor_ExceptionInProcess_KillsWorkerTh var shouldThrow = true; var secondMessageWasProcessed = false; var resetEventSlim = new ManualResetEventSlim(); - - var cut = new ThrottledBackgroundMessageProcessor(1, 1, async (m, t) => + + var cut = new ThrottledBackgroundMessageProcessor(1, 1, (m, t) => { if (shouldThrow) { @@ -93,19 +98,20 @@ public void ThrottledBackgroundMessageProcessor_ExceptionInProcess_KillsWorkerTh secondMessageWasProcessed = true; resetEventSlim.Set(); + return Task.CompletedTask; }); cut.Enqueue(new RaygunMessage()); resetEventSlim.Wait(TimeSpan.FromSeconds(5)); resetEventSlim.Reset(); - + shouldThrow = false; - + cut.Enqueue(new RaygunMessage()); resetEventSlim.Wait(TimeSpan.FromSeconds(5)); - + Assert.That(secondMessageWasProcessed, Is.True); } @@ -115,8 +121,8 @@ public void ThrottledBackgroundMessageProcessor_CancellationRequested_IsCaughtAn var shouldThrow = true; var secondMessageWasProcessed = false; var resetEventSlim = new ManualResetEventSlim(); - - var cut = new ThrottledBackgroundMessageProcessor(1, 1, async (m, t) => + + var cut = new ThrottledBackgroundMessageProcessor(1, 1, (m, t) => { if (shouldThrow) { @@ -126,6 +132,8 @@ public void ThrottledBackgroundMessageProcessor_CancellationRequested_IsCaughtAn secondMessageWasProcessed = true; resetEventSlim.Set(); + + return Task.CompletedTask; }); cut.Enqueue(new RaygunMessage()); @@ -134,12 +142,12 @@ public void ThrottledBackgroundMessageProcessor_CancellationRequested_IsCaughtAn resetEventSlim.Reset(); shouldThrow = false; - + cut.Enqueue(new RaygunMessage()); resetEventSlim.Wait(TimeSpan.FromSeconds(5)); - + Assert.That(secondMessageWasProcessed, Is.True); } } -} +} \ No newline at end of file diff --git a/Raygun.CrashReporting.sln b/Raygun.CrashReporting.sln index 63e22807c..0bcf46972 100644 --- a/Raygun.CrashReporting.sln +++ b/Raygun.CrashReporting.sln @@ -48,8 +48,12 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "..Items", "..Items", "{1F1C CHANGE-LOG.md = CHANGE-LOG.md Directory.Build.props = Directory.Build.props LICENSE = LICENSE + pack-all.bat = pack-all.bat + pack-all.ps1 = pack-all.ps1 EndProjectSection EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Raygun4Net.AspNetCore.Tests", "Raygun4Net.AspNetCore.Tests\Raygun4Net.AspNetCore.Tests.csproj", "{D932B9F5-9F08-495B-AAC7-16CE44C74CA4}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -74,6 +78,7 @@ Global {80C7BB59-C753-4E64-9C8D-79B8D57C8215} = {CA6237D9-BFC9-41DA-B2B7-EDC3681B8E7C} {392DE005-7FFA-429B-A196-C2DED6A1AF2D} = {CA6237D9-BFC9-41DA-B2B7-EDC3681B8E7C} {BC560F66-B06E-4C85-AC05-B889165AA818} = {CA6237D9-BFC9-41DA-B2B7-EDC3681B8E7C} + {D932B9F5-9F08-495B-AAC7-16CE44C74CA4} = {3A2DEED0-6A19-4D8B-BA49-2FC229533C8B} EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {BA3C3D49-0104-4958-9326-E964B7867200}.Debug|Any CPU.ActiveCfg = Debug|Any CPU @@ -140,5 +145,9 @@ Global {BC560F66-B06E-4C85-AC05-B889165AA818}.Debug|Any CPU.Build.0 = Debug|Any CPU {BC560F66-B06E-4C85-AC05-B889165AA818}.Release|Any CPU.ActiveCfg = Release|Any CPU {BC560F66-B06E-4C85-AC05-B889165AA818}.Release|Any CPU.Build.0 = Release|Any CPU + {D932B9F5-9F08-495B-AAC7-16CE44C74CA4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D932B9F5-9F08-495B-AAC7-16CE44C74CA4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D932B9F5-9F08-495B-AAC7-16CE44C74CA4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D932B9F5-9F08-495B-AAC7-16CE44C74CA4}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal diff --git a/Raygun4Net.AspNetCore.Tests/Raygun4Net.AspNetCore.Tests.csproj b/Raygun4Net.AspNetCore.Tests/Raygun4Net.AspNetCore.Tests.csproj new file mode 100644 index 000000000..251bb6e21 --- /dev/null +++ b/Raygun4Net.AspNetCore.Tests/Raygun4Net.AspNetCore.Tests.csproj @@ -0,0 +1,43 @@ + + + + net6.0 + enable + enable + + false + + Mindscape.Raygun4Net.AspNetCore.Tests + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + diff --git a/Raygun4Net.AspNetCore.Tests/RaygunMiddlewareTests.cs b/Raygun4Net.AspNetCore.Tests/RaygunMiddlewareTests.cs new file mode 100644 index 000000000..1cf23783b --- /dev/null +++ b/Raygun4Net.AspNetCore.Tests/RaygunMiddlewareTests.cs @@ -0,0 +1,95 @@ +using System.Diagnostics; +using System.Net; +using FluentAssertions; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using MockHttp; + +namespace Mindscape.Raygun4Net.AspNetCore.Tests; + +[TestFixture] +public class RaygunMiddlewareTests +{ + private HttpClient _httpClient = null!; + private MockHttpHandler _mockHttp = null!; + + [SetUp] + public void Init() + { + _mockHttp = new MockHttpHandler(); + } + + [TearDown] + public void UnInit() + { + _httpClient.Dispose(); + _mockHttp.Dispose(); + } + + [OneTimeSetUp] + public void StartTest() + { + Trace.Listeners.Add(new ConsoleTraceListener()); + } + + [OneTimeTearDown] + public void EndTest() + { + Trace.Flush(); + } + + [Test] + public async Task WhenExceptionIsThrown_ShouldBeCapturedByMiddleware_EntrySentToRaygun() + { + _mockHttp.When(match => match.Method(HttpMethod.Post).RequestUri("https://api.raygun.com/entries")) + .Respond(x => + { + x.Body("OK"); + x.StatusCode(HttpStatusCode.Accepted); + }).Verifiable(); + + _httpClient = new HttpClient(_mockHttp); + + var builder = new HostBuilder().ConfigureWebHost(webBuilder => + { + webBuilder.UseTestServer() + .ConfigureServices((_, services) => + { + services.AddRouting(); + services.AddSingleton(s => new RaygunClient(s.GetService(), _httpClient)); + services.AddRaygun(configure: settings => + { + settings.ApiKey = "banana"; + }); + }) + .Configure(app => + { + app.UseRouting(); + app.UseMiddleware(); + app.UseEndpoints(endpoints => + { + endpoints.MapGet("/test-exception", new Func(() => throw new Exception("Banana's are indeed yellow"))); + }); + }); + }); + + using var host = await builder.StartAsync(); + + var client = host.GetTestClient(); + + Func act = async () => await client.GetAsync("/test-exception"); + + await act.Should().ThrowAsync().WithMessage("Banana's are indeed yellow"); + + var token = new CancellationTokenSource(TimeSpan.FromSeconds(1)).Token; + + while (!token.IsCancellationRequested && _mockHttp.InvokedRequests.Count == 0) + { + } + + _mockHttp.InvokedRequests.Should().HaveCount(1); + } +} \ No newline at end of file diff --git a/Raygun4Net.AspNetCore.Tests/Usings.cs b/Raygun4Net.AspNetCore.Tests/Usings.cs new file mode 100644 index 000000000..cefced496 --- /dev/null +++ b/Raygun4Net.AspNetCore.Tests/Usings.cs @@ -0,0 +1 @@ +global using NUnit.Framework; \ No newline at end of file