Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
## vNext

- Add GetSpan() to IHub and SentrySdk (#782) @Tyrrrz
- Automatically inject 'sentry-trace' on outgoing requests in ASP.NET Core (#784) @Tyrrrz

## 3.0.1

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@
using Sentry.Extensibility;
using Sentry.Extensions.Logging.Extensions.DependencyInjection;

#if !NETSTANDARD
using Microsoft.Extensions.Http;
#endif

// ReSharper disable once CheckNamespace -- Discoverability
namespace Microsoft.Extensions.DependencyInjection
{
Expand All @@ -20,15 +24,20 @@ internal static class ServiceCollectionExtensions
/// <returns></returns>
public static ISentryBuilder AddSentry(this IServiceCollection services)
{
_ = services.AddSingleton<ISentryEventProcessor, AspNetCoreEventProcessor>();
services.AddSingleton<ISentryEventProcessor, AspNetCoreEventProcessor>();
services.TryAddSingleton<IUserFactory, DefaultUserFactory>();

_ = services
services
.AddSingleton<IRequestPayloadExtractor, FormRequestPayloadExtractor>()
// Last
.AddSingleton<IRequestPayloadExtractor, DefaultRequestPayloadExtractor>();

_ = services.AddSentry<SentryAspNetCoreOptions>();
services.AddSentry<SentryAspNetCoreOptions>();

#if !NETSTANDARD2_0
// Custom handler for HttpClientFactory
services.AddScoped<IHttpMessageHandlerBuilderFilter, SentryHttpMessageHandlerBuilderFilter>();
#endif

return new SentryAspNetCoreBuilder(services);
}
Expand Down
22 changes: 22 additions & 0 deletions src/Sentry.AspNetCore/SentryHttpMessageHandlerBuilderFilter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
#if !NETSTANDARD2_0
using System;
using Microsoft.Extensions.Http;

namespace Sentry.AspNetCore
{
// Injects Sentry's HTTP handler into HttpClientFactory
internal class SentryHttpMessageHandlerBuilderFilter : IHttpMessageHandlerBuilderFilter
{
private readonly IHub _hub;

public SentryHttpMessageHandlerBuilderFilter(IHub hub) => _hub = hub;

public Action<HttpMessageHandlerBuilder> Configure(Action<HttpMessageHandlerBuilder> next) =>
handlerBuilder =>
{
handlerBuilder.AdditionalHandlers.Add(new SentryHttpMessageHandler(_hub));
next(handlerBuilder);
};
}
}
#endif
6 changes: 6 additions & 0 deletions src/Sentry/Internal/SdkComposer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@ public SdkComposer(SentryOptions options)

private ITransport CreateTransport()
{
// Override for tests
if (_options.Transport is not null)
{
return _options.Transport;
}

if (_options.SentryHttpClientFactory is { })
{
_options.DiagnosticLogger?.LogDebug(
Expand Down
32 changes: 13 additions & 19 deletions src/Sentry/SentryHttpMessageHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,42 +2,39 @@
using System.Threading;
using System.Threading.Tasks;
using Sentry.Extensibility;
using Sentry.Protocol;

namespace Sentry
{
/// <summary>
/// Special HTTP message handler that can be used to propagate Sentry headers and other contextual information.
/// </summary>
public class SentryHttpMessageHandler : HttpMessageHandler
public class SentryHttpMessageHandler : DelegatingHandler
{
private readonly HttpMessageInvoker _httpMessageInvoker;
private readonly IHub _hub;

private SentryHttpMessageHandler(HttpMessageInvoker httpMessageInvoker, IHub hub)
/// <summary>
/// Initializes an instance of <see cref="SentryHttpMessageHandler"/>.
/// </summary>
public SentryHttpMessageHandler(IHub hub)
{
_httpMessageInvoker = httpMessageInvoker;
_hub = hub;
}

/// <summary>
/// Initializes an instance of <see cref="SentryHttpMessageHandler"/>.
/// </summary>
public SentryHttpMessageHandler(HttpMessageHandler innerHandler, IHub hub)
: this(new HttpMessageInvoker(innerHandler, false), hub) {}
: this(hub)
{
InnerHandler = innerHandler;
}

/// <summary>
/// Initializes an instance of <see cref="SentryHttpMessageHandler"/>.
/// </summary>
public SentryHttpMessageHandler(HttpMessageHandler innerHandler)
: this(innerHandler, HubAdapter.Instance) {}

/// <summary>
/// Initializes an instance of <see cref="SentryHttpMessageHandler"/>.
/// </summary>
public SentryHttpMessageHandler(IHub hub)
: this(new HttpMessageInvoker(new HttpClientHandler(), true), hub) {}

/// <summary>
/// Initializes an instance of <see cref="SentryHttpMessageHandler"/>.
/// </summary>
Expand All @@ -59,14 +56,11 @@ protected override Task<HttpResponseMessage> SendAsync(
);
}

return _httpMessageInvoker.SendAsync(request, cancellationToken);
}
// Prevent null reference exception in the following call
// in case the user didn't set an inner handler.
InnerHandler ??= new HttpClientHandler();

/// <inheritdoc />
protected override void Dispose(bool disposing)
{
_httpMessageInvoker.Dispose();
base.Dispose(disposing);
return base.SendAsync(request, cancellationToken);
}
}
}
3 changes: 3 additions & 0 deletions src/Sentry/SentryOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ public class SentryOptions
{
private Dictionary<string, string>? _defaultTags;

// Override for tests
internal ITransport? Transport { get; set; }

internal ISentryStackTraceFactory? SentryStackTraceFactory { get; set; }

internal string ClientVersion { get; } = SdkName;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
#if NET5_0 || NETCOREAPP3_1
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using FluentAssertions;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.TestHost;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Http;
using Sentry.Testing;
using Xunit;

namespace Sentry.AspNetCore.Tests
{
public class SentryHttpMessageHandlerBuilderFilterTests
{
// Inserts a recorder into pipeline
private class RecordingHandlerBuilderFilter : IHttpMessageHandlerBuilderFilter
{
private readonly RecordingHttpMessageHandler _handler;

public RecordingHandlerBuilderFilter(RecordingHttpMessageHandler handler) => _handler = handler;

public Action<HttpMessageHandlerBuilder> Configure(Action<HttpMessageHandlerBuilder> next) =>
handlerBuilder =>
{
handlerBuilder.AdditionalHandlers.Add(_handler);
next(handlerBuilder);
};
}

[Fact]
public async Task Generated_client_sends_Sentry_trace_header_automatically()
{
// Arrange

// Will use this to record outgoing requests
using var recorder = new RecordingHttpMessageHandler();

var hub = new Internal.Hub(new SentryOptions
{
Dsn = DsnSamples.ValidDsnWithoutSecret
});

var server = new TestServer(new WebHostBuilder()
.UseSentry()
.ConfigureServices(services =>
{
services.AddRouting();
services.AddHttpClient();

services.AddSingleton<IHttpMessageHandlerBuilderFilter>(new RecordingHandlerBuilderFilter(recorder));

services.RemoveAll(typeof(Func<IHub>));
services.AddSingleton<Func<IHub>>(() => hub);
})
.Configure(app =>
{
app.UseRouting();
app.UseSentryTracing();

app.UseEndpoints(routes =>
{
routes.Map("/trigger", async ctx =>
{
using var httpClient = ctx.RequestServices
.GetRequiredService<IHttpClientFactory>()
.CreateClient();

await httpClient.GetAsync("https://example.com");
});
});
})
);

var client = server.CreateClient();

// Act
await client.GetStringAsync("/trigger");

var request = recorder.GetRequests().Single();

// Assert
request.Headers.Should().Contain(header => header.Key == "sentry-trace");
}
}
}
#endif
51 changes: 0 additions & 51 deletions test/Sentry.Testing/FakeHttpClientHandler.cs

This file was deleted.

32 changes: 32 additions & 0 deletions test/Sentry.Testing/FakeHttpMessageHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
using System;
using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;

namespace Sentry.Testing
{
public class FakeHttpMessageHandler : HttpMessageHandler
{
private readonly Func<HttpRequestMessage, HttpResponseMessage> _getResponse;

public FakeHttpMessageHandler(Func<HttpRequestMessage, HttpResponseMessage> getResponse) =>
_getResponse = getResponse;

public FakeHttpMessageHandler(Func<HttpResponseMessage> getResponse)
: this(_ => getResponse()) {}

public FakeHttpMessageHandler() {}

protected override Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request,
CancellationToken cancellationToken)
{
return Task.FromResult(
_getResponse is not null
? _getResponse(request)
: new HttpResponseMessage(HttpStatusCode.OK)
);
}
}
}
38 changes: 38 additions & 0 deletions test/Sentry.Testing/RecordingHttpMessageHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
using System.Collections.Generic;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Sentry.Internal.Extensions;

namespace Sentry.Testing
{
public class RecordingHttpMessageHandler : DelegatingHandler
{
private readonly List<HttpRequestMessage> _requests = new();

public RecordingHttpMessageHandler() {}

public RecordingHttpMessageHandler(HttpMessageHandler innerHandler) =>
InnerHandler = innerHandler;

protected override async Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request,
CancellationToken cancellationToken)
{
// Clone the request to avoid ObjectDisposedException
_requests.Add(await request.CloneAsync());

InnerHandler ??= new HttpClientHandler();

return await base.SendAsync(request, cancellationToken);
}

public IReadOnlyList<HttpRequestMessage> GetRequests() => _requests.ToArray();

protected override void Dispose(bool disposing)
{
_requests.DisposeAll();
base.Dispose(disposing);
}
}
}
Loading