Skip to content

Latest commit

 

History

History
280 lines (221 loc) · 16.2 KB

README.md

File metadata and controls

280 lines (221 loc) · 16.2 KB

Bit.BevTools

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.


Table of Contents


Installation

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 it

Register the services in Program.cs:

using Bit.BevTools;

builder.Services.AddBitBevToolsServerServices();   // or AddBitBevToolsServices() in WebAssembly / other hosts

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

Opening it on demand, and lazy loading

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.BevTools is among the assemblies the app starts with and should run from the first render (RegisterAtStartup; Development by default), the loader registers it exactly as AddBitBevToolsServices does 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 every IHttpClientFactory pipeline, 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 loads Bit.BevTools (through LazyAssemblyLoader in 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 through BevToolsHttpHookHandler, 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, CanOpen is false outside Development unless AllowInMultiUserHost is set: the panel would show whoever opens it the server side objects of their circuit, injected services and IConfiguration included. 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.


What it answers

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.


Honest capability model

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 IComponentActivator hook. No reflection, no source changes.
  • Hierarchy, batches, render timing: cached reflection accessors over ComponentBase / Renderer internals (public ComponentState since .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.Components ActivitySource and meters (.NET 10). AddBitBevToolsServices registers them through the public ComponentsMetricsServiceCollectionExtensions.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 with options.UseFrameworkInstrumentation = false. On .NET 8 and 9 there is nothing to register, and those capabilities are reported as needing .NET 10.
  • HTTP: IHttpMessageHandlerBuilderFilter (all IHttpClientFactory clients). Bodies are never captured.
  • JS interop: IJSRuntime injected into components is wrapped on the first render (calls made earlier, from OnInitialized, are not attributed); IJSRuntime is 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-changed in 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.


Security

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.


Extending

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


Project layout

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

See CLAUDE.md for the principles every change has to respect.


Status

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