Skip to content

Commit

Permalink
Added IRaygunUserProvider to fetch user data
Browse files Browse the repository at this point in the history
  • Loading branch information
phillip-haydon committed Mar 10, 2024
1 parent dc7f054 commit 6e7beef
Show file tree
Hide file tree
Showing 15 changed files with 258 additions and 283 deletions.
67 changes: 59 additions & 8 deletions Mindscape.Raygun4Net.AspNetCore/ApplicationBuilderExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,47 +1,98 @@
#nullable enable

using System;
using System.Linq;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Logging;

namespace Mindscape.Raygun4Net.AspNetCore;

public static class ApplicationBuilderExtensions
{
private const string NoApiKeyWarning = "Raygun API Key is not set, please set an API Key in the RaygunSettings.";

/// <summary>
/// Checks to see if you have an API Key and registers the Raygun Middleware. If no API Key is found, a warning will be logged.
/// </summary>
public static IApplicationBuilder UseRaygun(this IApplicationBuilder app)
{
var settings = app.ApplicationServices.GetService<RaygunSettings>();

if (settings?.ApiKey == null)
{
var logger = app.ApplicationServices.GetService<ILoggerFactory>()?.CreateLogger<RaygunMiddleware>();

if (logger != null)
{
logger.LogWarning(NoApiKeyWarning);
}
else
{
Console.WriteLine(NoApiKeyWarning);
}
}

return app.UseMiddleware<RaygunMiddleware>();
}

public static IServiceCollection AddRaygun(this IServiceCollection services, IConfiguration configuration, Action<RaygunSettings>? configure = null)
/// <summary>
/// Registers the Raygun Client and Raygun Settings with the DI container. Settings will be fetched from the appsettings.json file,
/// and can be overridden by providing a custom configuration delegate.
/// </summary>
public static IServiceCollection AddRaygun(this IServiceCollection services, IConfiguration configuration, Action<RaygunSettings>? options = null)
{
// Fetch settings from configuration or use default settings
var settings = configuration.GetSection("RaygunSettings").Get<RaygunSettings>() ?? new RaygunSettings();

// Override settings with user-provided settings
configure?.Invoke(settings);
options?.Invoke(settings);

services.TryAddSingleton(settings);
services.TryAddSingleton(s => new RaygunClient(s.GetService<RaygunSettings>()));
services.TryAddSingleton(s => new RaygunClient(s.GetService<RaygunSettings>(), s.GetService<IRaygunUserProvider>()));
services.TryAddSingleton<IRaygunUserProvider, DefaultRaygunUserProvider>();
services.AddHttpContextAccessor();

return services;
}

public static IServiceCollection AddRaygun(this IServiceCollection services, Action<RaygunSettings>? configure = null)
/// <summary>
/// Registers the Raygun Client and Raygun Settings with the DI container. Settings will be defaulted and overridden by providing a custom configuration delegate.
/// </summary>
public static IServiceCollection AddRaygun(this IServiceCollection services, Action<RaygunSettings>? options)
{
// Fetch settings from configuration or use default settings
// Since we are not using IConfiguration, we need to create a new instance of RaygunSettings
var settings = new RaygunSettings();

// Override settings with user-provided settings
configure?.Invoke(settings);
options?.Invoke(settings);

services.TryAddSingleton(settings);
services.TryAddSingleton(s => new RaygunClient(s.GetService<RaygunSettings>()));
services.TryAddSingleton(s => new RaygunClient(s.GetService<RaygunSettings>(), s.GetService<IRaygunUserProvider>()));
services.TryAddSingleton<IRaygunUserProvider, DefaultRaygunUserProvider>();
services.AddHttpContextAccessor();

return services;
}

/// <summary>
/// Registers a custom User Provider with the DI container. This allows you to provide your own implementation of IRaygunUserProvider.
/// </summary>
public static IServiceCollection AddRaygunUserProvider<T>(this IServiceCollection services) where T : class, IRaygunUserProvider
{
// In case the default or any other user provider is already registered, remove it first
var existing = services.FirstOrDefault(x => x.ServiceType == typeof(IRaygunUserProvider));

if (existing != null)
{
services.Remove(existing);
}

// Add the new user provider
services.TryAddSingleton<IRaygunUserProvider, T>();

return services;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
namespace Mindscape.Raygun4Net.AspNetCore.Builders
{
// ReSharper disable once ClassNeverInstantiated.Global
public class RaygunAspNetCoreRequestMessageBuilder
internal class RaygunAspNetCoreRequestMessageBuilder
{
private const int MAX_RAW_DATA_LENGTH = 4096; // bytes

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

namespace Mindscape.Raygun4Net.AspNetCore.Builders
{
public class RaygunAspNetCoreResponseMessageBuilder
internal class RaygunAspNetCoreResponseMessageBuilder
{
public static RaygunResponseMessage Build(HttpContext? context)
{
Expand Down
42 changes: 42 additions & 0 deletions Mindscape.Raygun4Net.AspNetCore/DefaultRaygunUserProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
#nullable enable

using System.Security.Claims;
using Microsoft.AspNetCore.Http;

namespace Mindscape.Raygun4Net.AspNetCore;

public class DefaultRaygunUserProvider : IRaygunUserProvider
{
private readonly IHttpContextAccessor _contextAccessor;

public DefaultRaygunUserProvider(IHttpContextAccessor contextAccessor)
{
_contextAccessor = contextAccessor;
}

public RaygunIdentifierMessage? GetUser()
{
var ctx = _contextAccessor.HttpContext;

if (ctx == null)
{
return null;
}

var identity = ctx.User.Identity as ClaimsIdentity;

if (identity?.IsAuthenticated == true)
{
var email = identity.FindFirst(ClaimTypes.Email)?.Value ?? identity.Name;

return new RaygunIdentifierMessage(email)
{
IsAnonymous = false,
Email = email,
FullName = identity.Name
};
}

return null;
}
}
2 changes: 1 addition & 1 deletion Mindscape.Raygun4Net.AspNetCore/IRaygunHttpSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

namespace Mindscape.Raygun4Net.AspNetCore;

public interface IRaygunHttpSettings
internal interface IRaygunHttpSettings
{
List<string> IgnoreSensitiveFieldNames { get; }
List<string> IgnoreQueryParameterNames { get; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
<PackageReference Include="Microsoft.AspNetCore.Http" Version="2.2.2" />
<PackageReference Include="Microsoft.AspNetCore.Http.Extensions" Version="2.2.0" />
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="1.1.2" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="6.0.0" />
</ItemGroup>

<ItemGroup>
Expand Down
136 changes: 5 additions & 131 deletions Mindscape.Raygun4Net.AspNetCore/RaygunClient.cs
Original file line number Diff line number Diff line change
@@ -1,151 +1,25 @@
using System;
using System.Linq;
using System.Net.Http;
using Mindscape.Raygun4Net.Filters;

namespace Mindscape.Raygun4Net.AspNetCore;

public class RaygunClient : RaygunClientBase
{
public RaygunClient(string apiKey)
: this(new RaygunSettings {ApiKey = apiKey})
[Obsolete("Please use the RaygunClient(RaygunSettings settings) constructor instead.")]
public RaygunClient(string apiKey) : base(new RaygunSettings {ApiKey = apiKey})
{
}

public RaygunClient(RaygunSettings settings, HttpClient httpClient = null)
: base(settings, httpClient)
public RaygunClient(RaygunSettings settings, HttpClient httpClient = null, IRaygunUserProvider userProvider = null) : base(settings, httpClient, userProvider)
{
if (!string.IsNullOrEmpty(settings.ApplicationVersion))
{
ApplicationVersion = settings.ApplicationVersion;
}

IsRawDataIgnored = settings.IsRawDataIgnored;
IsRawDataIgnoredWhenFilteringFailed = settings.IsRawDataIgnoredWhenFilteringFailed;

UseXmlRawDataFilter = settings.UseXmlRawDataFilter;
UseKeyValuePairRawDataFilter = settings.UseKeyValuePairRawDataFilter;
}

internal Lazy<RaygunSettings> Settings => new(() => (RaygunSettings) _settings);

/// <summary>
/// Adds a list of keys to remove from the following sections of the <see cref="RaygunRequestMessage" />
/// <see cref="RaygunRequestMessage.Headers" />
/// <see cref="RaygunRequestMessage.QueryString" />
/// <see cref="RaygunRequestMessage.Cookies" />
/// <see cref="RaygunRequestMessage.Form" />
/// <see cref="RaygunRequestMessage.RawData" />
/// </summary>
/// <param name="names">Keys to be stripped from the <see cref="RaygunRequestMessage" />.</param>
public void IgnoreSensitiveFieldNames(params string[] names)
{
Settings.Value.IgnoreSensitiveFieldNames.AddRange(names);
}

/// <summary>
/// Adds a list of keys to remove from the <see cref="RaygunRequestMessage.QueryString" /> property of the <see cref="RaygunRequestMessage" />
/// </summary>
/// <param name="names">Keys to be stripped from the <see cref="RaygunRequestMessage.QueryString" /></param>
public void IgnoreQueryParameterNames(params string[] names)
{
Settings.Value.IgnoreQueryParameterNames.AddRange(names);
}

/// <summary>
/// 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.
/// </summary>
/// <param name="names">Keys to be stripped from the copy of the Form NameValueCollection when sending to Raygun.</param>
public void IgnoreFormFieldNames(params string[] names)
{
Settings.Value.IgnoreFormFieldNames.AddRange(names);
}

/// <summary>
/// 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.
/// </summary>
/// <param name="names">Keys to be stripped from the copy of the Headers NameValueCollection when sending to Raygun.</param>
public void IgnoreHeaderNames(params string[] names)
{
Settings.Value.IgnoreHeaderNames.AddRange(names);
}

/// <summary>
/// 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.
/// </summary>
/// <param name="names">Keys to be stripped from the copy of the Cookies NameValueCollection when sending to Raygun.</param>
public void IgnoreCookieNames(params string[] names)
public RaygunClient(RaygunSettings settings, IRaygunUserProvider userProvider = null) : base(settings, null, userProvider)
{
Settings.Value.IgnoreCookieNames.AddRange(names);
}

/// <summary>
/// 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.
/// </summary>
/// <param name="names">Keys to be stripped from the copy of the ServerVariables NameValueCollection when sending to Raygun.</param>
public void IgnoreServerVariableNames(params string[] names)
{
Settings.Value.IgnoreServerVariableNames.AddRange(names);
}

/// <summary>
/// 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.
/// </summary>
public bool IsRawDataIgnored
{
get => Settings.Value.IsRawDataIgnored;
set => Settings.Value.IsRawDataIgnored = value;
}

/// <summary>
/// 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.
/// </summary>
public bool IsRawDataIgnoredWhenFilteringFailed
{
get => Settings.Value.IsRawDataIgnoredWhenFilteringFailed;
set => Settings.Value.IsRawDataIgnoredWhenFilteringFailed = value;
}

/// <summary>
/// Specifies whether or not RawData from web requests is filtered of sensitive values using an XML parser.
/// </summary>
/// <value><c>true</c> if use xml raw data filter; otherwise, <c>false</c>.</value>
public bool UseXmlRawDataFilter
{
get => Settings.Value.UseXmlRawDataFilter;
set => Settings.Value.UseXmlRawDataFilter = value;
}

/// <summary>
/// Specifies whether or not RawData from web requests is filtered of sensitive values using an KeyValuePair parser.
/// </summary>
/// <value><c>true</c> if use key pair raw data filter; otherwise, <c>false</c>.</value>
public bool UseKeyValuePairRawDataFilter
{
get => Settings.Value.UseKeyValuePairRawDataFilter;
set => Settings.Value.UseKeyValuePairRawDataFilter = value;
}

/// <summary>
/// Add an <see cref="IRaygunDataFilter"/> 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.
/// </summary>
/// <param name="filter">Custom raw data filter implementation.</param>
public void AddRawDataFilter(IRaygunDataFilter filter)
{
Settings.Value.RawDataFilters.Add(filter);
}
internal Lazy<RaygunSettings> Settings => new(() => (RaygunSettings) _settings);

protected override bool CanSend(RaygunMessage message)
{
Expand Down
8 changes: 8 additions & 0 deletions Mindscape.Raygun4Net.NetCore.Common/IRaygunUserProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
#nullable enable

namespace Mindscape.Raygun4Net;

public interface IRaygunUserProvider
{
public RaygunIdentifierMessage? GetUser();
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,18 @@
{
public class RaygunIdentifierMessage
{
#if DEBUG
// Exists for unit test but want to force users to enter 'user' when creating a new instance.
public RaygunIdentifierMessage()
{
}
#endif

public RaygunIdentifierMessage(string user)
{
Identifier = user;
}

/// <summary>
/// Unique Identifier for this user. Set this to the identifier you use internally to look up users,
/// or a correlation id for anonymous users if you have one. It doesn't have to be unique, but we will
Expand Down Expand Up @@ -39,12 +46,12 @@ public RaygunIdentifierMessage(string user)
/// Device Identifier. Could be used to identify users across apps.
/// </summary>
public string UUID { get; set; }

public override string ToString()
{
// This exists because Reflection in Xamarin can't seem to obtain the Getter methods unless the getter is used somewhere in the code.
// The getter of all properties is required to serialize the Raygun messages to JSON.
return $"[RaygunIdentifierMessage: Identifier={Identifier}, IsAnonymous={IsAnonymous}, Email={Email}, FullName={FullName}, FirstName={FirstName}, UUID={UUID}]";
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
<ItemGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<InternalsVisibleTo Include="Mindscape.Raygun4Net.AspNetCore"/>
<InternalsVisibleTo Include="Mindscape.Raygun4Net.NetCore.Tests"/>
<InternalsVisibleTo Include="System.Text.Json"/>
</ItemGroup>

<ItemGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
Expand Down
Loading

0 comments on commit 6e7beef

Please sign in to comment.