Skip to content

Commit 684b0bc

Browse files
authored
Automatically inject 'sentry-trace' on outgoing requests in ASP.NET Core (#784)
1 parent 393cc9b commit 684b0bc

File tree

13 files changed

+230
-80
lines changed

13 files changed

+230
-80
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
## vNext
44

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

78
## 3.0.1
89

src/Sentry.AspNetCore/Extensions/DependencyInjection/ServiceCollectionExtensions.cs

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@
44
using Sentry.Extensibility;
55
using Sentry.Extensions.Logging.Extensions.DependencyInjection;
66

7+
#if !NETSTANDARD
8+
using Microsoft.Extensions.Http;
9+
#endif
10+
711
// ReSharper disable once CheckNamespace -- Discoverability
812
namespace Microsoft.Extensions.DependencyInjection
913
{
@@ -20,15 +24,20 @@ internal static class ServiceCollectionExtensions
2024
/// <returns></returns>
2125
public static ISentryBuilder AddSentry(this IServiceCollection services)
2226
{
23-
_ = services.AddSingleton<ISentryEventProcessor, AspNetCoreEventProcessor>();
27+
services.AddSingleton<ISentryEventProcessor, AspNetCoreEventProcessor>();
2428
services.TryAddSingleton<IUserFactory, DefaultUserFactory>();
2529

26-
_ = services
30+
services
2731
.AddSingleton<IRequestPayloadExtractor, FormRequestPayloadExtractor>()
2832
// Last
2933
.AddSingleton<IRequestPayloadExtractor, DefaultRequestPayloadExtractor>();
3034

31-
_ = services.AddSentry<SentryAspNetCoreOptions>();
35+
services.AddSentry<SentryAspNetCoreOptions>();
36+
37+
#if !NETSTANDARD2_0
38+
// Custom handler for HttpClientFactory
39+
services.AddScoped<IHttpMessageHandlerBuilderFilter, SentryHttpMessageHandlerBuilderFilter>();
40+
#endif
3241

3342
return new SentryAspNetCoreBuilder(services);
3443
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
#if !NETSTANDARD2_0
2+
using System;
3+
using Microsoft.Extensions.Http;
4+
5+
namespace Sentry.AspNetCore
6+
{
7+
// Injects Sentry's HTTP handler into HttpClientFactory
8+
internal class SentryHttpMessageHandlerBuilderFilter : IHttpMessageHandlerBuilderFilter
9+
{
10+
private readonly IHub _hub;
11+
12+
public SentryHttpMessageHandlerBuilderFilter(IHub hub) => _hub = hub;
13+
14+
public Action<HttpMessageHandlerBuilder> Configure(Action<HttpMessageHandlerBuilder> next) =>
15+
handlerBuilder =>
16+
{
17+
handlerBuilder.AdditionalHandlers.Add(new SentryHttpMessageHandler(_hub));
18+
next(handlerBuilder);
19+
};
20+
}
21+
}
22+
#endif

src/Sentry/Internal/SdkComposer.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,12 @@ public SdkComposer(SentryOptions options)
1717

1818
private ITransport CreateTransport()
1919
{
20+
// Override for tests
21+
if (_options.Transport is not null)
22+
{
23+
return _options.Transport;
24+
}
25+
2026
if (_options.SentryHttpClientFactory is { })
2127
{
2228
_options.DiagnosticLogger?.LogDebug(

src/Sentry/SentryHttpMessageHandler.cs

Lines changed: 13 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,42 +2,39 @@
22
using System.Threading;
33
using System.Threading.Tasks;
44
using Sentry.Extensibility;
5-
using Sentry.Protocol;
65

76
namespace Sentry
87
{
98
/// <summary>
109
/// Special HTTP message handler that can be used to propagate Sentry headers and other contextual information.
1110
/// </summary>
12-
public class SentryHttpMessageHandler : HttpMessageHandler
11+
public class SentryHttpMessageHandler : DelegatingHandler
1312
{
14-
private readonly HttpMessageInvoker _httpMessageInvoker;
1513
private readonly IHub _hub;
1614

17-
private SentryHttpMessageHandler(HttpMessageInvoker httpMessageInvoker, IHub hub)
15+
/// <summary>
16+
/// Initializes an instance of <see cref="SentryHttpMessageHandler"/>.
17+
/// </summary>
18+
public SentryHttpMessageHandler(IHub hub)
1819
{
19-
_httpMessageInvoker = httpMessageInvoker;
2020
_hub = hub;
2121
}
2222

2323
/// <summary>
2424
/// Initializes an instance of <see cref="SentryHttpMessageHandler"/>.
2525
/// </summary>
2626
public SentryHttpMessageHandler(HttpMessageHandler innerHandler, IHub hub)
27-
: this(new HttpMessageInvoker(innerHandler, false), hub) {}
27+
: this(hub)
28+
{
29+
InnerHandler = innerHandler;
30+
}
2831

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

35-
/// <summary>
36-
/// Initializes an instance of <see cref="SentryHttpMessageHandler"/>.
37-
/// </summary>
38-
public SentryHttpMessageHandler(IHub hub)
39-
: this(new HttpMessageInvoker(new HttpClientHandler(), true), hub) {}
40-
4138
/// <summary>
4239
/// Initializes an instance of <see cref="SentryHttpMessageHandler"/>.
4340
/// </summary>
@@ -59,14 +56,11 @@ protected override Task<HttpResponseMessage> SendAsync(
5956
);
6057
}
6158

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

65-
/// <inheritdoc />
66-
protected override void Dispose(bool disposing)
67-
{
68-
_httpMessageInvoker.Dispose();
69-
base.Dispose(disposing);
63+
return base.SendAsync(request, cancellationToken);
7064
}
7165
}
7266
}

src/Sentry/SentryOptions.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ public class SentryOptions
2323
{
2424
private Dictionary<string, string>? _defaultTags;
2525

26+
// Override for tests
27+
internal ITransport? Transport { get; set; }
28+
2629
internal ISentryStackTraceFactory? SentryStackTraceFactory { get; set; }
2730

2831
internal string ClientVersion { get; } = SdkName;
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
#if NET5_0 || NETCOREAPP3_1
2+
using System;
3+
using System.Collections.Generic;
4+
using System.Linq;
5+
using System.Net.Http;
6+
using System.Threading;
7+
using System.Threading.Tasks;
8+
using FluentAssertions;
9+
using Microsoft.AspNetCore.Builder;
10+
using Microsoft.AspNetCore.Hosting;
11+
using Microsoft.AspNetCore.TestHost;
12+
using Microsoft.Extensions.DependencyInjection;
13+
using Microsoft.Extensions.DependencyInjection.Extensions;
14+
using Microsoft.Extensions.Http;
15+
using Sentry.Testing;
16+
using Xunit;
17+
18+
namespace Sentry.AspNetCore.Tests
19+
{
20+
public class SentryHttpMessageHandlerBuilderFilterTests
21+
{
22+
// Inserts a recorder into pipeline
23+
private class RecordingHandlerBuilderFilter : IHttpMessageHandlerBuilderFilter
24+
{
25+
private readonly RecordingHttpMessageHandler _handler;
26+
27+
public RecordingHandlerBuilderFilter(RecordingHttpMessageHandler handler) => _handler = handler;
28+
29+
public Action<HttpMessageHandlerBuilder> Configure(Action<HttpMessageHandlerBuilder> next) =>
30+
handlerBuilder =>
31+
{
32+
handlerBuilder.AdditionalHandlers.Add(_handler);
33+
next(handlerBuilder);
34+
};
35+
}
36+
37+
[Fact]
38+
public async Task Generated_client_sends_Sentry_trace_header_automatically()
39+
{
40+
// Arrange
41+
42+
// Will use this to record outgoing requests
43+
using var recorder = new RecordingHttpMessageHandler();
44+
45+
var hub = new Internal.Hub(new SentryOptions
46+
{
47+
Dsn = DsnSamples.ValidDsnWithoutSecret
48+
});
49+
50+
var server = new TestServer(new WebHostBuilder()
51+
.UseSentry()
52+
.ConfigureServices(services =>
53+
{
54+
services.AddRouting();
55+
services.AddHttpClient();
56+
57+
services.AddSingleton<IHttpMessageHandlerBuilderFilter>(new RecordingHandlerBuilderFilter(recorder));
58+
59+
services.RemoveAll(typeof(Func<IHub>));
60+
services.AddSingleton<Func<IHub>>(() => hub);
61+
})
62+
.Configure(app =>
63+
{
64+
app.UseRouting();
65+
app.UseSentryTracing();
66+
67+
app.UseEndpoints(routes =>
68+
{
69+
routes.Map("/trigger", async ctx =>
70+
{
71+
using var httpClient = ctx.RequestServices
72+
.GetRequiredService<IHttpClientFactory>()
73+
.CreateClient();
74+
75+
await httpClient.GetAsync("https://example.com");
76+
});
77+
});
78+
})
79+
);
80+
81+
var client = server.CreateClient();
82+
83+
// Act
84+
await client.GetStringAsync("/trigger");
85+
86+
var request = recorder.GetRequests().Single();
87+
88+
// Assert
89+
request.Headers.Should().Contain(header => header.Key == "sentry-trace");
90+
}
91+
}
92+
}
93+
#endif

test/Sentry.Testing/FakeHttpClientHandler.cs

Lines changed: 0 additions & 51 deletions
This file was deleted.
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
using System;
2+
using System.Net;
3+
using System.Net.Http;
4+
using System.Threading;
5+
using System.Threading.Tasks;
6+
7+
namespace Sentry.Testing
8+
{
9+
public class FakeHttpMessageHandler : HttpMessageHandler
10+
{
11+
private readonly Func<HttpRequestMessage, HttpResponseMessage> _getResponse;
12+
13+
public FakeHttpMessageHandler(Func<HttpRequestMessage, HttpResponseMessage> getResponse) =>
14+
_getResponse = getResponse;
15+
16+
public FakeHttpMessageHandler(Func<HttpResponseMessage> getResponse)
17+
: this(_ => getResponse()) {}
18+
19+
public FakeHttpMessageHandler() {}
20+
21+
protected override Task<HttpResponseMessage> SendAsync(
22+
HttpRequestMessage request,
23+
CancellationToken cancellationToken)
24+
{
25+
return Task.FromResult(
26+
_getResponse is not null
27+
? _getResponse(request)
28+
: new HttpResponseMessage(HttpStatusCode.OK)
29+
);
30+
}
31+
}
32+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
using System.Collections.Generic;
2+
using System.Net.Http;
3+
using System.Threading;
4+
using System.Threading.Tasks;
5+
using Sentry.Internal.Extensions;
6+
7+
namespace Sentry.Testing
8+
{
9+
public class RecordingHttpMessageHandler : DelegatingHandler
10+
{
11+
private readonly List<HttpRequestMessage> _requests = new();
12+
13+
public RecordingHttpMessageHandler() {}
14+
15+
public RecordingHttpMessageHandler(HttpMessageHandler innerHandler) =>
16+
InnerHandler = innerHandler;
17+
18+
protected override async Task<HttpResponseMessage> SendAsync(
19+
HttpRequestMessage request,
20+
CancellationToken cancellationToken)
21+
{
22+
// Clone the request to avoid ObjectDisposedException
23+
_requests.Add(await request.CloneAsync());
24+
25+
InnerHandler ??= new HttpClientHandler();
26+
27+
return await base.SendAsync(request, cancellationToken);
28+
}
29+
30+
public IReadOnlyList<HttpRequestMessage> GetRequests() => _requests.ToArray();
31+
32+
protected override void Dispose(bool disposing)
33+
{
34+
_requests.DisposeAll();
35+
base.Dispose(disposing);
36+
}
37+
}
38+
}

0 commit comments

Comments
 (0)