Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixes: Identifying Users with OpenTelemetry doesn't work #2618

Merged
merged 10 commits into from
Sep 18, 2023
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,16 @@

- Sentry tracing middleware now gets configured automatically ([#2602](https://github.com/getsentry/sentry-dotnet/pull/2602))

### Fixes

- Identifying Users with OpenTelemetry doesn't work ([#2618](https://github.com/getsentry/sentry-dotnet/pull/2618))
jamescrosswell marked this conversation as resolved.
Show resolved Hide resolved

### Dependencies

- Bump CLI from v2.20.6 to v2.20.7 ([#2604](https://github.com/getsentry/sentry-dotnet/pull/2604))
- [changelog](https://github.com/getsentry/sentry-cli/blob/master/CHANGELOG.md#2207)
- [diff](https://github.com/getsentry/sentry-cli/compare/2.20.6...2.20.7)

## 3.39.1

### Fixes
Expand Down
2 changes: 2 additions & 0 deletions Sentry.sln.DotSettings
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:Boolean x:Key="/Default/UserDictionary/Words/=Enricher/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=enrichers/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=instrumenter/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
using System.Security.Claims;
using System.Text.Encodings.Web;
using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.Options;

namespace Sentry.Samples.OpenTelemetry.AspNetCore;

public class FakeAuthHandler : AuthenticationHandler<AuthenticationSchemeOptions>
{
public const string UserId = "UserId";

public const string AuthenticationScheme = "Test";

public FakeAuthHandler(
IOptionsMonitor<AuthenticationSchemeOptions> options,
ILoggerFactory logger,
UrlEncoder encoder,
ISystemClock clock) : base(options, logger, encoder, clock)
{
}

protected override Task<AuthenticateResult> HandleAuthenticateAsync()
{
var claims = new List<Claim>
{
new(ClaimTypes.Name, "Fake user"),
new(ClaimTypes.NameIdentifier, "fake-user")
};

var identity = new ClaimsIdentity(claims, AuthenticationScheme);
var principal = new ClaimsPrincipal(identity);
var ticket = new AuthenticationTicket(principal, AuthenticationScheme);

var result = AuthenticateResult.Success(ticket);

return Task.FromResult(result);
}
}
15 changes: 15 additions & 0 deletions samples/Sentry.Samples.OpenTelemetry.AspNetCore/Program.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Diagnostics;
using Microsoft.AspNetCore.Authentication;
using OpenTelemetry.Resources;
using OpenTelemetry.Trace;
using Sentry.OpenTelemetry;
Expand All @@ -22,11 +23,18 @@
{
//options.Dsn = "...Your DSN...";
options.Debug = builder.Environment.IsDevelopment();
options.SendDefaultPii = true;
options.TracesSampleRate = 1.0;
options.UseOpenTelemetry(); // <-- Configure Sentry to use OpenTelemetry trace information
});

builder.Services
.AddAuthorization()
.AddAuthentication(FakeAuthHandler.AuthenticationScheme)
.AddScheme<AuthenticationSchemeOptions, FakeAuthHandler>(FakeAuthHandler.AuthenticationScheme, _ => { });

var app = builder.Build();
app.UseAuthorization();

var httpClient = new HttpClient();
app.MapGet("/hello", async context =>
Expand All @@ -48,6 +56,13 @@

app.MapGet("/echo/{name}", (string name) => $"Hi {name}!");

app.MapGet("/private", async context =>
{
var user = context.User;
var result = $"Hello {user.Identity?.Name}";
await context.Response.WriteAsync(result);
}).RequireAuthorization();

app.MapGet("/throw", _ => throw new Exception("test"));

app.Run();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
{
"profiles": {
"http": {
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"launchUrl": "hello",
"applicationUrl": "http://localhost:5092",
"applicationUrl": "https://localhost:5092",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
Expand Down
17 changes: 16 additions & 1 deletion src/Sentry.AspNetCore/DefaultUserFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,23 @@

namespace Sentry.AspNetCore;

internal class DefaultUserFactory : IUserFactory
#pragma warning disable CS0618
internal class DefaultUserFactory : IUserFactory, ISentryUserFactory
#pragma warning restore CS0618
{
private readonly IHttpContextAccessor? _httpContextAccessor;

public DefaultUserFactory()
{
}

public DefaultUserFactory(IHttpContextAccessor httpContextAccessor)
{
_httpContextAccessor = httpContextAccessor;
}

public User? Create() => _httpContextAccessor?.HttpContext is {} httpContext ? Create(httpContext) : null;

public User? Create(HttpContext context)
{
var principal = context.User;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Microsoft.Extensions.DependencyInjection.Extensions;
using Sentry;
using Sentry.AspNetCore;
using Sentry.Extensibility;
using Sentry.Extensions.Logging.Extensions.DependencyInjection;
Expand All @@ -20,7 +21,12 @@ public static ISentryBuilder AddSentry(this IServiceCollection services)
{
services.AddSingleton<ISentryEventProcessor, AspNetCoreEventProcessor>();
services.AddSingleton<ISentryEventExceptionProcessor, AspNetCoreExceptionProcessor>();

services.AddHttpContextAccessor();
#pragma warning disable CS0618
services.TryAddSingleton<IUserFactory, DefaultUserFactory>();
#pragma warning restore CS0618
services.TryAddSingleton<ISentryUserFactory, DefaultUserFactory>();

services
.AddSingleton<IRequestPayloadExtractor, FormRequestPayloadExtractor>()
Expand Down
1 change: 1 addition & 0 deletions src/Sentry.AspNetCore/IUserFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ namespace Sentry.AspNetCore;
/// <summary>
/// Sentry User Factory
/// </summary>
[Obsolete("This interface is tightly coupled to AspNetCore and will be removed in version 4.0.0. Please consider using ISentryUserFactory with IHttpContextAccessor instead.")]
public interface IUserFactory
{
/// <summary>
Expand Down
2 changes: 2 additions & 0 deletions src/Sentry.AspNetCore/ScopeExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,10 @@ public static void Populate(this Scope scope, HttpContext context, SentryAspNetC

if (options.SendDefaultPii && !scope.HasUser())
{
#pragma warning disable CS0618
var userFactory = context.RequestServices.GetService<IUserFactory>();
var user = userFactory?.Create(context);
#pragma warning restore CS0618

if (user != null)
{
Expand Down
22 changes: 22 additions & 0 deletions src/Sentry.OpenTelemetry/AspNetCoreEnricher.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
namespace Sentry.OpenTelemetry;

internal class AspNetCoreEnricher : IOpenTelemetryEnricher
{
private readonly ISentryUserFactory _userFactory;

internal AspNetCoreEnricher(ISentryUserFactory userFactory) => _userFactory = userFactory;

public void Enrich(ISpan span, Activity activity, IHub hub, SentryOptions? options)
{
if (options?.SendDefaultPii is true)
{
hub.ConfigureScope(scope =>
{
if (!scope.HasUser() && _userFactory.Create() is {} user)
{
scope.User = user;
}
});
}
}
}
6 changes: 6 additions & 0 deletions src/Sentry.OpenTelemetry/IOpenTelemetryEnricher.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace Sentry.OpenTelemetry;

internal interface IOpenTelemetryEnricher
{
void Enrich(ISpan span, Activity activity, IHub hub, SentryOptions? options);
}
13 changes: 12 additions & 1 deletion src/Sentry.OpenTelemetry/SentrySpanProcessor.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using OpenTelemetry;
using OpenTelemetry.Trace;
using Sentry.Extensibility;
using Sentry.Internal;
using Sentry.Internal.Extensions;

namespace Sentry.OpenTelemetry;
Expand All @@ -11,6 +12,7 @@ namespace Sentry.OpenTelemetry;
public class SentrySpanProcessor : BaseProcessor<Activity>
{
private readonly IHub _hub;
private readonly IEnumerable<IOpenTelemetryEnricher> _enrichers;

// ReSharper disable once MemberCanBePrivate.Global - Used by tests
internal readonly ConcurrentDictionary<ActivitySpanId, ISpan> _map = new();
Expand All @@ -27,9 +29,14 @@ public SentrySpanProcessor() : this(SentrySdk.CurrentHub)
/// <summary>
/// Constructs a <see cref="SentrySpanProcessor"/>.
/// </summary>
public SentrySpanProcessor(IHub hub)
public SentrySpanProcessor(IHub hub) : this(hub, null)
{
}

internal SentrySpanProcessor(IHub hub, IEnumerable<IOpenTelemetryEnricher>? enrichers)
{
_hub = hub;
_enrichers = enrichers ?? Enumerable.Empty<IOpenTelemetryEnricher>();
_options = hub.GetSentryOptions();

if (_options is not { })
Expand Down Expand Up @@ -165,6 +172,10 @@ public override void OnEnd(Activity data)
GenerateSentryErrorsFromOtelSpan(data, attributes);

var status = GetSpanStatus(data.Status, attributes);
foreach (var enricher in _enrichers)
{
enricher.Enrich(span, data, _hub, _options);
}
span.Finish(status);

_map.TryRemove(data.SpanId, out _);
Expand Down
15 changes: 14 additions & 1 deletion src/Sentry.OpenTelemetry/TracerProviderBuilderExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using Microsoft.Extensions.DependencyInjection;
using OpenTelemetry;
using OpenTelemetry.Context.Propagation;
using OpenTelemetry.Trace;
Expand Down Expand Up @@ -29,6 +30,18 @@ public static TracerProviderBuilder AddSentry(this TracerProviderBuilder tracerP
{
defaultTextMapPropagator ??= new SentryPropagator();
Sdk.SetDefaultTextMapPropagator(defaultTextMapPropagator);
return tracerProviderBuilder.AddProcessor<SentrySpanProcessor>();
return tracerProviderBuilder.AddProcessor(services =>
{
List<IOpenTelemetryEnricher> enrichers = new();

// AspNetCoreEnricher
var userFactory = services.GetService<ISentryUserFactory>();
if (userFactory is not null)
{
enrichers.Add(new AspNetCoreEnricher(userFactory));
}

return new SentrySpanProcessor(SentrySdk.CurrentHub, enrichers);
});
}
}
6 changes: 6 additions & 0 deletions src/Sentry/ISentryUserFactory.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace Sentry;

internal interface ISentryUserFactory
Copy link

@jeremy-allocate jeremy-allocate Oct 11, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jamescrosswell I'm hitting the IUserFactory warning in our builds but when I went to do the cutover to ISentryUserFactory to silence them I discovered this interface is marked internal so I can't implement it in my projects. Was this unintentional? If intentional, how are we supposed to proceed here?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @jeremy-allocate , see #2618 (comment) above.

If it's an emergency and you just need to get a build out, you can disable the warning with a pragma, as we've done here:

#pragma warning disable CS0618
services.TryAddSingleton<IUserFactory, DefaultUserFactory>();
#pragma warning restore CS0618

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All good, thanks @jamescrosswell. We'll live with the warnings until #2714 is released.

{
public User? Create();
}
1 change: 0 additions & 1 deletion src/Sentry/Sentry.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,6 @@
<InternalsVisibleTo Include="Sentry.NLog" PublicKey="$(SentryPublicKey)" />
<InternalsVisibleTo Include="Sentry.NLog.Tests" PublicKey="$(SentryPublicKey)" />
<InternalsVisibleTo Include="Sentry.OpenTelemetry" PublicKey="$(SentryPublicKey)" />
<InternalsVisibleTo Include="Sentry.OpenTelemetry.AspNetCore" PublicKey="$(SentryPublicKey)" />
<InternalsVisibleTo Include="Sentry.OpenTelemetry.Tests" PublicKey="$(SentryPublicKey)" />
<InternalsVisibleTo Include="Sentry.Profiling" PublicKey="$(SentryPublicKey)" />
<InternalsVisibleTo Include="Sentry.Profiling.Tests" PublicKey="$(SentryPublicKey)" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ namespace Sentry.AspNetCore
{
Microsoft.Extensions.DependencyInjection.IServiceCollection Services { get; }
}
[System.Obsolete("This interface is tightly coupled to AspNetCore and will be removed in version 4." +
"0.0. Please consider using ISentryUserFactory with IHttpContextAccessor instead." +
"")]
public interface IUserFactory
{
Sentry.User? Create(Microsoft.AspNetCore.Http.HttpContext context);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ namespace Sentry.AspNetCore
{
Microsoft.Extensions.DependencyInjection.IServiceCollection Services { get; }
}
[System.Obsolete("This interface is tightly coupled to AspNetCore and will be removed in version 4." +
"0.0. Please consider using ISentryUserFactory with IHttpContextAccessor instead." +
"")]
public interface IUserFactory
{
Sentry.User? Create(Microsoft.AspNetCore.Http.HttpContext context);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ namespace Sentry.AspNetCore
{
Microsoft.Extensions.DependencyInjection.IServiceCollection Services { get; }
}
[System.Obsolete("This interface is tightly coupled to AspNetCore and will be removed in version 4." +
"0.0. Please consider using ISentryUserFactory with IHttpContextAccessor instead." +
"")]
public interface IUserFactory
{
Sentry.User? Create(Microsoft.AspNetCore.Http.HttpContext context);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ namespace Sentry.AspNetCore
{
Microsoft.Extensions.DependencyInjection.IServiceCollection Services { get; }
}
[System.Obsolete("This interface is tightly coupled to AspNetCore and will be removed in version 4." +
"0.0. Please consider using ISentryUserFactory with IHttpContextAccessor instead." +
"")]
public interface IUserFactory
{
Sentry.User? Create(Microsoft.AspNetCore.Http.HttpContext context);
Expand Down
10 changes: 10 additions & 0 deletions test/Sentry.AspNetCore.Tests/DefaultUserFactoryTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,16 @@ public void Create_NoClaims_IpAddress()
Assert.Equal(IPAddress.IPv6Loopback.ToString(), actual.IpAddress);
}

[Fact]
public void Create_ContextAccessorNoClaims_IpAddress()
{
_ = HttpContext.User.Claims.Returns(Enumerable.Empty<Claim>());
var contextAccessor = Substitute.For<IHttpContextAccessor>();
contextAccessor.HttpContext.Returns(HttpContext);
var actual = new DefaultUserFactory(contextAccessor).Create();
Assert.Equal(IPAddress.IPv6Loopback.ToString(), actual?.IpAddress);
}

[Fact]
public void Create_ClaimNameAndIdentityDontMatch_UsernameFromIdentity()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,11 +68,13 @@ public void AddSentry_DefaultRequestPayloadExtractor_LastRegistration()
Assert.Same(typeof(DefaultRequestPayloadExtractor), last.ImplementationType);
}

#pragma warning disable CS0618
[Fact]
public void AddSentry_DefaultUserFactory_Registered()
{
_ = _sut.AddSentry();
_sut.Received().Add(Arg.Is<ServiceDescriptor>(d => d.ServiceType == typeof(IUserFactory)
&& d.ImplementationType == typeof(DefaultUserFactory)));
}
#pragma warning restore CS0618
}
Loading