Repository navigation
Internal Flow Tracking
Internal Flow Tracking adds interactive internal flow visualisation to your sequence diagrams. When enabled, clicking an arrow in a PlantUML sequence diagram opens a popup showing the internal class/method activity that occurred inside the SUT between that HTTP boundary and the next — captured via OpenTelemetry spans.
This lets you see not just what your API called, but what happened inside the API while processing each request and response.
Setup order: This page covers the report configuration for Internal Flow Tracking. For the OpenTelemetry exporter setup that captures the spans, see Integration OpenTelemetry Extension first. Complete that setup, then return here to configure how internal flow data is displayed in your reports.
-
OpenTelemetry spans are captured during test execution using the
Kronikol.Extensions.OpenTelemetrypackage. A custom exporter stores allActivityspans in memory. -
HTTP boundaries are timestamped —
TestTrackingMessageHandlerandMessageTrackerrecordActivity.Current?.TraceId,Activity.Current?.SpanId, andDateTimeOffset.UtcNowon every request and response log entry. From 4.26.5 the response of a call sent over a socket (SocketsHttpHandler,HttpClientHandler) records the span its transport started for the call instead, the one the transport sends in the call'straceparent: the handler logs the request before the transport starts that span, under the span current then, which inside a host is the host's request span. From 5.31.2 the same holds through a handler that hands the call on through an invoker or a client it holds, which the handler follows to the transport, and the span is read from the copy of the request when that handler sends one. From 4.17.0 Kafka entries record ids too: a produce those of its message'straceparentor ofActivity.Current, a consume those of the message'straceparent, so a consume's span id is the producer's (Trace Ids and traceparent). -
Segments are built at report generation time.
InternalFlowSegmentBuildercorrelates the captured spans with the calls' records, one segment per arrow in the sequence diagram. A call's segment holds the spans of its trace that started between its request (less 50 ms, for a span that starts before the request is logged) and its response, but for a span from those 50 ms that began in the time of another call of its scenario, the call it was made in or one before it, which is that call's work, or that runs across another call made beside it, which is the span of what made both; the call's own span and the spans under it are its own whatever their times, unless another call made beside it was made under that span, which is then the method that made both, or the span's parent is another call's own span, the first span of that call's handling, which a tracker that records the current span records for a call made while serving it. A client library's span, a client or producer span no call claims, that begins a moment before a call with no trace id is logged and is still running then is that call's, with the spans under it, as when Cosmos DB's SDK starts its operation before its request reaches Kronikol's handler: each such span goes to the first such call logged after it began, in the order the spans began, unless a server or consumer span runs under it, which makes it a call to a service the run saw, as a gRPC call's client span is (from 4.26.7: 4.26.5 and 4.26.6 showed it in the request's popup only). The kind is read from spans recorded in the test process, so an ingested call with no trace id takes no such span. A call's own span is the one its request records, or the one its response records when that span runs under the request's (step 2), so the calls a host makes under one request each keep their own spans (from 4.26.5: before, a call took every span of those 50 ms, and the calls a host made under one request, at once or one after another, all claimed the host's request span, so each of their popups showed the host's whole request, as did a call with no trace id made inside a request). A call that records no trace id, as no database tracker does, takes what its scenario's traced calls could take, or, when they record none, the spans of traces no call records. Where the calls of several scenarios record one trace (an ambientActivityaround them), a call takes its own span and the spans descending from it. Where the calls of one scenario record one trace under span ids of their own (a browser test's one trace, which a proxy tap forwards under a span per call), a span that descends from another of those calls, and not from the call's own, shows in that call's popup only; a call nested in another still shows inside it, and a span that descends from none of them stays in every call whose time holds it (from 4.18.3, #170: before, two such calls that overlapped in time each showed both calls' spans). Which call another ran inside is read from the span tree first, so the queries a service makes for two calls an app made at once stay each with its own call, though both were made while both calls were open (from 4.19.4: before, both went inside the call opened last, whose popup showed the other's query as well). A span that calls of two scenarios would both take goes to neither, and the popup says how many spans it left out and why (from 3.35.1, #87: before, a call that recorded no trace id took every span of the run that started in its time, so with tests running at the same time one request's popup showed the others' work as its own). -
Clickable links are injected into the PlantUML sequence diagram arrows using PlantUML's
[[link]]syntax. Each arrow becomes a clickable hyperlink with an#iflow-{id}anchor. -
A popup script intercepts clicks on these SVG links. When you click an arrow, it looks up the segment data in the report's
iflow-segmentselement, which it decodes once, on the first popup, and displays the internal flow as either a PlantUML activity diagram (rendered in-browser) or an HTML call tree.
Note: Internal Flow Tracking requires
DiagramFormat = DiagramFormat.PlantUml.
Internal flow tracking is enabled by default — no extra configuration is needed for the standard case.
Span capture is auto-started on first use: by TestTrackingMessageHandler on its first request, by GrpcTrackingInterceptor on its first tracked call, and from 4.17.0 by a Kafka tracker on its first call. All well-known auto-instrumentation sources (ASP.NET Core, HttpClient, EF Core, Redis, Cosmos, etc.) are captured automatically. From 5.32.2 HttpClient's spans keep their source and are drawn in a System.Net.Http lane, labelled with the HTTP method on .NET 9 and later. Before, the listener left that source out, so each traced HttpClient call, the request under each gRPC call over a socket among them, was drawn in an "Unknown" lane (#138). The listener takes HttpClient's span only under a span, so a call made outside a trace makes no span and sends no traceparent, and it leaves out the runtime's connection-level sources (Experimental.System.Net.*), whose spans are noise.
Your SUT's own ActivitySources are captured as well, with no setup, as the listener hears every source. The handler options' InternalFlowActivitySources, which took their names, was never read and is obsolete from 5.34.0 (#138); to draw only some sources, see Manual Source Filtering.
Alternatively, you can explicitly start a listener via DI with AddActivityListenerForInternalFlowTracking(), or use the manual OTel SDK AddTestTrackingExporter() approach. See Integration OpenTelemetry Extension for details.
InternalFlowTracking defaults to true on ReportConfigurationOptions. To disable it, set it to false:
new ReportConfigurationOptions
{
InternalFlowTracking = false, // opt out
// ... other options
}When InternalFlowTracking is enabled, the following are automatically forced:
-
InlineSvgRendering = true— SVG diagrams are embedded inline in the HTML (required for clickable arrows) -
PlantUmlImageFormat = PlantUmlImageFormat.Svg— when usingPlantUmlRendering.ServerorPlantUmlRendering.Local(required for SVG-based interaction)
Note:
PlantUmlRendering.BrowserJsalready renders inline SVG, so no override is needed for that mode.
The popup content can be rendered in two styles, controlled by InternalFlowDiagramStyle:
Renders a PlantUML activity diagram with swimlanes grouped by ActivitySource name. Each span appears as an action in its source's swimlane, with duration shown in milliseconds. The activity diagram is rendered client-side using the PlantUML TeaVM JS engine. That holds under every PlantUmlRendering: under NodeJs, Server and Local, whose report arrives with its sequence diagrams drawn, the page fetches the engine the first time a reader opens the Activity tab or a popup, and never when it opens. Until 3.31.8 such a page carried no engine and those views stayed blank; under NodeJs an arrow's link, which that engine draws as text, also opened nothing.
InternalFlowDiagramStyle = InternalFlowDiagramStyle.ActivityDiagramRenders an HTML nested list showing the span parent-child hierarchy as a call tree. Each node shows the source name, span display name, and duration. This is lightweight and doesn't require the PlantUML JS engine.
InternalFlowDiagramStyle = InternalFlowDiagramStyle.CallTreeReserved for future use.
All internal flow content types support the right-click context menu:
- Activity diagrams (SVG) — Full menu with PNG, PNG (no transparency), SVG, and source options
-
Flame charts (
data-diagram-type="flamechart") — PNG copy/save only -
Call trees (
data-diagram-type="calltree") — PNG copy/save only
The context menu renders at z-index: 20001 to appear above popup overlays. See Inline SVG Rendering › Context Menu for the full menu reference.
A gRPC call made through Kronikol's interceptor records three spans: Kronikol's span for the call, grpc-dotnet's Grpc.Net.Client.GrpcOut under it, and HttpClient's request under that. From 5.34.0 every view draws them as one step, named for the call's method (/orders.Orders/Place), where they were three steps in three lanes, the last one a POST in an "Unknown" lane before 5.32.2.
- A step is named from its span's display name when something set one, else from its
grpc.methodtag, else from OpenTelemetry'srpc.serviceandrpc.method, else from its operation. A host in the test's process names its step for the method it served, as its request span carriesgrpc.methodtoo. - A transport span that is the only one of its source under the span it carries folds into it, and its own children move up to that span. A call that sent two requests, a retry or a hedge, keeps both.
- The popup's title still counts the spans captured.
Control which spans are included using InternalFlowSpanGranularity:
| Value | Behaviour | Best for |
|---|---|---|
AutoInstrumentation (default) |
Only includes spans from well-known auto-instrumentation sources (ASP.NET Core, HttpClient, EF Core, Redis, Cosmos, etc.) | Most projects — shows framework-level flow without noise |
Manual |
Only includes spans from sources listed in InternalFlowActivitySources
|
When you want to show only your own custom activity sources |
Full |
Includes all captured spans regardless of source | Debugging or when you need complete visibility |
The AutoInstrumentation filter recognises these ActivitySource names:
Microsoft.AspNetCoreSystem.Net.HttpMicrosoft.EntityFrameworkCoreNpgsqlStackExchange.RedisAzure.CosmosAzure.StorageMicrosoft.Azure.CosmosOpenTelemetry.Instrumentation.HttpOpenTelemetry.Instrumentation.AspNetCoreOpenTelemetry.Instrumentation.SqlClientOpenTelemetry.Instrumentation.EntityFrameworkCoreKronikol.GrpcGrpc.Net.Client-
Kronikol.Kafka(4.17.0+: the spanKafkaTracker.TrackConsumehandles a message under; from 4.18.0 also the wrappers' spans underTraceMessageProcessing)
From 5.32.2 a name also covers the sources in its namespace: the Cosmos DB SDK's source is Azure.Cosmos.Operation, and the Azure Storage clients' are named after the client (Azure.Storage.Blobs.BlobClient), so before, Azure.Cosmos and Azure.Storage matched none of them. Before 5.32.2 no captured HttpClient span carried System.Net.Http either (see Spans without a source), so a trace whose only well-known span was an HttpClient, Cosmos DB or Azure Storage call was dropped.
An activity made with new Activity(name), rather than from an ActivitySource, belongs to .NET's unnamed default source. HttpClient makes its span that way whenever nothing hears System.Net.Http, the Azure SDK unless its client uses its source, and Application Insights for its own work. From 5.32.2 such a span is named from its operation, and its lane, the granularity filters and the diagnostics report's discovered sources all read that name:
| Operation name | Name |
|---|---|
System.Net.Http.HttpRequestOut |
System.Net.Http |
Microsoft.AspNetCore.Hosting.HttpRequestIn |
Microsoft.AspNetCore |
Grpc.Net.Client.GrpcOut |
Grpc.Net.Client |
Any other name with a .
|
the name up to its last .: Microsoft.ApplicationInsights.Init is Microsoft.ApplicationInsights
|
A name with no .
|
the name itself |
Before, it was drawn in an "Unknown" lane and matched no filter. An Azure SDK span made this way lands in a lane named after its client (BlobClient), not after the source the same call comes from when the client uses one (Azure.Storage.Blobs.BlobClient): the activity does not carry the client's namespace. Spans ingested from another process (kronikol ingest --spans) keep their instrumentation scope, and "Unknown" remains only for a span that has none.
To show only specific activity sources, use Manual granularity with InternalFlowActivitySources:
new ReportConfigurationOptions
{
InternalFlowTracking = true,
InternalFlowSpanGranularity = InternalFlowSpanGranularity.Manual,
InternalFlowActivitySources = ["MyApi.Services", "MyApi.Repositories"]
}| Property | Type | Default | Description |
|---|---|---|---|
InternalFlowTracking |
bool |
true |
Master switch. When true, enables internal flow popups on PlantUML sequence diagram arrows. Forces InlineSvgRendering = true and PlantUmlImageFormat.Svg for Server/Local. |
InternalFlowDisplay |
InternalFlowDisplay |
Popup |
How the internal flow content is shown. Popup shows it in a modal overlay. Inline is not implemented: no report reads this option, and every report uses the popup. |
InternalFlowTrigger |
InternalFlowTrigger |
Click |
User interaction that opens the popup. Click requires clicking the arrow. Hover is not implemented: no report reads this option, and a click opens the popup. |
InternalFlowDiagramStyle |
InternalFlowDiagramStyle |
ActivityDiagram |
Visual style for the internal flow content. ActivityDiagram renders a PlantUML activity diagram with swimlanes. CallTree renders an HTML nested list. |
InternalFlowSpanGranularity |
InternalFlowSpanGranularity |
AutoInstrumentation |
Controls which spans are included. AutoInstrumentation filters to well-known sources. Manual uses InternalFlowActivitySources. Full includes everything. |
InternalFlowActivitySources |
string[]? |
null |
Activity source names to include when InternalFlowSpanGranularity is Manual. Ignored for other granularity settings. |
InternalFlowNoDataBehavior |
InternalFlowNoDataBehavior |
HideLink |
What happens when an arrow has no captured spans. HideLink (default) removes the clickable link. ShowMessage shows "No internal activity captured". VisualDistinction is not implemented and behaves as ShowMessage. |
InternalFlowHasDataBehavior |
InternalFlowHasDataBehavior |
ShowLinkOnHover |
What happens when an arrow has captured spans. ShowLinkOnHover (default) shows the clickable link only on hover. ShowLink always shows the link. |
InternalFlowShowFlameChart |
bool |
true |
When true, adds a flame chart visualisation alongside the main content. |
InternalFlowFlameChartPosition |
InternalFlowFlameChartPosition |
BehindWithToggle |
Where the flame chart appears. Underneath places it below the main content. BehindWithToggle shows a toggle button to switch between the main view and the flame chart. |
InternalFlowContentStrategy |
InternalFlowContentStrategy |
Embedded |
Not implemented: no report reads it. Every report embeds the segment data, compressed, in one element (see [[Content Strategy |
InternalFlowFragmentsFolderName |
string |
"spans" |
Not implemented: no report writes fragment files. |
InternalFlowPopupCustomStyleSheet |
string? |
null |
Custom CSS added to both HTML reports while internal-flow tracking is on, after the built-in stylesheets and the specifications stylesheet and before CustomCss, so it can override or extend the default popup styles. Applied from 3.29.2; earlier releases ignored it. |
When the OpenTelemetry extension is not installed, or spans are not captured for a particular segment, the InternalFlowNoDataBehavior setting controls how empty segments are handled:
| Value | Effect |
|---|---|
HideLink (default) |
The arrow is not a link: it rests in the diagram's ink and a click opens nothing. Kronikol writes the [[link]] on every tracked arrow before the spans are known, and the report hides the ones whose segment captured nothing, under every rendering mode (from 3.31.9, and its look from 3.32.3 under Server and Local rendering, where it kept the link's blue and underline; before 3.31.9, an inline SVG drawn by Server or Local rendering kept the link, and a click opened a popup saying there was no data). |
ShowMessage |
The arrow is still clickable but the popup shows a diagnostic message including the current granularity setting, configured sources, total span count, and actionable suggestions (from 3.35.1 as a list under Diagnostic info: before, the popup showed the message's tags as text). See [[Diagnostics and Debugging › Empty Diagram Diagnostics |
VisualDistinction |
Not implemented: behaves as ShowMessage. |
A call that captured spans but lost all of them to calls of another scenario (see step 3 of How It Works) is not empty: under every setting its arrow stays a link, and its popup says how many spans it left out and why (3.35.1).
When spans are captured for a segment, the InternalFlowHasDataBehavior setting controls how the clickable link is displayed:
| Value | Effect |
|---|---|
ShowLinkOnHover (default) |
The clickable link is only visible when hovering over the diagram arrow. This keeps the diagram clean while maintaining interactivity. Under every rendering mode since 3.32.3; before it, under Server and Local rendering, whose Java engine draws a link as an <a>, every link showed at rest, blue and underlined. |
ShowLink |
The clickable link is always visible on arrows that have internal flow data. |
Both HTML reports carry the segment data as one element in the head, <script id="iflow-segments" type="application/json">. It holds the whole map of segments, gzipped and base64-encoded, and the list of arrow ids the page needs to know which arrows open something without decoding the map: hidden, the linked ids that have no segment, or has, the segment ids some diagram links, whichever is shorter. The map is decoded once, on the first popup, and the popup shows "Loading…" until it is. A report with no segment carries no element. From 3.31.9; before it, the map was a raw JavaScript object in a <script> that set window.__iflowSegments, which a page may still set itself. A popup that cannot decode the map, because the browser has no DecompressionStream (Chrome and Edge before 80, Firefox before 113, Safari before 16.4) or the file was damaged, says "Internal flow data could not be decompressed:" and why, and logs the error to the console. Before 3.32.4, a browser without DecompressionStream left it on "Loading…".
From 5.6.0 the HTML reports are written compressed (Generated Reports › The compressed page). There the element's z holds the map's JSON as a string, stored as it is inside the page's zstd frame, so a popup reads it without DecompressionStream, and a damaged file is refused by the page's loading screen before any popup opens. A plain page, one written before 5.6.0 or the copy kronikol decode writes, holds the map gzipped and base64-encoded as above.
From 3.35.2 a flow that more than one arrow opens, such as a call and the calls nested in it, which show the same spans, is stored in the map once: the first segment that shows it keeps it, and each later one names that segment under sameAs and keeps its own title (#86). On BreakfastProvider's lanes the map a browser decodes on the first popup went from 2,418,029 to 1,583,452 bytes and from 1,659,007 to 1,039,491, and the page's compressed copy from 152,528 to 145,616 and from 90,384 to 84,848 bytes. A map written before holds every flow in full, and renders as it did, alone or merged with newer ones. From 4.0.1 the segment that holds a flow names its diagram and its flame chart by their places in one table, which ends the map under the first segment's key with a ~ before it and holds each distinct diagram and flame chart once: the same calls starting at other moments draw one diagram, which gives each span's duration in whole milliseconds, and flame charts of their own, which place each span in time, and diagrams beside diagrams and flame charts beside flame charts compress better than the two interleaved. No other shard holds the table's key, so a merge keeps every shard's table. On BreakfastProvider's lanes the page's compressed copy of the map went from 144,884 to 133,128 bytes and from 83,856 to 77,240, and the map a browser decodes from 1,577,081 to 1,416,035 and from 1,035,395 to 862,459. From 3.35.3 the map holds only the segments some arrow of the report links; a segment no arrow links, such as a call an arrow cap (MaxArrowsPerDiagram) leaves undrawn or a call of a test the report does not show, could never be opened, and is no longer written.
Measured on a real suite (BreakfastProvider's xUnit lane, 203 scenarios, 1,296 segments), the report went from 4,842,803 to 2,565,751 bytes and what a visitor downloads from a server that compresses it (GitHub Pages) from about 1,157,608 to 898,931 bytes, and the page's domContentLoaded from 99 to 75 ms. A filtered export carries the element whole.
InternalFlowContentStrategy.SeparateFragments and InternalFlowFragmentsFolderName, which this section used to recommend for large reports, are not implemented: no report reads them. Whether v6 removes or implements them is recorded in the roadmap.
// Span capture is automatic — the TestTrackingMessageHandler auto-starts
// an ActivityListener that hears every source but the runtime's connection-level ones,
// your own ActivitySources among them, so the handler options need nothing for it:
new XUnitTestTrackingMessageHandlerOptions
{
CallerName = "Tests",
PortsToServiceNames = { [5001] = "MyApi" }
};
// In your report generation (InternalFlowTracking defaults to true)
var options = new ReportConfigurationOptions
{
// InternalFlowTracking = true, // already the default
InternalFlowDiagramStyle = InternalFlowDiagramStyle.ActivityDiagram,
InternalFlowSpanGranularity = InternalFlowSpanGranularity.AutoInstrumentation,
InternalFlowNoDataBehavior = InternalFlowNoDataBehavior.HideLink,
PlantUmlRendering = PlantUmlRendering.BrowserJs // Recommended for internal flow
};When you open the HTML report and click a sequence diagram arrow, a popup will show the internal flow activity that occurred inside your SUT during that segment of the request processing.
In addition to per-arrow flow popups, you can visualise the entire test's internal activity as a single flame chart or activity diagram. This shows all OTel spans captured during the test, across all HTTP boundaries, with optional boundary markers showing where each HTTP request occurred.
The WholeTestFlowVisualization property on ReportConfigurationOptions controls this feature:
| Value | Behaviour |
|---|---|
None |
No whole-test flow section is rendered. |
FlameChart |
Shows a flame chart with vertical dashed boundary markers at HTTP request timestamps. |
ActivityDiagram |
Shows a PlantUML activity diagram with all spans. |
Both (default) |
Shows a toggle between Activity and Flame Chart views. |
var options = new ReportConfigurationOptions
{
WholeTestFlowVisualization = WholeTestFlowVisualization.Both // default
};When enabled, each scenario whose calls captured spans gets two more views in its diagrams section, beside its sequence diagrams (or on their own when it has none):
-
Activity Diagrams: a full PlantUML activity diagram with swimlanes grouped by
ActivitySourcename. -
Flame Chart: an HTML flame chart where each span is a horizontal bar proportional to its duration. Vertical dashed markers show when the scenario's requests were made, one per request; a step bar or an assertion note draws none (from 3.31.4: since 3.15.1 each drew a line labelled
: /). Supports click-to-zoom: click any bar to zoom into that time range, double-click to reset. Tooltips show the source name, span name, duration, and percentage of total.
The toggle buttons let you switch between views without reloading. A scenario that made no tracked call has no whole-test flow: which spans are a scenario's own is known only from its calls (from 3.31.4: since 3.15.1 such a scenario showed every span of the run). A scenario's flow holds every span of each trace only its calls record, and, of a trace other scenarios' calls record too or for calls that record none, the spans its calls' popups kept (from 3.35.1: before, a scenario whose calls recorded no trace id showed every span of the run). InternalFlowHtmlGenerator.GenerateWholeTestFlowHtml renders the same views as a collapsed <details> block titled "Whole Test Flow (N spans)", for a page you compose yourself.
A LightBDD run writes the same Kronikol report as every other framework, so it carries the whole-test flow whenever InternalFlowTracking is enabled. (The WholeTestFlowHtmlProvider that added it to LightBDD's own HTML report was retired with LightBDD.Contrib.ReportingEnhancements in 2.0.61-beta.)
A run captured outside the test process and turned into a report by kronikol ingest has internal flow too when its
services export their spans: kronikol ingest --spans <file|dir|glob> reads them as OTLP/JSON lines, and
IngestRequest.Spans takes them in memory. Each call's popup, its flame chart and each scenario's whole flow are then
drawn from those spans, by the rules above, and the report looks as an in-process one does.
-
The join is the call's: an ingested call names the trace and the span it was made under in its
activityTraceIdandactivitySpanId, and takes the spans of that trace that started in its time. The capturer stamps them where it sees the call (OpenTelemetry JS'strace.getActiveSpan()in a Fastify hook or an MSW listener). -
Every span is drawn, unless
Manualgranularity names sources: theAutoInstrumentationlist above is .NET's. An OTLP span's instrumentation scope is its source, the swimlane (@fastify/otel,@opentelemetry/instrumentation-graphql), else its service. - The span store is not involved: spans given to an ingest are drawn instead of it, and it is neither read nor written, so an ingest in a process that recorded spans of its own neither shows them nor loses them. The diagnostics count the spans supplied, the calls whose trace no span has, and the spans no call's popup holds.
Internal flow reads seven things of a span: its trace id, span id and parent, its name, its source, its start and its
duration. From 4.4.0 they are a FlowSpan, which an Activity becomes when the report is generated
(FlowSpan.From) and an OTLP span when it is read (OtlpSpan.ToFlowSpan()); InternalFlowSegment.FlowSpans holds a
segment's, and Spans still holds its activities, empty for ingested spans. An in-process report is the same, byte
for byte, as before.
Ingesting External Captures › Spans has the file format and the rules; Ingesting from Jest a Node harness that writes the spans.
Test Execution Report Generation
┌──────────────────────────────┐ ┌──────────────────────────────┐
│ │ │ InternalFlowSpanCollector │
│ Auto-started (default): │ │ └─ Reads spans directly │
│ ActivityListener (BCL) │ │ from InternalFlowSpanStore│
│ └─ Started by │ │ (no reflection) │
│ TestTrackingMessageHandler│ │ │
│ └─ AllData sampling ────────┼──┐ │ InternalFlowSegmentBuilder │
│ (non-invasive) │ │ │ └─ Correlates spans with │
│ │ │ │ HTTP boundary timestamps │
│ Optional (manual): │ │ │ │
│ OTel TracerProvider │ │ │ InternalFlowRenderer │
│ └─ AddTestTrackingExporter()│ │ │ └─ ActivityDiagram or │
│ └─ TestTrackingSpanExporter ┼──┤ │ CallTree output │
│ │ │ │ │
│ Both paths write to: │ ▼ │ InternalFlowHtmlGenerator │
│ InternalFlowSpanStore ◄─────┼──┘ │ └─ iflow-segments element │
│ (core package, static) │ │ gzip JSON + id list │
│ (dedup by reference) │ │ │
│ │ │ DiagramContextMenu │
│ TestTrackingMessageHandler ──┼──────────────► │ └─ Popup JS/CSS for │
│ └─ Timestamps + TraceId │ │ interactive arrows │
│ on RequestResponseLog │ │ │
└──────────────────────────────┘ └──────────────────────────────┘
-
PlantUML only — Internal Flow Tracking requires
DiagramFormat.PlantUml. -
Inline SVG required — The feature relies on injecting
[[link]]anchors into PlantUML arrows and intercepting them in the rendered SVG. This requires inline SVG rendering (automatically enabled). -
Zero-config by default — The
TestTrackingMessageHandlerauto-starts anInternalFlowActivityListener, which hears every source but the runtime's connection-level ones (Experimental.System.Net.*), as a Kafka tracker does from 4.17.0.InternalFlowTrackingdefaults totrue. No extra packages, no extension methods needed for the standard case. UseInternalFlowActivitySourcesonTestTrackingMessageHandlerOptionsfor custom sources, orAddActivityListenerForInternalFlowTracking()for DI-based registration. TheKronikol.Extensions.OpenTelemetrypackage is only needed for manual OTel SDK exporter control viaAddTestTrackingExporter(). The store deduplicates by reference, so both paths can be active without duplicates. - Span correlation by trace, tree and time — A call takes the spans of the trace it recorded that started in its time, from 50 ms before its request to its response, less the spans of those 50 ms that step 3 gives to other calls (from 4.26.5); where calls of several scenarios record one trace, only the spans under its own span; where calls of one scenario record one trace under span ids of their own, not the spans under another of those calls (from 4.18.3); a call with no trace id takes what its scenario's traced calls could take (How It Works, step 3). Before 3.35.1 spans were matched by timestamp alone, which is what this line said until 4.4.0.
- Step Tracking — BDD-style step attributes for structuring tests (complementary to internal flow tracking)
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