Skip to content

Internal Flow Tracking

Aryeh Citron edited this page Apr 5, 2026 · 13 revisions

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.


How It Works

  1. OpenTelemetry spans are captured during test execution using the TestTrackingDiagrams.Extensions.OpenTelemetry package. A custom exporter stores all Activity spans in memory.

  2. HTTP boundaries are timestampedTestTrackingMessageHandler and MessageTracker record Activity.Current?.TraceId, Activity.Current?.SpanId, and DateTimeOffset.UtcNow on every request and response log entry.

  3. Segments are built at report generation time. InternalFlowSegmentBuilder correlates the captured OTel spans with the HTTP boundary timestamps, grouping spans into segments — one segment per arrow in the sequence diagram. Each segment contains the spans that started between two consecutive HTTP boundaries.

  4. 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.

  5. A popup script intercepts clicks on these SVG links. When you click an arrow, it looks up the segment data from window.__iflowSegments 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. It is not supported with Mermaid output.


Prerequisites

Internal flow tracking is enabled by default — no extra configuration is needed for the standard case.

Span capture is auto-started by the TestTrackingMessageHandler constructor. All well-known auto-instrumentation sources (ASP.NET Core, HttpClient, EF Core, Redis, Cosmos, etc.) are captured automatically.

To also capture your SUT's custom ActivitySources, add their names to the handler options:

new XUnitTestTrackingMessageHandlerOptions
{
    CallingServiceName = "Tests",
    PortsToServiceNames = { ... },
    InternalFlowActivitySources = ["MyApi.Services", "MyApi.Database"]  // optional
}

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.


Configuration

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 using PlantUmlRendering.Server or PlantUmlRendering.Local (required for SVG-based interaction)

Note: PlantUmlRendering.BrowserJs already renders inline SVG, so no override is needed for that mode.


Diagram Styles

The popup content can be rendered in two styles, controlled by InternalFlowDiagramStyle:

ActivityDiagram (default)

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.

InternalFlowDiagramStyle = InternalFlowDiagramStyle.ActivityDiagram

CallTree

Renders 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.CallTree

SequenceDiagram

Reserved for future use.


Span Granularity

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

Well-Known Auto-Instrumentation Sources

The AutoInstrumentation filter recognises these ActivitySource names:

  • Microsoft.AspNetCore
  • System.Net.Http
  • Microsoft.EntityFrameworkCore
  • Npgsql
  • StackExchange.Redis
  • Azure.Cosmos
  • Azure.Storage
  • Microsoft.Azure.Cosmos
  • OpenTelemetry.Instrumentation.Http
  • OpenTelemetry.Instrumentation.AspNetCore
  • OpenTelemetry.Instrumentation.SqlClient
  • OpenTelemetry.Instrumentation.EntityFrameworkCore

Manual Source Filtering

To show only specific activity sources, use Manual granularity with InternalFlowActivitySources:

new ReportConfigurationOptions
{
    InternalFlowTracking = true,
    InternalFlowSpanGranularity = InternalFlowSpanGranularity.Manual,
    InternalFlowActivitySources = ["MyApi.Services", "MyApi.Repositories"]
}

Configuration Reference

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 embeds it directly below the diagram.
InternalFlowTrigger InternalFlowTrigger Click User interaction that opens the popup. Click requires clicking the arrow. Hover shows on mouse hover.
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 ShowMessage What happens when an arrow has no captured spans. ShowMessage shows "No internal activity captured". HideLink removes the clickable link. VisualDistinction visually marks the arrow differently.
InternalFlowShowFlameChart bool false 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 How segment data is stored. Embedded includes all data inline in the HTML. SeparateFragments writes each segment to a separate file in InternalFlowFragmentsFolderName.
InternalFlowFragmentsFolderName string "spans" Folder name for separate fragment files when InternalFlowContentStrategy is SeparateFragments. Relative to the reports folder.
InternalFlowPopupCustomStyleSheet string? null Custom CSS injected into the popup. When set, allows overriding or extending the default popup styles.

No-Data Behaviour

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
ShowMessage (default) The arrow is still clickable but the popup shows "No internal activity captured for this segment."
HideLink The arrow is not clickable — no [[link]] is injected for segments without span data.
VisualDistinction The arrow is clickable but visually styled differently (e.g. dashed or lighter colour) to indicate no data is available.

Content Strategy

By default, all segment data is embedded inline in the HTML report as a <script> block containing window.__iflowSegments. For large test suites with many spans, this can increase file size significantly.

Use InternalFlowContentStrategy.SeparateFragments to write each segment's data to a separate file:

new ReportConfigurationOptions
{
    InternalFlowTracking = true,
    InternalFlowContentStrategy = InternalFlowContentStrategy.SeparateFragments,
    InternalFlowFragmentsFolderName = "spans"  // default
}

This produces files like Reports/spans/iflow-{guid}.html that are loaded on demand when the user clicks an arrow.


Full Example

// Span capture is automatic — the TestTrackingMessageHandler auto-starts
// an ActivityListener for well-known instrumentation sources.
// To capture custom ActivitySources, add them to handler options:
new XUnitTestTrackingMessageHandlerOptions
{
    CallingServiceName = "Tests",
    PortsToServiceNames = { [5001] = "MyApi" },
    InternalFlowActivitySources = ["MyApi.Services", "Microsoft.EntityFrameworkCore"]
};

// 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.ShowMessage,
    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.


Architecture

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 ◄─────┼──┘               │  └─ window.__iflowSegments   │
│  (core package, static)      │                  │     JSON data block          │
│  (dedup by reference)        │                  │                              │
│                              │                  │ DiagramContextMenu           │
│ TestTrackingMessageHandler ──┼──────────────►   │  └─ Popup JS/CSS for         │
│  └─ Timestamps + TraceId     │                  │     interactive arrows       │
│     on RequestResponseLog    │                  │                              │
└──────────────────────────────┘                  └──────────────────────────────┘

Requirements and Limitations

  • PlantUML only — Internal Flow Tracking requires DiagramFormat.PlantUml. Mermaid does not support clickable links with the same mechanism.
  • 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 TestTrackingMessageHandler auto-starts an InternalFlowActivityListener for well-known sources. InternalFlowTracking defaults to true. No extra packages, no extension methods needed for the standard case. Use InternalFlowActivitySources on TestTrackingMessageHandlerOptions for custom sources, or AddActivityListenerForInternalFlowTracking() for DI-based registration. The TestTrackingDiagrams.Extensions.OpenTelemetry package is only needed for manual OTel SDK exporter control via AddTestTrackingExporter(). The store deduplicates by reference, so both paths can be active without duplicates.
  • Span correlation by timestamp — Spans are correlated with HTTP boundaries using timestamps, not parent-child span relationships. This means spans are grouped into the segment whose HTTP boundary immediately precedes them.

Home


Demo


Getting Started

Common Tasks

Integration Guides

Extensions

Configuration

Features

Reference

Clone this wiki locally