An in-app inspection and diagnostics tool for Blazor applications: a live component tree, a render profiler that explains why a component rendered, a unified activity timeline, state inspection with diffs, network and JS interop tracking, an error center, a DI inspector, circuit diagnostics for Blazor Server, and an evidence-based diagnostics engine. Built as developer infrastructure with a public extension API, minimal runtime overhead and no changes to application source.
Targets .NET 8, 9 and 10 · Blazor WebAssembly, Blazor Server, Interactive Auto and static SSR. The richest answers (UI-event render causes, handler durations, render-diff and lifecycle timings) come from the framework's own diagnostics, which arrived in .NET 10; on .NET 8 and 9 those rows say so instead of showing nothing.
dotnet add package Bit.BevTools # Blazor WebAssembly / any Blazor host
dotnet add package Bit.BevTools.Server # adds circuit instrumentation for Blazor Server
dotnet add package Bit.BevTools.Loader # opens BevTools on demand, and lets WebAssembly lazy load itRegister the services in Program.cs:
using Bit.BevTools;
builder.Services.AddBitBevToolsServerServices(); // or AddBitBevToolsServices() in WebAssembly / other hostsIn an Interactive Auto solution, call AddBitBevToolsServerServices() in the server project and
AddBitBevToolsServices() in the .Client project, so BevTools works whichever render mode the page ends up in.
In a WebAssembly client, pass the host environment explicitly so Development-only activation does not depend on
server process environment variables:
builder.Services.AddBitBevToolsServices(o => o.Enabled = builder.HostEnvironment.IsDevelopment());In Blazor WebAssembly, add <MetricsSupport>true</MetricsSupport> to the project if you want render-diff and
lifecycle timings: the WebAssembly SDK compiles System.Diagnostics.Metrics out by default. The About panel says so
when it is missing, and everything else still works without it.
Then place the panel once, inside an interactive component:
@* _Imports.razor *@
@using Bit.BevTools
@* MainLayout.razor (or any interactive component) *@
<BevToolsPanel />That is the whole setup. BevTools is enabled only when the host environment is Development (override with
options.Enabled), renders nothing during prerendering, and stays dormant in production builds. The browser bridge
(_content/Bit.BevTools/bit-bevtools.js) and its stylesheet ship as static web assets of the package and are loaded by
the panel itself, so no <script> or <link> tag is needed.
Press Ctrl+Shift+D or click the badge to open it; Ctrl+K opens search and the command palette.
An app that wants BevTools in production too, without anyone downloading it until they ask for it, references
Bit.BevTools.Loader next to Bit.BevTools and lets Blazor WebAssembly lazy load the latter:
<PackageReference Include="Bit.BevTools" />
<PackageReference Include="Bit.BevTools.Loader" />
<!-- the Blazor WebAssembly project; leave it out where BevTools should register at startup -->
<BlazorWebAssemblyLazyLoad Include="Bit.BevTools.wasm" />builder.Services.AddBitBevToolsLoaderServices(o => o.RegisterAtStartup = builder.HostEnvironment.IsDevelopment());@* MainLayout.razor, instead of <BevToolsPanel /> *@
<BevToolsLoaderPanel />
@* the app's own "Open dev tools" button *@
@inject BevToolsLoader BevToolsLoader
@if (BevToolsLoader.CanOpen)
{
<button @onclick="BevToolsLoader.OpenAsync">Open dev tools</button>
}The loader is small, trimmable and AOT compatible, and it is the only BevTools assembly the startup code touches: an
app that lazy loads BevTools must not name a Bit.BevTools type in code that runs before the load, which is why the
loader repeats the few options it forwards (ShowBadge, ToggleShortcut, TrackHttp, UseFrameworkInstrumentation).
- Registered at startup. When
Bit.BevToolsis among the assemblies the app starts with and should run from the first render (RegisterAtStartup; Development by default), the loader registers it exactly asAddBitBevToolsServicesdoes and<BevToolsLoaderPanel />renders the panel from the start. - Attached on demand. Otherwise it installs hooks that cost next to nothing until BevTools is attached: a
pass-through
IComponentActivator(the renderer takes its activator when it is created, so this is the only way in without reflection), pass-through handlers in everyIHttpClientFactorypipeline, the framework's ActivitySource and meters (.NET 10; a renderer reads them when it is created too), and the service collection for the Services panel.BevToolsLoader.OpenAsync()then loadsBit.BevTools(throughLazyAssemblyLoaderin Blazor WebAssembly), attaches it to the running app and opens the panel. It reads the components that already exist from the renderer and sees everything that happens from then on; what happened before stays unrecorded. The session ends with the scope. - Hand-built
HttpClients record throughBevToolsHttpHookHandler, which passes requests through until BevTools runs. Put it closest to the transport handler so every retry attempt is recorded. - Blazor Server. In a process that serves many users,
CanOpenis false outside Development unlessAllowInMultiUserHostis set: the panel would show whoever opens it the server side objects of their circuit, injected services andIConfigurationincluded. WebAssembly and Blazor Hybrid only ever show users their own process.
An app that trims keeps Bit.BevTools whole once it uses it, the loader's reference by name included (an
ILLink.Descriptors.xml embedded in the assembly), so a lazily loaded copy keeps everything it reflects over; an app
that references it but never uses it ships none of it. AOT builds cannot lazy load and start with it instead. In Blazor
WebAssembly, render-diff and lifecycle timings still need <MetricsSupport>true</MetricsSupport>; consider it for
Development only.
Without the loader, BitBevTools.Attach(serviceProvider) attaches BevTools from a component rendered after the load
(then render <BevToolsPanel /> from that scope). It falls back to reflection where the loader has hooks: it replaces
the renderer's activator and wraps the handler of the HttpClient registered in DI, and cannot see IHttpClientFactory
clients or the DI graph; in an app that called AddBitBevToolsLoaderServices it uses the loader's hooks instead. In a
server process outside Development it stays off unless BitBevToolsOptions.Enabled is true. Either way the session ends
with its scope, and a BevTools registered at startup but left off does not stand in the way of an attach. The About
panel lists exactly what an attached session can see.
| Question | Where |
|---|---|
| Which components exist, with what parameters, cascading values, state and services? | Components |
| Why did this component render, and how many times / how long? | Components → Rendering, Profiler |
| Did it need to render, or did the parent re-apply identical parameters? | Components → Rendering (forced re-renders), Diagnostics |
| What did this one click cost, across every component? | Profiler → Reset, click, read since reset |
| What do all 500 rows of my list cost together? | Profiler → by type |
| What happened when I clicked, and what did it cause (renders, HTTP, JS, state)? | Timeline (select an event: caused by / caused) |
| What changed in my state container and which click did it? | State (history with before/after diffs) |
| Which HTTP requests ran, how long, which failed, what triggered them? | Network |
| Which JS interop calls are slow or excessive, and from which component? | JS Interop |
| Where did this exception come from and what preceded it? | Errors |
| What is registered in DI, with which lifetimes, dependencies and mistakes? | Services |
| Is my circuit blocked, reconnecting, dying? | Circuit (Server), Browser |
| What is BevTools itself costing me? | About → overhead |
| How do I share what I found? | About → Export session (JSON) / Copy report (Markdown), or >export in the palette |
Diagnostics rules turn symptoms into causes with evidence, e.g. "ProductList rendered 87 times in 2 s: 81 × parent re-render (Products), 6 × UI event (oninput → FilterBar.OnQuery). Consider ShouldRender…".
Pause (header button or >pause) freezes every buffer so what you are reading is not evicted by a busy
application; errors are still collected, and the resume marker says how many renders were skipped meanwhile.
BevTools never fakes data. Every piece of information is classified in the About panel and in docs/capabilities.md as Available, Partially available, Requires instrumentation, Requires application integration, Requires runtime support (.NET 10 activities/metrics), Not technically reliable or Not available, with the reason.
How the information is obtained, in short:
- Component instances: the public
IComponentActivatorhook. No reflection, no source changes. - Hierarchy, batches, render timing: cached reflection accessors over
ComponentBase/Rendererinternals (publicComponentStatesince .NET 8). Every accessor is optional; when a member is missing the capability is reported as unavailable instead of crashing. - UI events, navigation, lifecycle and diff timings: the framework's own
Microsoft.AspNetCore.ComponentsActivitySource and meters (.NET 10).AddBitBevToolsServicesregisters them through the publicComponentsMetricsServiceCollectionExtensions.AddComponentsTracing/AddComponentsMetrics, because outside server-side rendering the framework does not register them itself — without that, WebAssembly could not answer why did this render? at all. Turn it off withoptions.UseFrameworkInstrumentation = false. On .NET 8 and 9 there is nothing to register, and those capabilities are reported as needing .NET 10. - HTTP:
IHttpMessageHandlerBuilderFilter(allIHttpClientFactoryclients). Bodies are never captured. - JS interop:
IJSRuntimeinjected into components is wrapped on the first render (calls made earlier, fromOnInitialized, are not attributed);IJSRuntimeis not decorated in DI because the framework casts it to its concrete type. - Errors: render wrapper + Blazor's own logger categories + browser
error/unhandledrejection. - Circuits:
CircuitHandler(lifecycle, connection state, inbound message processing time) +components:reconnect-state-changedin the browser.
The design behind it - how render causes are decided, how late diff measurements are matched to their batch, how BevTools keeps from observing itself, session isolation per circuit - is in docs/architecture.md, and its measured cost in docs/performance.md.
Development-only by default. Opened on demand outside Development (Bit.BevTools.Loader, BitBevTools.Attach), it
stays off in a process that serves many users unless allowed explicitly. Member, dictionary-key, header, query, URL-fragment and storage names matching sensitive
patterns (password, token, authorization, cookie, connectionstring, …) are redacted, in navigation URLs as in
request URLs; [BevToolsSensitive] redacts explicitly (fields, properties and auto-properties);
[BevToolsIgnore] hides members or whole components. Request/response bodies and DI instance values are never read.
Circuit ids are truncated. Nothing is sent anywhere: all data stays in the session's memory and is bounded by ring
buffers (MaxEvents, MaxRendersPerComponent, …). Exports contain exactly what the panels show, with the same
redaction, and omit browser storage previews.
public sealed class MyLibraryBevTools : IBevToolsExtension
{
public string Name => "MyLibrary";
public void Configure(IBevToolsExtensionBuilder b) => b
.AddPanel(new BevToolsPanelDescriptor("mylib", "MyLibrary", typeof(MyPanel)))
.AddStateProvider(sp => new StoreAdapter(sp.GetRequiredService<MyStore>()))
.AddDiagnosticRule(new MyRule())
.AddCommand(new MyCommand())
.AddValueFormatter(typeof(Money), v => ((Money)v).ToString("C"))
.HideComponentsInNamespace("MyLibrary.Internal");
}
services.AddBitBevToolsServices(o => o.Extensions.Add(new MyLibraryBevTools()));A library that wants to expose itself to BevTools references Bit.BevTools, which carries the extension contracts:
IBevToolsExtension/IBevToolsExtensionBuilder- contribute panels, state providers, diagnostic rules, commands and value formatters, or hide internal components.IBevStateProvider- expose a state container to the State panel, with snapshots, change history and diffs.IBevDiagnosticRule- turn symptoms into findings with evidence.IBevToolsCommand- add commands to the command palette.IBevToolsTimeline/BevToolsEvent- write your own events into the activity timeline.[BevToolsSensitive]/[BevToolsIgnore]- redact members, or hide members and whole components.
Application code can also emit timeline events through IBevToolsTimeline, expose state with
IBevStateProvider (or services.AddBevToolsStateProvider<T>()), attribute background work with
session.UseAmbient(), pause and resume capture (session.PauseCapture() / ResumeCapture()) and export everything
with BevSessionExporter. Commands get CopyTextAsync / DownloadTextAsync on their context to hand text to the
browser. The Bit.BevTools.Server package is itself an extension (Circuit panel, circuit rules, command).
Bit.BevTools instrumentation, session stores, diagnostics, inspection, export, BevTools UI (Razor
class library) and the browser bridge (Scripts/bit-bevtools.ts -> wwwroot/bit-bevtools.js)
Abstractions/ public integration API (events, state providers, rules, commands, panels, capabilities)
Bit.BevTools.Server circuit handler, circuit registry, Circuit panel (reference extension)
Bit.BevTools.Loader trimmable startup half for opening BevTools on demand: hooks, BevToolsLoader, the panel host
Demo Blazor Web App (Interactive Auto, Server + Client) with a "Problems to diagnose" index
Tests/Bit.BevTools.Tests MSTest + bUnit on net8.0/net9.0/net10.0: instrumentation, tree, rendering, timeline,
state, network, interop, diagnostics, DI graph, concurrency, overhead, memory bounds,
scale, panel interaction, framework-diagnostics attribution, extensibility, forced-render
detection, capture control, profiler baseline/grouping, export
Tests/bit-bevtools-js vitest suite for the browser bridge: isolation, pre-.NET redaction, downloads, shortcuts
Tests/Bit.BevTools.Benchmarks reproducible overhead measurement (dotnet run -c Release)
docs/ capability matrix, architecture, performance
Run the demo with dotnet run --project src/Utilities/BevTools/Demo/Server, then open /problems. The tests:
cd src/Utilities/BevTools/Tests/Bit.BevTools.Tests && dotnet test # run from inside src, so global.json's test runner applies
cd src/Utilities/BevTools/Tests/bit-bevtools-js && npm ci && npm test # after a build of Bit.BevTools, which produces bit-bevtools.jsSee CLAUDE.md for the principles every change has to respect.
The component tree and inspector, parameters, render tracking and profiler, event timeline, error inspection, state inspection, network, JS interop, search/commands, DI inspection, circuit diagnostics and the rules engine. See docs/architecture.md for the design and the list of deliberate non-goals (component ↔ DOM highlighting, request bodies, SignalR payloads).