Skip to content

Getting started

A package, one line in Program.cs and one tag in your layout. BevTools then inspects the running app - its components, renders, state, forms, routes, requests, interop calls, errors, services and circuits - with no change to the components themselves.

Install

Bit.BevTools works in any Blazor host. Bit.BevTools.Server adds the circuit instrumentation of Blazor Server - add it to the server project of a Blazor Server or Interactive Auto app. Bit.BevTools.Loader is only for opening BevTools on demand, below.

dotnet add package Bit.BevTools          # any Blazor host
dotnet add package Bit.BevTools.Server   # + Blazor Server circuits
dotnet add package Bit.BevTools.Loader   # optional: open it on demand, lazy load it in WebAssembly

Register the services

In the server project, AddBitBevToolsServerServices registers everything plus the circuit diagnostics; elsewhere AddBitBevToolsServices does. An Interactive Auto solution calls both - the first in the server project, the second in the .Client project - so BevTools works whichever render mode a page ends up in.

using Bit.BevTools;

// Program.cs of the server project
builder.Services.AddBitBevToolsServerServices();

// Program.cs of a WebAssembly (.Client) project
builder.Services.AddBitBevToolsServices(o =>
    o.Enabled = builder.HostEnvironment.IsDevelopment());

BevTools turns itself on only when the host environment is Development; set options.Enabled to decide yourself. In a WebAssembly client pass the host environment explicitly, as above, so Development-only activation does not depend on the server's environment variables. Disabled, it registers no instrumentation at all.

WebAssembly: keep the metrics

The WebAssembly SDK compiles System.Diagnostics.Metrics out by default, and render-diff and lifecycle timings come from those meters. Put them back in the WebAssembly project; without it the About panel says that these timings are not available, and everything else still works.

<PropertyGroup>
    <MetricsSupport>true</MetricsSupport>
</PropertyGroup>

Place the panel

Once, inside an interactive component - typically the layout. It renders nothing during prerendering, and it loads its own script and stylesheet (static web assets of the package), so no <script> or <link> tag is needed.

@* _Imports.razor *@
@using Bit.BevTools

@* MainLayout.razor (or any interactive component) *@
<BevToolsPanel />

Open it

Press CtrlShiftD or click the βš™ BevTools badge in the bottom corner; the panel remembers its tab, dock, size and theme. CtrlK (or πŸ” Search / > command) searches components and runs commands: >pause, >pick, >export, >trace… The header, left to right:

  • ⏸ Pause freezes every buffer, so what you are reading is not pushed out by a busy app; errors are still collected. β–Ά Resume says how many renders it skipped.
  • live re-reads the open tab as data arrives; turned off, ↻ Refresh re-reads it on request.
  • βŒ– Pick picks a component in the page; ⧉ opens the companion window; then the theme, the dock (bottom or right), the size, and βœ•.

The panel's defaults are options too:

builder.Services.AddBitBevToolsServerServices(o =>
{
    o.Ui.ToggleShortcut = "Ctrl+Shift+D";   // the default
    o.Ui.Dock = BevToolsDock.Right;        // or Bottom (the default)
    o.Ui.Theme = BevToolsTheme.Auto;       // Auto, Light or Dark
    o.Ui.OpenByDefault = false;
    o.Ui.ShowBadge = true;            // false: only the companion window and the extension show it
    o.Ui.EditorUrlTemplate = "vscode://file/{path}:{line}:{column}";   // what source links open
});

The tabs at a glance

Each tab answers one kind of question; every scenario page of this site is built to ask some of them.

TabWhat it answersTry it on
ComponentsWhich components exist, with their parameters, cascading values, state, services, handlers, lifecycle, source and render mode; why each render happened. βŒ– picks one in the page.Dashboard, Counter
ProfilerRenders and time per component or per type, since a reset; each render batch as a cost tree or on a time axis; renders per page; comparison with an earlier export.Products
TimelineEvery render, batch, event, navigation, state change, request, interop call, error and custom event, each linked to its cause and its effects.Counter, Products
StateRegistered state containers and the signed-in user: snapshots and a history of named changes with diffs.Checkout, Routing
FormsEvery EditForm: its fields, what validates it, what shows its messages, each validation's outcome.Checkout
RoutingThe current page, template, route values and query; the route table; navigation history and guards; who is signed in.Routing & sign-in
NetworkEvery IHttpClientFactory request with its trigger and sender; requests per endpoint; WebSocket and SignalR traffic.Orders
JS Interop.NET β†’ JS calls per identifier and component, and JS β†’ .NET calls.JS interop
ErrorsExceptions, failed requests, JS errors, resources that did not load and CSP violations, with what preceded each; groups of the same failure.Errors
ServicesEvery DI registration with its lifetime, dependencies and consumers; captive dependencies and cycles.Dashboard
DiagnosticsFindings with evidence - render storms, request bursts, leaks, loops, forms that do not validate… - and the rules behind them.every page
RuntimeWhere each render mode runs, what prerendering did, the server process or WebAssembly runtime, the heap, and disposed components still alive.Products, Circuit
BrowserConnectivity, visibility, the JS heap, page load, storage (redacted) and an accessibility check of the page.JS interop, Checkout
AboutWhat this session can and cannot observe and why, what BevTools itself costs, and the exports.Share & automate
CircuitBlazor Server only: the circuit's connection, inbound message time, reconnects and the process's other circuits.Circuit

Extensions add tabs after these - on this site, Store (see Extending). About also lists every piece of information as Available, Partially available, Requires instrumentation, Requires application integration, Requires runtime support, Not technically reliable or Not available, with the reason: what BevTools cannot observe, it says it cannot.

How much it reads

Structure, counts, timings, causes, requests and errors are recorded at every level; options.ValueAccess decides how much of the app's own values BevTools reads.

LevelParameters, fields, state, claimsDefault
FullRead and shown, member by member, redactedDevelopment, and every single-user host (WebAssembly, Blazor Hybrid)
ShallowNumbers, booleans, dates and harmless strings only; objects show their type and size-
NoneNames and types onlyOutside Development in a process that serves many users

At every level, values are redacted where they are captured: by name (password, token, authorization, apikey, secret…), by [BevToolsSensitive], and by shape - JSON Web Tokens, bearer tokens, connection strings, private keys, cloud keys. Configuration, options, credentials, request and response bodies and WebSocket payloads are never read, nothing is sent anywhere, and every export, trace and companion view shows exactly what the panel does. Runtime shows the level in force; the dashboard has secrets to look for.

Open it on demand, even in production

With Bit.BevTools.Loader, an app ships BevTools without anyone downloading or running it until they ask. The loader is small, trimmable and AOT compatible; until BevTools is attached it installs only hooks that pass everything through. In Blazor WebAssembly, Bit.BevTools itself can then be lazy loaded.

<BlazorWebAssemblyLazyLoad Include="Bit.BevTools.wasm" />   <!-- the WebAssembly project -->

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>
}

Opened later, BevTools reads the components that already exist and sees everything from then on; About lists exactly what such a session can see. On Blazor Server one process serves many users, so outside Development CanOpen stays false unless the app sets AllowInMultiUserHost together with Authorize, a callback that decides who may open it - asked on every open, logged on every attach, and with ValueAccess at None unless the app raises it.

builder.Services.AddBitBevToolsLoaderServices(o =>
{
    o.AllowInMultiUserHost = true;
    o.Authorize = async services =>
    {
        var state = await services.GetRequiredService<AuthenticationStateProvider>().GetAuthenticationStateAsync();
        return state.User.IsInRole("Developer");
    };
});

How this site does it

This demo is a Blazor Web App in Interactive Auto, and it exists to show BevTools wherever it is deployed, so it enables it explicitly instead of relying on the Development environment, and it registers an extension of its own. The panel sits in Routes.razor, after the router, so it survives every navigation.

// Demo/Server/Program.cs (made-up data: values readable outside Development too)
builder.Services.AddBitBevToolsServerServices(o =>
{
    o.Enabled = true;
    o.ValueAccess = BevValueAccess.Full;
    o.Extensions.Add(new DemoBevToolsExtension());
});

// Demo/Client/Program.cs
builder.Services.AddBitBevToolsServices(options =>
{
    options.Enabled = true;
    options.Ui.OpenByDefault = false;
    options.Extensions.Add(new DemoBevToolsExtension());
});
The site's own header, docs layout and footer are ordinary application components here, so they appear in the Components tree, the Profiler and the JS Interop panel next to the scenarios, as a layout does in any app. Under Interactive Auto a first visit runs on a Server circuit; once the WebAssembly runtime is cached, a full reload runs the same pages in the browser. Every scenario says which renderer it got.

Rejoining the server...

Rejoin failed... trying again in seconds.

Failed to rejoin.
Please retry or reload the page.

The session has been paused by the server.

Failed to resume the session.
Please retry or reload the page.

An unhandled error has occurred. Reload 🗙