Repository navigation
HTTP Tracking Setup
HTTP tracking is the core mechanism that captures all HTTP traffic for diagram generation. There are two sides to track:
- Outgoing calls from the SUT — Calls your API makes to its downstream dependencies
- Incoming calls to the SUT — Calls your test makes to the API under test
Deprecation (v2.28.1):
CallingServiceNamehas been renamed toCallerNameacross all options classes. The old property still works but produces a compile warning. Update your code to useCallerName— the old name will be removed in a future major version.
This page covers the basic setup using
TrackDependenciesForDiagrams()and the options reference. If your project uses named clients, typed clients, existing DelegatingHandler chains, or other advancedHttpClientpatterns, see Tracking Dependencies for a step-by-step guide covering every common pattern.
Inside your WebApplicationFactory configuration, register tracking for outgoing calls from the SUT:
Simplified imports (v2.0.131+):
TrackDependenciesForDiagrams()andCreateTestTrackingClient()are now available directly from the coreKronikolnamespace. You can useusing Kronikol;instead of a framework-specific namespace — the core extensions accept the baseTestTrackingMessageHandlerOptionstype, so any framework-specific options class works.
builder.ConfigureServices(services =>
{
services.TrackDependenciesForDiagrams(
new XUnitTestTrackingMessageHandlerOptions
{
CallerName = "My API",
PortsToServiceNames =
{
{ 80, "My API" },
{ 5001, "Auth Service" },
{ 5002, "Payment Service" }
}
});
});This registers:
-
TestTrackingMessageHandlerOptionsas a singleton -
IHttpContextAccessorfor extracting tracking headers from incoming requests -
IHttpMessageHandlerBuilderFilterasTrackingHttpMessageHandlerBuilderFilter(automatically injects the trackingDelegatingHandlerinto everyHttpClientbuilt byIHttpClientFactory)
| Property | Type | Default | Description |
|---|---|---|---|
PortsToServiceNames |
Dictionary<int, string> |
{} |
Maps port numbers to human-readable service names for the diagram. When the SUT makes an HTTP call to localhost:5001, the diagram will show the target as the mapped name. The map is looked up by the port alone, whatever the host. A call no map names appears as its host and port (payments.example.com:443), or as localhost:5001 when it goes to this machine (localhost, 127.0.0.1, [::1]); before 5.13.0 every such call appeared as localhost:{port}, whatever its host. |
ClientNamesToServiceNames |
Dictionary<string, string> |
{} |
Maps IHttpClientFactory client names to human-readable service names. Useful when HTTP mocking makes port-based mapping unreliable. Pass the client name via the clientName constructor parameter: new TestTrackingMessageHandler(options, clientName: builder.Name). Resolution order: FixedNameForReceivingService > ClientNamesToServiceNames > PortsToServiceNames > the call's host and port (localhost:{port} for this machine). |
FixedNameForReceivingService |
string? |
null |
If set, all requests handled by this handler are labelled with this fixed name (useful for the test-to-SUT client). |
TestDouble |
string? |
null |
What answers this handler's calls in their service's place, written on each call as testDouble (5.32.0). null reads it from the chain: a handler at its end that answers the call itself, followed past any handler that hands the call on. A name marks every call the handler tracks as that double's answer, whatever the chain ends in, for a double the chain cannot show, such as "WireMock" on a socket. Empty or white space marks no call. See Tracking Dependencies. |
CallerName |
string |
"Caller" |
The name shown in the diagram for the service making the call. |
HeadersToForward |
IEnumerable<string> |
[] |
Additional HTTP headers to forward from incoming requests to outgoing requests (propagated through the call chain). A header the outgoing request already carries is left as it is (4.16.1): a value the caller set wins, and a retry that sends the same message again no longer carries it twice. The four test identity headers need not be listed: the handler puts them on every request it tracks, whatever the phase (4.14.3+). It does not track a request sent synchronously (Send), one to a host in ExcludedHosts or one for which no identity resolves, and such a request carries none of them. On a hop the request carries the Kronikol trace id of the request being served only when the handler holds an IHttpContextAccessor. |
CurrentTestInfoFetcher |
Func<(string Name, string Id)>? |
null |
Delegate that returns the current test's name and unique ID. Each framework-specific options class sets this automatically. |
CurrentStepTypeFetcher |
Func<string?>? |
null |
Delegate that returns the current BDD step type (e.g. "GIVEN", "WHEN"), for a framework that starts no steps: it marks the Setup/Action boundary. It is not asked once a framework's own steps have set the test's phase, and one that throws counts as no step type. Set automatically by the BDDfy, LightBDD and ReqNRoll options classes. |
InternalFlowActivitySources |
string[]? |
null |
Obsolete from 5.34.0, never read (#138). The listener the handler starts hears every source but the runtime's connection-level ones whatever this holds, your own ActivitySources among them. To draw only some sources, set ReportConfigurationOptions.InternalFlowActivitySources with InternalFlowSpanGranularity.Manual; see [[Internal Flow Tracking › Manual Source Filtering |
TrackDuringSetup |
bool |
true |
When false, HTTP requests made during the Setup phase are not tracked. See Phase-Aware Tracking. |
TrackDuringAction |
bool |
true |
When false, HTTP requests made during the Action phase are not tracked. See Phase-Aware Tracking. |
ExcludedHosts |
IReadOnlyCollection<string> |
["override.com"] |
Host names to exclude from tracking. Requests to these hosts are forwarded without logging. Default includes override.com (ASP.NET Core TestServer's internal base address in WebApplicationFactory scenarios). Set to an empty collection to disable host-based filtering. |
HttpContextAccessor |
IHttpContextAccessor? |
null |
Optional — enables dual-resolution of test identity from HTTP headers. Auto-resolved by CreateTestTrackingClient() (v2.26.3+). See [[Dual-Resolution Test Identity (v2.23.0+) |
ClientNamesToServiceNames uses a tiered resolution strategy (v3.0.23+):
- Exact match — The full client name is looked up as a dictionary key
-
EndsWith with boundary — If the client name (stripped of assembly qualification) ends with a key, and the preceding character is non-alphanumeric (e.g.
+,.,-), the match succeeds -
Contains (assembly-qualified names only) — For Refit v9 and other source generators that produce assembly-qualified client names (containing
,), a case-sensitiveContainscheck is used as a final fallback
The Contains fallback is restricted to assembly-qualified names to prevent false positives on simple names (e.g. key "Client" should not match "MyBetterClient").
For typed HttpClients registered via services.AddHttpClient<TClient>(), IHttpClientFactory uses the full type name as the client name. For example:
// Registration:
services.AddHttpClient<TenantHierarchyHttpClient>(o => o.BaseAddress = ...);
// In IHttpMessageHandlerBuilderFilter.Configure(), builder.Name = "TenantHierarchyHttpClient"
// So the dictionary key must be exactly "TenantHierarchyHttpClient":
ClientNamesToServiceNames =
{
{ "TenantHierarchyHttpClient", "Tenant Hierarchy Service" } // ✅ exact match
// { "TenantHierarchyService", "Tenant Hierarchy Service" } // ❌ won't match
}For named HttpClients registered via services.AddHttpClient("my-client"), the key is the string you passed.
Refit / source-generated clients (v3.0.21+): Refit and other source generators produce complex client names. For Refit v8-style names like Refit.Implementation.Generated+SomeModule+IIntelligenceAiApiClient, ends-with matching works (the + is a valid boundary). For Refit v9 assembly-qualified names like ...IntelligenceAiIIntelligenceAiApiClient, Data.Insights.Api, Version=1.0.0.0, ..., the contains fallback matches:
ClientNamesToServiceNames =
{
{ "IIntelligenceAiApiClient", "Intelligence AI" } // ✅ matches both Refit v8 (EndsWith) and v9 (Contains)
}Diagnostic tip (v2.28.12+): When
DiagnosticMode=true, the diagnostic report includes an Unmatched HTTP Client Names section that lists allclientNamevalues that didn't match anyClientNamesToServiceNameskey (neither exact nor ends-with), along with request counts. This makes misconfigured mappings immediately visible.
Framework-specific options classes (
XUnitTestTrackingMessageHandlerOptions,NUnitTestTrackingMessageHandlerOptions,BDDfyTestTrackingMessageHandlerOptions,LightBddTestTrackingMessageHandlerOptions,ReqNRollTestTrackingMessageHandlerOptions) extendTestTrackingMessageHandlerOptionsand auto-populateCurrentTestInfoFetcherfrom the framework's test context. Each framework package also provides aCurrentTestInfostatic class with aFetcherproperty — useCurrentTestInfo.Fetcherwhen you need to setCurrentTestInfoFetcheron extension options.
⚠️ Startup HTTP calls:XUnitTestTrackingMessageHandlerOptions(andXUnit2TestTrackingMessageHandlerOptions) setCurrentTestInfoFetcherto read from the test context. If your SUT makes HTTP calls during startup — e.g. hosted services fetching configuration, health checks, warm-up requests — the fetcher will execute when no test is active.CurrentTestInfo.FetcherthrowsInvalidOperationExceptionwhen no test is running (since v2.28.22; xUnit v2's and NUnit's since 4.13.1). The tracking handler catches it and sends the startup call untracked, without test headers, unless aTestIdentityScopeorTestIdentityScope.GlobalFallbacknames a test (it goes to that test) orRequestResponseLogger.CaptureBackgroundis on (it is kept as a background call, under theunknownidentity). Tests still pass and diagrams for actual tests are unaffected.If you want explicit control, use the base
TestTrackingMessageHandlerOptionswithCurrentTestInfo.Fetcher:services.TrackDependenciesForDiagrams(new TestTrackingMessageHandlerOptions { CallerName = "My API", PortsToServiceNames = { { 80, "My API" }, { 5001, "Auth Service" } }, CurrentTestInfoFetcher = CurrentTestInfo.Fetcher, });See also: Tracking Dependencies — Gotcha: HTTP Calls During HttpClient Construction for a related pitfall when HTTP calls are made during
HttpClientconstruction (not just app startup).
A send that throws (a refused connection, a broken response body, HttpClient's timeout surfacing as a
TaskCanceledException) is logged as a response whose status is the exception's type behind a bang,
!HttpRequestException, with the message chain in error (3.18.0); the exception is rethrown untouched.
The fingerprint the history reads names it (GET /asyncapi/v1.json !HttpRequestException), the diagram
draws the return with the message in the note on it (5.35.0), kronikol query http <report> s3/i1 prints the message under the status (the address of
the response half, which holds it: s3/i0 is the request), and from 4.3.0 Failures.md prints it under the
calls table and kronikol export sends the call as an ERROR span with it as the message. Before 3.18.0 the
request stood alone with no reason, which hid every first attempt a retry loop swallowed.
FailedSend.Status and FailedSend.Describe are public for any other capturer.
From 5.34.3 every Kronikol tracker answers a call that throws this way (#232): the AWS handlers, the Atlas Data API, Redis, the Google and Azure storage handlers when a response body breaks off as it is read, the brokers, the SQL family and EF Core (a cancelled command among them), Spanner, gRPC, MongoDB, Elasticsearch and TrackingProxy. A tracker whose service gives a status of its own keeps it, with the reason in error: the SQL family and Spanner answer Error, MassTransit draws a Fault call, and Kafka answers its error code (!Local_MsgTimedOut). Before, many of them left the request alone.
Not all dependencies use IHttpClientFactory. For dependencies with their own HTTP internals:
-
Azure Cosmos DB SDK — Uses its own
HttpClientinternally. UseKronikol.Extensions.CosmosDBwhich providesCosmosTrackingMessageHandler. See Integration CosmosDB Extension. -
EF Core / Relational databases — EF Core uses ADO.NET
DbCommandinternally, notHttpClient. UseKronikol.Extensions.EfCore.Relationalwhich providesSqlTrackingInterceptor— aDbCommandInterceptorthat classifies SQL operations and shows them asSelect: /ordersdb/Users,Insert: /ordersdb/Orders, etc. Works with SQL Server, PostgreSQL, MySQL, SQLite, Oracle, and Spanner. See Integration EF Core Relational Extension. -
gRPC services — gRPC clients use
GrpcChannel, notIHttpClientFactory. UseKronikol.Extensions.Grpcwhich providesGrpcTrackingInterceptorfor outgoing calls,GrpcTrackingChannel.Create()for incoming (test → SUT) calls, andAddTrackedGrpcClient<TClient>()for DI-based registration (v2.26.0+). Produces protobuf-aware labels likeSayHello: grpc:///greet.Greeter/SayHelloinstead of raw HTTP/2 binary traffic.AddTrackedGrpcClient<T>()andCreateTestTrackingGrpcClientauto-resolveIHttpContextAccessorfrom DI so test identity flows through for SUT → downstream calls. See Integration Grpc Extension for the full setup guide. -
Non-HTTP interactions (Kafka, Service Bus, etc.) — Use
MessageTracker. See Event & Message Tracking.
To track incoming calls from the test to the SUT, create the test's HttpClient via the CreateTestTrackingClient() extension method:
HttpClient client = factory.CreateTestTrackingClient(
new XUnitTestTrackingMessageHandlerOptions
{
FixedNameForReceivingService = "My API"
});This is a convenience method equivalent to:
var client = factory.CreateDefaultClient(new TestTrackingMessageHandler(options));It wraps the WebApplicationFactory's HttpClient in a TestTrackingMessageHandler so that all calls from the test to the SUT appear in the diagrams.
Parallel tests sharing one factory (5.6.1+). CreateTestTrackingClient() makes its client under a lock of the factory's own, as every framework package's CreateTestTrackingClient() and the gRPC extension's CreateTestTrackingGrpcClient() do, so tests running in parallel can share one factory and make their clients from it at the same moment, as a Reqnroll suite's features do in their [BeforeScenario] hooks. WebApplicationFactory has no lock of its own there: before 5.6.1, the first clients made at once each started a copy of the application, and a client made at the same moment as another could be lost from the factory's list of clients or leave a null in it, so the factory's Dispose threw NullReferenceException. A client you make with the factory's own CreateClient() while tests run is outside that lock: start the factory before the tests run (factory.StartServer() in a run-start hook) and make such clients one at a time.
If your test suite already has custom DelegatingHandlers on the test-to-SUT client (e.g. a handler that waits for background processing to complete after each request), use the overload that accepts additional handlers:
HttpClient client = factory.CreateTestTrackingClient(
new XUnitTestTrackingMessageHandlerOptions
{
FixedNameForReceivingService = "My API"
},
new MyBackgroundWorkHandler(),
new MyRetryHandler());This is equivalent to manually calling CreateDefaultClient with the tracking handler first:
var trackingHandler = new TestTrackingMessageHandler(
new XUnit2TestTrackingMessageHandlerOptions
{
FixedNameForReceivingService = "My API"
});
HttpClient client = factory.CreateDefaultClient(trackingHandler, new MyBackgroundWorkHandler());Ordering: The tracking handler is always placed first (outermost) so it captures the full request/response including any modifications made by subsequent handlers.
For more complex handler chaining scenarios (named clients, typed clients, existing DelegatingHandler chains, IHttpMessageHandlerBuilderFilter, etc.), see Tracking Dependencies which covers 8 common patterns in detail.
When your SUT processes a request inside WebApplicationFactory, the code runs on the server's thread pool — not on the test thread. This means AsyncLocal<T>-based test context (used by xUnit, NUnit, LightBDD, ReqNRoll, etc.) is not available inside the SUT's request pipeline.
This affects all tracking extensions that run inside the pipeline. For example, if your API produces a Kafka message, writes to Cosmos DB, or logs to Redis as part of handling a request, the extension's CurrentTestInfoFetcher delegate will throw an exception (caught internally by TestInfoResolver, so tests still pass — but unless a TestIdentityScope or the global fallback names the test, the operation is not logged at all, or with RequestResponseLogger.CaptureBackground on is kept as a background call under the unknown identity, and won't appear in diagrams).
The standard HTTP tracking (TestTrackingMessageHandler) never had this problem because it propagates test identity via HTTP headers (test-tracking-current-test-name, test-tracking-current-test-id) on the incoming request. But non-HTTP extensions (Kafka, Cosmos DB, Redis, EF Core, Service Bus, etc.) had no way to read those headers — until now.
Starting in v2.23.0, all 22 tracking extensions accept an optional IHttpContextAccessor? httpContextAccessor parameter in their constructors. When provided, the extension uses dual-resolution to determine the current test identity:
-
Try HTTP headers first — If an
HttpContextis available (meaning the code is running inside a request pipeline), readtest-tracking-current-test-nameandtest-tracking-current-test-idfrom the request headers -
Fall back to the delegate — If no
HttpContextis available, or its request carries no tracking headers (e.g. the code is running directly on the test thread), useCurrentTestInfoFetcheras before
This is fully backward-compatible. The httpContextAccessor parameter defaults to null, and all existing code continues to work unchanged.
Every extension options class with a CurrentTestInfoFetcher has an HttpContextAccessor property (the TcpTap, ProxyTap and OTLP taps take neither): most since v2.26.3, MongoDB's since 4.10.0, and Kafka's, Bigtable's, Spanner's, MediatR's and EF Core's (SqlTrackingInterceptorOptions) since 4.11.0. Convenience methods, DI extensions, and CreateTestTrackingClient() auto-resolve IHttpContextAccessor from DI when available — no manual wiring needed for the standard setup paths:
builder.ConfigureTestServices(services =>
{
// TrackDependenciesForDiagrams() registers IHttpContextAccessor automatically
services.TrackDependenciesForDiagrams(new XUnitTestTrackingMessageHandlerOptions { ... });
// ServiceBus — AddServiceBusTestTracking auto-resolves IHttpContextAccessor from DI
services.AddServiceBusTestTracking(new ServiceBusTrackingOptions
{
ServiceName = "ServiceBus",
CallerName = "My API",
CurrentTestInfoFetcher = CurrentTestInfo.Fetcher
});
// gRPC — AddTrackedGrpcClient auto-resolves IHttpContextAccessor from DI
services.AddTrackedGrpcClient<MyGrpcClient>(handler, uri, opts => { ... });
});If you construct trackers manually (not via DI extensions), you can set HttpContextAccessor on the options:
builder.ConfigureTestServices(services =>
{
services.AddHttpContextAccessor();
services.AddSingleton(sp =>
{
var options = new KafkaTrackingOptions
{
ServiceName = "Kafka",
CallerName = "My API",
CurrentTestInfoFetcher = CurrentTestInfo.Fetcher,
HttpContextAccessor = sp.GetRequiredService<IHttpContextAccessor>()
};
return new KafkaTracker(options);
});
});The constructor
httpContextAccessorparameter still works and takes precedence over the options property.KafkaTrackingOptions.HttpContextAccessoris 4.11.0+; before it, pass the accessor to the constructor:new KafkaTracker(options, accessor).
All DI extensions and convenience methods auto-resolve IHttpContextAccessor from the service provider when it's available in DI. This covers:
-
Core:
CreateTestTrackingClient()(both overloads) -
ServiceBus:
AddServiceBusTestTracking() -
gRPC:
AddTrackedGrpcClient<TClient>(),CreateTestTrackingGrpcClient() -
MediatR:
TrackMediatorForDiagrams()(auto-resolved since v2.23.0) -
All other extensions: Set
HttpContextAccessoron the options class — the tracker reads it via?? options.HttpContextAccessor(BigQuery's and Pub/Sub's trackers since 4.18.2)
You need dual-resolution (via auto-resolve or manual HttpContextAccessor) if all of these are true:
- Your extension tracker runs inside the SUT's request pipeline (not on the test thread)
- The SUT is hosted via
WebApplicationFactory(in-process) - You're using
CreateTestTrackingClient()or manually addingTestTrackingMessageHandleron the test-to-SUT client, or (4.13.0+) a gRPC test client fromCreateTestTrackingGrpcClientorGrpcTrackingChannel(each propagates the headers)
If your extension only runs on the test thread (e.g. in [BeforeScenario] / [AfterScenario] hooks, or in step definitions that call the SDK directly without going through the SUT), the delegate alone is sufficient.
A host that registers AddTestTrackingContextPropagation() resolves the scenario without an accessor: its middleware begins a TestIdentityScope from the headers of each request it serves, so a tracker in the pipeline whose fetcher cannot see the test resolves through that scope (attribution Scope, where the accessor gives RequestHeader), and a TestTrackingMessageHandler without an accessor passes that identity on. The accessor adds what the scope cannot: the request's Kronikol trace id, which the HTTP handler, the gRPC interceptor and MessageTracker reuse so kronikol query flow can nest their calls under the request by trace id, and the headers HeadersToForward copies.
Note: If you register trackers manually via
services.AddSingleton(sp => new XxxTracker(options, sp.GetService<IHttpContextAccessor>())), the explicit constructor parameter takes precedence over the options property.
All extensions delegate to TestInfoResolver.Resolve(httpContextAccessor, currentTestInfoFetcher) (or ResolveWithSource, which also says which step answered), which tries, in order:
- The request the host is serving: when
httpContextAccessor?.HttpContext?.Requestcarries bothtest-tracking-current-test-nameandtest-tracking-current-test-id, their values, decoded (HTTP Tracking Setup › Header values). - In a flow detached from the test (
TestIdentityScope.Detach();AddTestTrackingContextPropagation()detaches a host's hosted services), only an explicitTestIdentityScopeor the scenario whose document the flow is working on; otherwise the call is background. - The
CurrentTestInfoFetcherdelegate, with its exceptions caught, unless it answers the unknown id. -
TestIdentityScope.Current: the scope the middlewareAddTestTrackingContextPropagation()installs begins for each request that carries the headers, or one the test began. -
TestIdentityScope.GlobalFallback. - Nothing:
null, so the operation is not logged, unlessRequestResponseLogger.CaptureBackgroundis on, when it is logged as background (Unknown).
All 22 tracking extensions support the optional IHttpContextAccessor parameter as of v2.23.0. v2.26.3+: options classes expose an HttpContextAccessor property (MongoDB's from 4.10.0; Kafka's, Bigtable's, Spanner's, MediatR's and EF Core's (SqlTrackingInterceptorOptions) from 4.11.0), and convenience methods / DI extensions auto-resolve it from DI:
| Category | Extensions | Auto-resolved since |
|---|---|---|
| Core | CreateTestTrackingClient() |
v2.26.2 |
| Messaging | Kafka, ServiceBus, EventHubs, EventBridge, SQS, SNS, MassTransit, PubSub, StorageQueues | v2.26.2 (ServiceBus DI extension); others via options property (Kafka's from 4.11.0) |
| Databases | CosmosDB, MongoDB, DynamoDB, Redis, EF Core (Dapper), BigQuery, Elasticsearch | v2.26.2 via options property (MongoDB's from 4.10.0; Bigtable's, Spanner's and EF Core's from 4.11.0). The BigQuery, Bigtable, Spanner, EF Core, PubSub, Kafka and MongoDB DI registrations use the options' accessor first and the container's when the options carry none (BigQuery's and Pub/Sub's tracker registrations since 4.18.2); the subscriber AddMongoDbTestTracking registers records nothing until a client subscribes it ([[MongoDB |
| Storage | BlobStorage, S3, CloudStorage | v2.26.2 via options property |
| RPC | gRPC | v2.26.1 (AddTrackedGrpcClient / CreateTestTrackingGrpcClient) |
| Proxy-based | DispatchProxy, MediatR | MediatR auto-resolved since v2.23.0; DispatchProxy v2.26.2 via ReplaceWithTrackingProxy overload |
When using JustEat.HttpClientInterception for HTTP mocking, you need a custom IHttpMessageHandlerBuilderFilter to insert TestTrackingMessageHandler before the interception handler:
public class HttpClientInterceptionFilter(
HttpClientInterceptorOptions options,
IServiceProvider serviceProvider,
TestTrackingMessageHandlerOptions? trackingOptions = null) : IHttpMessageHandlerBuilderFilter
{
public Action<HttpMessageHandlerBuilder> Configure(Action<HttpMessageHandlerBuilder> next) =>
builder =>
{
next(builder);
string? serviceName = null;
if (trackingOptions is not null && !string.IsNullOrEmpty(builder.Name))
{
var match = trackingOptions.ClientNamesToServiceNames
.FirstOrDefault(kvp => builder.Name.Contains(kvp.Key, StringComparison.OrdinalIgnoreCase));
serviceName = match.Value;
}
if (serviceName is not null)
{
var httpContextAccessor = serviceProvider.GetRequiredService<IHttpContextAccessor>();
builder.PrimaryHandler = new TestTrackingMessageHandler(
new TestTrackingMessageHandlerOptions
{
CallerName = trackingOptions!.CallerName,
FixedNameForReceivingService = serviceName,
CurrentTestInfoFetcher = trackingOptions.CurrentTestInfoFetcher,
},
httpContextAccessor)
{
InnerHandler = new MockHttpMessageHandler(options)
{
InnerHandler = builder.PrimaryHandler
}
};
}
else
{
builder.AdditionalHandlers.Add(options.CreateHttpMessageHandler());
}
};
}The MockHttpMessageHandler wrapper handles GetResponseAsync properly as a DelegatingHandler:
public class MockHttpMessageHandler(HttpClientInterceptorOptions options) : DelegatingHandler
{
protected override async Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request, CancellationToken cancellationToken)
{
var response = await options.GetResponseAsync(request, cancellationToken);
if (response is not null) return response;
if (options.ThrowOnMissingRegistration)
throw new HttpRequestNotInterceptedException(
$"No HTTP response configured for {request.Method} {request.RequestUri}.", request);
return await base.SendAsync(request, cancellationToken);
}
}Register in ConfigureTestServices:
services.AddSingleton<IHttpMessageHandlerBuilderFilter>(sp =>
new HttpClientInterceptionFilter(interceptorOptions, sp, trackingOptions));Common mistake —
_ =>vssp =>: Using_ =>discards the service provider, leavingIHttpContextAccessornull. Always usesp =>and resolveIHttpContextAccessorfrom it. See Dual-Resolution Test Identity (v2.23.0+).
When combining TestTrackingMessageHandler with test faking (JustEat.HttpClientInterception, WireMock, or custom handlers), the handler chain ordering is critical:
┌─────────────────────────────────────┐
│ HttpClient │
├─────────────────────────────────────┤
│ TestTrackingMessageHandler │ ← Records request + response
│ InnerHandler ─┐ │
│ ▼ │
│ InterceptionHandler │ ← Returns fake response
│ InnerHandler ─┐ │
│ ▼ │
│ PrimaryHandler (HttpClientHandler) │ ← Never reached (intercepted)
└─────────────────────────────────────┘
The tracking handler must be outermost — it wraps the interception handler, not the reverse. This way:
- Request flows through
TestTrackingMessageHandler(recorded) - Then through the interception handler (fake response returned)
- Response flows back through
TestTrackingMessageHandler(recorded)
When implementing IHttpMessageHandlerBuilderFilter, replace PrimaryHandler with the tracking handler chained to the interceptor:
// ❌ Wrong: AdditionalHandlers runs after PrimaryHandler interception
builder.AdditionalHandlers.Add(trackingHandler);
// ✅ Correct: tracking wraps interception as the primary handler
var interceptionHandler = _interceptor.CreateHttpMessageHandler();
interceptionHandler.InnerHandler = builder.PrimaryHandler;
builder.PrimaryHandler = new TestTrackingMessageHandler(options, httpContextAccessor)
{
InnerHandler = interceptionHandler
};TestTrackingMessageHandler needs IHttpContextAccessor for dual-resolution of test identity. This must be resolved from DI at handler creation time, not at registration time:
// ❌ Wrong: HttpContextAccessor resolved at registration — HttpContext will be null
services.AddSingleton<IHttpMessageHandlerBuilderFilter>(_ =>
new MyFilter(new HttpContextAccessor()));
// ✅ Correct: resolved from service provider at filter creation time
services.AddSingleton<IHttpMessageHandlerBuilderFilter>(sp =>
new MyFilter(sp));CreateTestTrackingClient() is the primary entry point for tracking inbound HTTP requests (test → SUT). It:
- Creates an
HttpClienttargeting the in-memory test server - Inserts a
TestTrackingMessageHandleras the outermost handler - Auto-injects
test-tracking-current-test-name,test-tracking-current-test-id,test-tracking-trace-idandtest-tracking-caller-nameheaders into every request it tracks, each only when the request lacks it. The call is logged with thetest-tracking-trace-idthe request carries, such as one you set or a first attempt's when a retry sends the message again (4.16.1; before, it was logged with another) - (v2.26.3+) Auto-resolves
IHttpContextAccessorfrom DI - (5.6.1+) Makes the client under a lock of the factory's own, so tests running in parallel can share the factory (Parallel tests sharing one factory)
These headers are how the SUT's internal tracking (CosmosDB, Redis, Service Bus, etc.) correlates operations back to the originating test.
A test name, test id or caller name that is plain printable ASCII, neither begins nor ends with a space and does not begin with UTF-8'' is sent as it is. Any other value is sent in the RFC 8187 form that Content-Disposition: filename*= uses: UTF-8'' followed by the value's UTF-8 bytes, percent-encoded, so Café order travels as UTF-8''Caf%C3%A9%20order. That covers a name with an accent, an emoji or a line feed, an xUnit v3 theory whose long string argument xUnit shortened with ···, and an MSTest id naming a method with a non-ASCII letter.
Every Kronikol reader decodes the form: the middleware AddTestTrackingContextPropagation() installs, the dual resolution described above, MessageTracker, ProxyTap, and TestTrackingServerBridge.GetCurrentTestInfo(accessor), which is the way to read the identity in your own code. The handler, the gRPC interceptor (4.13.0+), ProxyTap and Playwright's ToHeaders() write it. Until 4.12.1 such a value failed the request over a real socket ("Request headers must contain only ASCII characters"), a line feed failed it in memory too, and ProxyTap and Playwright sent ? for each such character.
When you write the headers yourself, encode the same way:
static string HeaderValue(string value) =>
value.All(c => c is >= ' ' and <= '~') && !value.StartsWith("UTF-8''") && value.Trim(' ') == value
? value
: "UTF-8''" + Uri.EscapeDataString(value);FixedNameForReceivingService sets the target service name in diagrams. It should match the CallerName used in outbound tracking options:
// Inbound: "Caller" → "My Service"
var client = factory.CreateTestTrackingClient(new XUnitTestTrackingMessageHandlerOptions
{
FixedNameForReceivingService = "My Service"
});
// Outbound: "My Service" → "Downstream A"
services.TrackDependenciesForDiagrams(new XUnitTestTrackingMessageHandlerOptions
{
CallerName = "My Service", // Must match FixedNameForReceivingService above
ClientNamesToServiceNames = { { "DownstreamAClient", "Downstream A" } }
});One client per collection: Create one tracked client per test collection (not per test). The client is thread-safe and Kronikol headers are injected per-request based on the current test context.
Home · 🔍 Search this wiki Full-text search across every page. The “Pages” list above matches page and heading names, not page content.
Getting Started
- Quick Start (xUnit)
- Project Templates
- AI Integration Prompt
- Framework Integration Guides
- Migrating to v5
- Migrating to v4
Common Tasks
Integration Guides
- Integration xUnit3
- Integration xUnit2
- Integration NUnit
- Integration MSTest
- Integration TUnit
- Integration BDDfy xUnit3
- Integration LightBDD xUnit2
- Integration LightBDD xUnit3
- Integration LightBDD TUnit
- Integration ReqNRoll xUnit2
- Integration ReqNRoll xUnit3
- Integration ReqNRoll TUnit
- Integration Playwright
Uninstrumentable / polyglot backends
- Integration ProxyTap Extension
- Integration TcpTap Extension
- Integration Otlp Extension
- Exporting to OpenTelemetry
- Ingesting External Captures
- Ingesting from Jest
- Integration Cucumber Messages
- Capture-Time Redaction
Extensions
- Integration AtlasDataApi Extension
- Integration BigQuery Extension
- Integration Bigtable Extension
- Integration BlobStorage Extension
- Integration ClickHouse Extension
- Integration CloudStorage Extension
- Integration CosmosDB Extension
- Integration Dapper Extension
- Integration DynamoDB Extension
- Integration EF Core Relational Extension
- Integration Elasticsearch Extension
- Integration EventBridge Extension
- Integration EventHubs Extension
- Integration Grpc Extension
- Integration Kafka Extension
- Integration MassTransit Extension
- Integration MongoDB Extension
- Integration MySqlConnector Extension
- Integration Npgsql Extension
- Integration Oracle Extension
- Integration PubSub Extension
- Integration Redis Extension
- Integration S3 Extension
- Integration ServiceBus Extension
- Integration SNS Extension
- Integration Spanner Extension
- Integration SqlClient Extension
- Integration Sqlite Extension
- Integration SQS Extension
- Integration StorageQueues Extension
- Integration OpenTelemetry Extension
- Integration DispatchProxy Extension
- Integration MediatR Extension
- Integration PlantUML IKVM
Configuration
- Tracking Dependencies
- Tracking Custom Dependencies
- HTTP Tracking Setup
- Report Configuration
- Diagram Customisation
- Phase-Aware Tracking
- Content Formatting
- PlantUML Server Configuration
Features
- Generated Reports
- Report Themes
- Search and Filter Syntax
- Component Diagrams
- PlantUML Browser Rendering
- Inline SVG Rendering
- Internal Flow Tracking
- Tags and Attributes
- Excluding Requests
- Excluded Headers
- Multi-Host Test Architectures
- Event-Driven Architecture Testing
- Service Bus Tracking Patterns
- Background Thread Correlation
- Parallel-Safe Background Correlation
- Event & Message Tracking
- Assertion Tracking
- Step Tracking
- Tabular Attributes
- Large Response and Diagram Handling
- Querying Reports
- Service Card
- Cross-Run History
- Diagnostics and Debugging
- CI Summary Integration
- CI Artifact Upload
- Merging Parallel Reports
Reference