What BevTools can observe, how, and where it stops. The same matrix is rendered live in the About panel with the runtime-verified status of each row (for example, whether HandleEvent activities have actually been observed on this host). Nothing in the UI is approximated without saying so.
Legend: Available — works out of the box · Partial — works with stated gaps · Instrumentation — obtained by BevTools instrumenting framework internals (dev-only reflection or wrappers) · Integration — the application or library must call an API · Runtime — depends on framework support (.NET 10 activities/metrics) · Unreliable — deliberately not shown as fact · N/A — not obtainable.
BevTools ships for .NET 10 and 11.
| Information | Status | How / why |
|---|---|---|
| Component instances (all render modes) | Available | Public IComponentActivator hook wraps any existing activator; every instance the renderer creates is registered. |
| Parent/child hierarchy | Instrumentation | Renderer._componentStateByComponent → public ComponentState.LogicalParentComponentState. Cached expression-compiled accessor; reported unavailable if the member disappears. |
Parameters, cascading values, [SupplyParameterFrom*] |
Available | Read on demand from attributed properties; lazy inspection; redaction by name/attribute. |
| Component state (private fields) | Partial | Read on demand; diffed per render only for the selected component (or all with CaptureComponentStateOnRender). Mutations that do not lead to a render are not observed. |
| Injected services with lifetime | Available | [Inject] properties + captured DI registrations; each instance the component received is inspectable like its state (same inspector, same redaction). |
| Source location | Available with symbols | From the app's portable PDB: the .razor file a component is written in (the Razor compiler's #line directives make it BuildRenderTree's document; opened at its top), a code-behind .cs where its members are written, and the exact line of each event handler and callback it binds; links open them in an editor (Ui.EditorUrlTemplate, VS Code by default; script URLs are refused). A PDB beside the assembly is used only when its id is the one the assembly was built with; an embedded PDB is the assembly's own; on WebAssembly the hub fetches the PDB the runtime's own resource list names for this build. Without symbols (Release builds, PDBs not deployed) the location is inferred from the type name and labelled so, with the reason. On WebAssembly the PDB is fetched with the integrity hash the runtime's resource list gives, past the HTTP cache, so a PDB of an earlier build is refused; a failed download is retried, not remembered as "no symbols". After a hot reload edit the locations of the edited assembly are marked before hot reload (its PDB is the build's). An assembly with no file outside WebAssembly (single-file, loaded from memory) says so. Editor links are checked on the final address: a clean scheme that is not a script scheme. |
| Disposal | Partial | Detected when the renderer no longer knows the instance (checked on refresh), so timing is approximate. |
| Event handlers and callbacks it bound | Instrumentation | Read on demand from the frames of its latest render (ComponentState.CurrentRenderTree): DOM handlers, @bind change handlers, EventCallbacks passed to child components, each named by its method (a lambda by the one component method it calls, else the method declaring it; an @bind handler by the method it sets through, "@bind-value → FilterBar.OnQuery", when @bind:set names one). A UI event in the timeline carries only the handler's name, so a bound input's event names its component: "oninput → FilterBar (@bind)". A handler bound under a condition appears exactly while the condition holds. |
| Lifecycle | Partial | Created (activator), initialized and OnAfterRender run (ComponentBase fields), first and last render, a queued render, disposal (noticed on refresh). OnInitializedAsync and the like are not timed per instance; per-type SetParametersAsync times come from the framework meters. |
| Render mode | Instrumentation | What the renderer assigned (@rendermode, [RenderMode*]), read on the first render. A renderer answers for a whole subtree (an interactive renderer with its own mode for every component), so a boundary is where the mode differs from the parent's: on a circuit or in WebAssembly, the interactive root. The tree marks each boundary and the inspector names the mode a component inherits and from whom. The interactive side knows the runtime it got, not that it was chosen by InteractiveAuto; the prerendering summary (Runtime) carries the mode as declared. |
| Component ↔ page (pick an element, outline a component, flash renders) | Instrumentation | Blazor's browser renderer keeps a logical element tree on the DOM nodes it creates (Symbol()-keyed children and parent, a true marker on attached root containers). BevTools computes a component's position as a path of logical indices from its ancestors' render trees (ComponentState.CurrentRenderTree: one logical child per element, text, component and markup frame; regions flattened) and the page hub follows it, checking every step's tag or node kind. A mismatch - a render not yet applied in the browser, DOM changed by script - is reported, never resolved to a guess. The keys are learned from the panel's own element (its logical parent is its component's container comment, which no physical parent can be; for a panel that is itself a root, the root's marked container). A root counts while it carries an attached mark and no detached one (a disposed Web App root keeps its preserve-content mark). The root the panel renders in is found from the panel; any other root by the kinds of its first logical children: a path that fits a single such root identifies it; identical roots are matched by position only when the page holds exactly the runtime's own, and the outline says so; identical roots of two runtimes are reported as ambiguous instead of picked. Nodes rendered through a framework component (a CascadingValue's child content, Virtualize items) belong to the nearest application component unless internals is on. Static (SSR) content and nodes added by script belong to no component, and picking says so. Requires the hub (EnableBrowserBridge). |
| Information | Status | How / why |
|---|---|---|
| Disposed components still alive | Instrumentation | Disposed components are remembered weakly (at most 2000); BevTools keeps no reference to them between checks. One becomes free to go once its grace period is over (10 s; 1 s for a forced check, whose collections then count) and the renderer no longer holds it - a parent's previous render tree keeps a removed child until the parent renders again, which is not a leak and resets the count. Only full (generation 2) collections after that count, and it is called retained after two of them. One exception, on a forced check: a singleton or scoped service that still references it after its disposal is evidence on its own, renderer or not. |
| What keeps it alive | Instrumentation | References checked by identity only, never values: the delegates and framework collections of the singleton and scoped services it injected (a transient one is its own, not what keeps it) and of the values cascaded to it - a handler left on an event, named by method or as a lambda that captures it, also through a subscription object such as IOptionsMonitor.OnChange's; of Microsoft.Extensions.* services only their delegates - the JS runtime's DotNetObjectReference table, and its own timers that are still scheduled with a callback or state that reaches it. Each holder is read once per check, however many retained instances share it (1000 leaked rows: about 56 ms). Anything else (a static, a cache) is not found, and the finding says so. |
| Forced collection | Available | Development and single-user processes (WebAssembly, Hybrid); refused outside Development in a process that serves many users, where it would pause every circuit. |
| Memory over time, full collections, heap after each | Available | Read once a second while the panel component is on the page, open or not (the last ten minutes), and kept while capture is on - paused, no reading is added and a collection made meanwhile is not recorded: GC.GetTotalMemory, the count of the highest generation's collections (gen 2 on CoreCLR, gen 1 on Mono's two-generation GC), the total pause. Each full collection is a timeline event - with the heap after it as the runtime reported it (GC.GetGCMemoryInfo(GCKind.FullBlocking), CoreCLR), else the heap at the first reading after it (Mono keeps no such record: its info's members throw, and are read guarded) - so a slow interaction or a long frame can be read next to the pause that came with it. Runtime → Memory over time draws the heap with each full collection marked. In a server process the heap is the process's, shared by every circuit, and the panel and the events say so. Exported (memory, at most 120 readings and the collections). |
| WebAssembly linear memory | Instrumentation | getDotnetRuntime(0).Module.HEAPU8.length through the in-process module - a synchronous call on the same thread - with each reading: what the runtime got from the browser; it grows and never shrinks, so its rise is how much the heap once needed. |
| A heap that keeps growing | Available | memory.growth: at least five full collections in ten minutes, the heap after the last at least half again the first and 20 MB more, never back near the first. What holds the objects is a heap snapshot's question (dotnet-gcdump, the browser's memory tool); retained components are found by the check above. |
| Information | Status | How / why |
|---|---|---|
Render count, BuildRenderTree duration per instance |
Instrumentation | The private _renderFragment of each ComponentBase is replaced with a measuring wrapper at creation. Components implementing IComponent directly are listed but not measured (shown as not measured). |
| Render batches (one DOM diff) | Instrumentation | Batch boundary = RenderBatchBuilder.UpdatedComponentDiffs.Count == 0 at render start. Fallback: idle-time heuristic (flagged Unreliable). |
| Batch diff duration and DOM edit count | Runtime | aspnetcore.components.render_diff.duration/size meters (.NET 10). On Blazor Server the framework records them when the browser acknowledges the render, so BevTools matches each measurement back to the batch that produced it. Batches BevTools does not record (its own UI, renders while capture is paused) are still bookkept, so their measurements never land on an application batch. Blazor WebAssembly compiles System.Diagnostics.Metrics out by default: add <MetricsSupport>true</MetricsSupport> to the WebAssembly project, otherwise the About panel reports this row as Not available and says why. |
| Cause: parent re-render | Available* | An ancestor rendered earlier in the same batch (renderer re-applied parameters). *Needs hierarchy + batches. |
| Cause: UI event (with attribute, component, method) | Runtime | An inline lambda (@onclick="() => Load(4000)") arrives as its compiled name (<BuildRenderTree>b__0_1); BevTools reads the lambda's IL - or its async state machine's - and names it after the method it calls when that is its only call into application code (framework calls and property accessors aside) and a method of the component: onclick → Orders.Load (lambda). A second application call (a service, a local function, a base class in another assembly), none, a compiled name more than one lambda of the component and its base classes carries, a generic component, or unreadable IL keep lambda in Orders.BuildRenderTree. Hot reload clears what was read. The inspector's event wiring names handlers the same way. |
| Cause: UI event, how it is captured | Runtime | Microsoft.AspNetCore.Components.HandleEvent activity; BevTools adds an ActivityListener and registers the framework's own ComponentsActivitySource (AddComponentsTracing), which server-side rendering registers but WebAssembly and custom hosts do not. Interactions with the BevTools panel itself are excluded, a panel an extension contributes from its own assembly included: the activity names only a component type, so the registry remembers the types only BevTools' own UI rendered. Before the first activity is seen the About panel says so. An event owns the renders made while its activity runs and the batch that renders what it had queued (read from RenderBatchBuilder.ComponentRenderQueue when the activity stops, since the framework renders a synchronous handler's changes after stopping it); a timer or fire-and-forget task the handler started is not the event's once it has ended, although it inherits the activity. |
| Cause: navigation | Partial | NavigationManager.LocationChanged always; Navigate activity when emitted. Same ownership as a UI event: the renders of the navigation itself, not those of a timer the page it rendered started. |
Cause: explicit StateHasChanged / async continuation |
Partial | Everything not attributable above. The caller is not identified (no stack walk by design). |
| Forced re-render (parent re-applied identical parameters) | Partial | For the component selected in the inspector (or all, with CaptureComponentStateOnRender), parameter and cascading values are flattened two levels deep (≤ 25 collection items, one time budget per render) before each render and diffed against the previous captured render. A parent-caused render with no observable change is marked forced and counted. Delegates compare by target method, so a recreated lambda with the same body counts as unchanged. Render fragments compare by instance, and one that reads component state as it runs never counts as unchanged, so new child content is never called forced. [BevToolsSensitive] parameters compare as redacted. Not decided for the first captured render (nothing to compare against), nor for the first one after renders that were not compared (the component was not inspected, or capture was paused). |
SetParametersAsync cost (OnInitialized/OnParametersSet) |
Runtime | aspnetcore.components.update_parameters.duration, per component type. A type only BevTools' own UI renders - its panels, an extension's panel, the Virtualize of the component tree - is left out; a framework type both render (CascadingValue) is counted whole, since the meter names no instance. Same WebAssembly caveat as the diff meters. |
| Event handler duration | Runtime | Activity duration; also handle_event.duration meter. |
| Bytes allocated per render | Available where the runtime counts them | GC.GetAllocatedBytesForCurrentThread() read right around each instrumented BuildRenderTree, after BevTools' own work for the render: what the component's render fragment allocated - strings it formatted, closures and lambdas, collections - on its thread. Children render separately and count for themselves. Per render (the inspector: per render, max, last), per component and per type in the Profiler (Allocated, sortable, per render in each row's title) since the session or the baseline, exports (allocatedBytes, maxAllocatedBytes, allocatedBytesSinceBaseline; the profile's allocatedBytes per type), the Markdown report and each render's timeline data. Probed once at startup: a runtime whose counter does not move reports the row Not available rather than every render allocating nothing. Comparisons: a type allocating MinAllocationChangeBytes (1 KB) and AllocationChangeRatio (25 %) more per render regresses, fewer improves; per render, so rendering more often is not counted twice; compared only when both recordings measured. render.allocations: a type averaging HeavyRenderAllocationBytes (256 KB) per render over at least three renders in the last 30 s. An EditForm's render includes BevTools' watch of its EditContext the first time it gets one. |
| p95 render time per component and per type | Available | Nearest-rank 95th percentile of the samples each instance still holds (the last MaxRendersPerComponent, 100 by default), within the since reset window when it is on; the cell says how many samples it is of. Counts, totals and maxima come from session-long counters, so they are exact however few samples remain. |
| What one render batch rendered | Available | Profiler → a batch → its cost tree: each component under its nearest ancestor that rendered in the same batch, with its own BuildRenderTree time and its subtree's; same-type siblings with nothing rendered below them (a list's rows) are one row ("ProductRow ×120 instances"); at most 300 rows, with a count of the rest. Built when the batch is selected (selecting it again or ✕ closes it; clearing the timeline drops it), from the retained samples, so a batch older than the last 100 renders of its components shows what is left. Tree shows it as that indented table; Time puts the same renders on a time axis - each where its render began (the sample's start timestamp, taken just before BuildRenderTree, and before BevTools' state capture for a component being inspected, so such a bar starts a little early) and as long as its BuildRenderTree took, one row per depth among the components that rendered in the batch. The gaps between bars hold everything else: children's SetParametersAsync and OnParametersSet, the renderer's own work, BevTools' bookkeeping. When the clock could not tell the renders apart (a span of zero), they are drawn side by side in the order they started, at equal widths, and the view says it is not to scale. Bars too narrow for their name show it on hover; a bar opens its component; to a screen reader the chart is one image whose label points to the tree. At most 300 bars, in the order they started. |
| Renders per page | Available | Profiler → Renders per page, closed until asked and then recomputed only when something was captured since: each retained render on the page shown when it happened - the latest navigation the timeline holds at or before it (of several at the same instant, the last) - with visits, renders, render time and the three costliest components there. A navigation is dated from its first render: the router renders the new page before anything else hears of the navigation, so where no Navigate activity records it first (framework instrumentation off) the panel dates it from the render batch the router just ran. Pages are named as their navigation was (route templates; an enhanced navigation to the same path is the same page). Renders older than the oldest navigation the timeline still holds - it drops the oldest events first, and every render is one - have a row of their own. Read from what BevTools still holds: each component's last MaxRendersPerComponent renders and the timeline's last MaxEvents events; recomputed at most once a second while open. |
| Comparison of two recordings | Available | Profiler → Compare… with a saved export, or BevProfileComparison.Compare(baselineJson, currentJson) in a test. Per component type (matched by full name without assembly versions, so a stamped build version does not turn Grid<Product> into a new type): instances, renders, render time, max and p95 (p95 in the API and the Markdown), as regressed / improved / new / gone. Render counts are compared exactly by default (RenderChangeRatio tolerates some, for recordings made by hand); times only beyond 5 ms and 50 %, because they vary between runs; a type that renders as often but clearly longer is a regression. Warnings when the two windows, render modes or runtime major/minor versions differ, a registry was truncated, or an export has no profile section (read from its components, which miss components let go before it). Text from the file is reduced to one line before it reaches the panel or the Markdown. Exports carry a profile section that counts components already let go after DisposedComponentRetentionSeconds, so a long interaction still adds up. |
| What prerendering did | Instrumentation | The prerendering panel persists a bounded summary (components, render totals, slowest components, requests, errors) through PersistentComponentState; the interactive panel takes it back. Needs the panel in the prerendered output and ValueAccess Full (the summary travels in the page). The data fetched twice rule compares its requests with the interactive session's first 30 s. |
| Information | Status |
|---|---|
| Unified timeline (renders, batches, events, navigation, state, HTTP, JS, errors, circuit, browser, diagnostics, custom) | Available |
| Cause/effect links (event → renders/HTTP/JS/state; parent render → child render) | Available where the cause is known (see above) |
| Bounded history with configurable size | Available (MaxEvents, ring buffer, oldest dropped) |
| Custom events from libraries | Integration (IBevToolsTimeline) |
| Markers | Available: ⚑ Mark in the Timeline toolbar (numbered), >mark before the fix in the palette, BevToolsSession.AddMarker, the protocol's mark command; a marker event in the timeline, exports and the performance trace. Not while capture is paused. |
| Information | Status | How / why |
|---|---|---|
| Registered state containers, snapshots, change history, before/after diffs | Integration | Implement IBevStateProvider or register an adapter (AddBevToolsStateProvider). Diffs compare bounded flattened snapshots (depth 3, 25 items per collection). |
| Cascading values | Available | From [CascadingParameter] properties. |
| bit Blex | Integration (Bit.BevTools.Blex) |
AddBevToolsBlex(): one Blex provider - the global state of every store as JSON, diffed member by member, a change per action (cart/AddItem, its arguments read through the inspector at the session's value access - redacted by name and by shape, names only under None - nested actions part of the outer one), and restoring the state after any of them through BlexManager.RestoreGlobalState, on Blex's dispatcher; a restore Blex makes itself (the Redux DevTools, persistence, another tab) is a change too. The last hundred states are kept. |
| Time travel (restoring state) | Integration | Through the provider's own contract, never by BevTools writing application objects (restoring an arbitrary graph is unsafe): a provider that implements IBevStateRestorer gives each change a RestoreToken (BevStateChangeInfo) and restores itself when asked. State → a change → ⟲ restore, or the protocol's restore command (by change id; never waited for on the dispatcher, so a provider that awaits reports through the timeline). Offered only in Development or a process that serves one user, like a forced collection; the panel and About say so elsewhere. A token is kept only for a provider that can restore, exported only as restorable. The restore is a timeline event linked to the change, and the provider's own change notification records the new state. |
| Change details and search | Available | Each change's Detail (what the action was about: Add · Quantum Lamp 3) in the history and the diff; the State filter finds providers by name and changes by action, detail or member. |
| Information | Status | How / why |
|---|---|---|
| Forms on the page: model, owner, fields, modified, changes per field | Instrumentation | Each EditForm's EditContext (_editContext) or an input's, watched from the EditForm's first render and again when it gets a new EditContext (a form reset after submit); held weakly, and forgotten - BevTools' handlers removed - once the EditForm or input is disposed. Field states from _fieldStates; changes counted from OnFieldChanged. Values are never read. At most 200 forms at once, and the panel says when more were alive. |
| Each validation and its outcome | Instrumentation | OnValidationRequested. BevTools moves its handler to the end of the list at the first validation, so from the second on the outcome is read right after every validator ran and before OnValidSubmit runs; the first is read once the work yields. Recorded on the timeline with its trigger. |
| Validation messages | Available | GetValidationMessages per field, redacted; hidden under ValueAccess None, since a parse error can repeat what was typed. |
| What validates a form, and what shows its messages | Instrumentation | The handlers of OnValidationRequested (DataAnnotationsValidator, a custom validator), without BevTools' own and without the framework's form mapping (an EditForm with a FormName on a server subscribes to report form-post binding errors, which checks no attribute); ValidationSummary and ValidationMessage<T> among the handlers of OnValidationStateChanged (inputs subscribe too, for their CSS class, and are not displays). |
| Rules | Available | forms.validation: the model has validation attributes and nothing validates the form (Validate() and OnValidSubmit always pass); a validation failed and no ValidationSummary or ValidationMessage listens. |
| Information | Status | How / why |
|---|---|---|
| Current route: page, route values, query, layout | Available | From the RouteView's public RouteData and NavigationManager; values through the inspector, the URI redacted. Without a RouteView in the session (the router runs during static rendering, per-page interactivity) only the URI is known, and the panel says so. |
| Matched template | Partial | RouteData.Template when the router fills it in (.NET 10; not every router does, the demo's interactive one does not); otherwise inferred only for a page with a single @page, and labeled inferred. |
| Route table with authorization, render mode, layout | Available | The Router's AppAssembly and AdditionalAssemblies, read as the router reads them (exported types with [Route]); [Authorize] read by name. Without a router: the assemblies of the pages the session rendered, labeled so. |
| Navigation guards | Instrumentation | NavigationLock components (whether they confirm leaving the site, which callback runs before internal navigation) and the handlers in NavigationManager._locationChangingHandlers, each with the component that registered it. Read only: registering a LocationChanging handler of BevTools' own would make the browser ask .NET before every navigation. |
| What each navigation rendered | Available | The renders each navigation caused - every render event names the event that triggered it, and on .NET 10 a navigation is one - so later clicks and timers are not counted. Where the runtime does not report navigations as triggers, the renders from one navigation until the next, and the column's tooltip says which it is. Needs render events (RecordRenderEvents). |
| Redirect loops and navigation storms | Available | navigation.loop: at least six navigations alternating between two destinations within 10 s, or ten within 2 s - counting only navigations code made (NavigateTo), not a click on a link, a back/forward or one made from a UI event, so a person browsing a list and its details or a search box that writes the query string is not a loop. Read from the last five minutes of navigations by their own times, so a loop that ran while the panel was closed is found when it opens. How each navigation came about is noted from LocationChanged even when the router's activity recorded it (.NET 10). |
Cancelled navigations, forceLoad, NotFound |
N/A | No event exists for a cancelled navigation without registering a LocationChanging handler (see guards); a forced load ends the page; subscribing to OnNotFound could change what happens when nothing else handles it. |
| Authentication state, roles, claims, changes | Available | The scope's AuthenticationStateProvider (by name) as a built-in state provider; AuthenticationStateChanged records changes with diffs. Asking for the current state is BevTools' own work, so a provider's API call meanwhile is not recorded as the app's. Claim values follow ValueAccess; sensitive claim types and token-shaped values are redacted. |
| Information | Status | How / why |
|---|---|---|
Requests via IHttpClientFactory: method, URL, status, duration, sizes, headers |
Available | IHttpMessageHandlerBuilderFilter adds a DelegatingHandler to every pipeline, innermost so each retry is its own record; the issuing session is resolved at the outer end, so attempts made after another handler left the renderer's context are still attributed. Sensitive headers and query/fragment values redacted. |
Hand-built HttpClient |
Integration | Add BevToolsHttpMessageHandler manually. |
| The trace a request carried to the server (OpenTelemetry) | Available when the client propagates one | HttpClient's diagnostics handler writes the W3C traceparent header in the primary handler, below BevTools' handler, so it is read from the request once it completes: its trace id is what the server's spans and logs are filed under in a tracing backend, its parent id the client span. On a server the client propagates while an activity is current - a request made in a UI event carries one (measured on the demo's circuit: GET /api/orders from onclick → Orders.Load), one made from OnInitializedAsync with no activity and nothing listening to System.Net.Http does not; the browser's HttpClient propagates nothing by default. Only a well-formed header counts. Shown in a request's detail (logs in it filters the Logs panel to the same trace; a log entry's requests in it the other way), filterable (trace:), exported (traceId, spanId) and in the companion. |
| Bodies | Opt-in | CaptureHttpBodies (off): text bodies only (JSON, XML, forms, text, GraphQL), at most MaxHttpBodyBytes (16 KB), redacted at capture - a JSON or form value under a sensitive name, anything credential-shaped. Never at the cost of the request: a request body is read only when it is byte-array content (strings, forms) or JSON content (serialized into the buffer the send then uses, once), never a stream; a response body only when its length is known and within the limit (buffered as HttpClient would, so the app reads the same bytes), never a chunked body or server-sent events. Honoured only at ValueAccess Full, and in a process that serves many users only in Development; About says when it is set but not honoured. Each body that was not read says why. Shown in a request's detail, exported, in the companion. |
| gRPC-Web, server-sent events, GraphQL | Partial | Told apart by content type and path (application/grpc*, text/event-stream, /graphql); a GraphQL operation is named from its body when bodies are captured. A request's duration is to its response headers - the body is read above BevTools' handler - which for a stream is when it started, not when it ended. |
| Originating UI event | Partial | Linked when a HandleEvent activity is executing; lifecycle/timer requests show no trigger. |
| Component that sent a request | Available | Read from the stack when the request enters the pipeline: the nearest component method, and the application method between them - OrderRow.OnInitializedAsync via ProductApi.GetOrderDetailsAsync. Async methods are named through their state machines (on the stack again after every await), local functions by themselves, lambdas by the method they are in; framework and BevTools frames are passed over. In an IHttpClientFactory pipeline it is read at the outer end, once per request, so a retry handler that awaits before sending does not lose it and every attempt carries it; a hand-built client's BevToolsHttpMessageHandler reads it itself. A request sent by code no component method is waiting on synchronously - a timer, a background loop, a service that awaited something of its own before sending - has no component on its stack and says so, with the reason: nothing on the stack; a continuation that another task's completion ran (on WebAssembly, without a synchronization context, completing a task runs what awaited it on the completer's stack, so the walk stops there rather than name the completer); none within the 256 innermost frames; or a stack that cannot be read (a trimmed or AOT build). A generic component is named by its definition (GenericRow<TItem>): a stack frame does not say which instantiation ran. Shown as Sent from in a request's detail, in the Trigger column when no UI event triggered it, per endpoint (the top sender, and how many others in all), in http.per-item's evidence, the request filter, exports (caller or callerMissing; callers and callerCount per endpoint) and the companion. One stack walk per recorded request (CaptureHttpCallers, on by default; none while capture is paused; see performance.md). |
Cancelled requests and HttpClient.Timeout |
Partial | A request whose own token was cancelled is shown as cancelled, not as a failure: by the caller, HttpClient.Timeout or a handler in the pipeline (a resilience attempt timeout, hedging) - inside the pipeline these look the same, and BevTools, innermost, sees each attempt. Any other cancellation (a connect timeout's TaskCanceledException) is a failure, recorded in Errors with its inner exception's message. |
| Bursts to one endpoint (a request per keystroke) | Available | http.burst: one endpoint (method, path, query parameter names) - or one endpoint template whose requests each come from their own UI event - called HttpBurstCount times within HttpBurstWindowMs with different query values; the evidence names the triggering UI events and how many requests started before the previous one completed, which is when an older response can arrive last and overwrite a newer result. Exact repeats stay http.duplicate's. |
| Requests per endpoint | Available | Network → Endpoints: one row per method, path and query parameter names, with the path segments that are ids - numbers, GUIDs, hexadecimal strings of 16 characters or more - as {id} (GET /api/orders/{id}; /api/orders/recent stays itself). Requests, failures, cancellations, p50/p95/max of the completed requests (nearest rank), bytes received (a total only when every response reported its size - a chunked body does not - else –), the last one; selecting an endpoint lists its requests. Exports carry the same as httpEndpoints; the Markdown report lists the busiest ten. |
| Sorting | Available | The request list (every column but Trigger) and the endpoints, by buttons in the headers (aria-sort); selecting the sorted column again reverses it; ties newest first. A request that failed without a response sorts above every status. The Profiler's and the Errors panel's headers work the same way. |
| One request per list item (N+1) | Available | http.per-item: one endpoint template called for HttpPerItemCount (5) different ids within HttpBurstWindowMs, from one UI event or none - typically each row's OnInitializedAsync. When most of the requests come from UI events of their own, they are a lookup typed into a box, and http.burst reports them instead. Both rules slide one window over the requests (500 requests: about 1 ms). |
| WebSocket / SignalR traffic | Instrumentation | Counted in the browser by direction and SignalR message kind (JSON and MessagePack/blazorpack envelopes; arguments skipped, never kept); for a circuit, render batch sizes and the browser's time to apply them; BevTools' own reports counted apart. A socket that existed before BevTools attached is seen from its next send. BevTools' own messages are counted apart: its reports, its calls into its own module (exports handed to the browser), and render batches that changed only its panel (told by where their DOM changes landed); a batch that changed the app and the panel together counts as the app's. |
| Browser-level waterfall, WebSocket payloads | N/A | Use the browser's DevTools. |
| Information | Status | How / why |
|---|---|---|
| .NET → JS from components (identifier, duration, args types, result, errors, component) | Instrumentation | [Inject] IJSRuntime is replaced with a tracking wrapper on the first render — the earliest point after the framework performs property injection. Calls made from OnInitialized/OnInitializedAsync therefore run before the wrapper exists and are not attributed. During prerendering, and on Blazor Server until the browser has connected, the runtime is left alone: it cannot make calls, and libraries recognize it by its type. Whether it can is read from host internals (RemoteJSRuntime.IsInitialized, WebViewJSRuntime._ipcSender); where a framework version lacks one, that runtime is wrapped regardless and the About panel says so. Module references obtained through a wrapped runtime are wrapped too (TrackJsModuleReferences, on by default); BevTools unwraps them again when they are passed back as arguments through a tracked runtime, but cannot do so for references nested inside other arguments — turn the option off if a module reference must round-trip through code BevTools does not see. On .NET 10, InvokeConstructorAsync, GetValueAsync and SetValueAsync (and their in-process forms) go through the wrapper too and are recorded as new X, get X and set X. |
| .NET → JS from services | Integration | jsRuntime.WithBevToolsTracking(session). Global decoration is unsafe (framework casts IJSRuntime to RemoteJSRuntime/WebAssemblyJSRuntime). |
| JS → .NET | Instrumentation | DotNet.invokeMethodAsync/invokeMethod wrapped by the browser bridge; reported in 500 ms batches. |
| Information | Status |
|---|---|
| Render exceptions with component | Available (render wrapper) |
| Event handler / circuit / ErrorBoundary / JS interop failures | Available (Blazor's own logger categories at Warning+) |
| Exceptions of tasks nobody awaited (fire-and-forget) | Available in single-user hosts: TaskScheduler.UnobservedTaskException, which the runtime raises when such a task is collected, on the finalizer thread - so it reaches the only session of WebAssembly or Hybrid, and nobody's in a server process, where nothing says whose work it was. Source task; never marked observed, so the app's own handling is unchanged. A task BevTools started is left out: the outermost frame below the task machinery (the async method whose task it is, the lambda Task.Run ran) decides, so an application's call that only passed through a BevTools wrapper - a tracked IJSRuntime - stays the application's. |
| Errors the application caught and logged | Available: an application log entry at Error or above (source log), its exception's type and the logged message; the same exception reported by another path too is one error. Needs TrackLogs (on by default) |
| JS errors, unhandled promise rejections | Instrumentation (browser bridge) |
| Resources that failed to load, CSP violations | Instrumentation (browser bridge: capture-phase error on elements, securitypolicyviolation), URLs redacted in the browser |
| HTTP failures | Available |
| Preceding UI event and last activity | Available |
| Source location | Available with symbols: the stack trace's "in file:line"; when the text has none (a WebAssembly runtime without symbols loaded, as in every browser Blazor does not debug in), the first application frame's method and IL offset mapped through the app's PDB, downloaded on WebAssembly - shown once it arrives. Grouping keeps using the stack text, so it never depends on when the symbols arrived. |
| One problem with different values in its messages | Available: Errors → Groups. A message template replaces what identifies an instance - GUIDs, ISO timestamps, hexadecimal strings of 16 characters or more, quoted values, the ids in addresses (absolute URLs and "GET /path" alike, as the Network endpoints; hosts and ports stay), numbers of four digits or more - and keeps what names a failure (404, 100 seconds, 10.0.1, localhost:5001). Errors are one group when source, type, template and where they were thrown agree, whatever the component instance. Where is the source location (file and line, never templated), else the first application frame of the stack: a JavaScript stack's opening message line and the frames of System, Microsoft and the framework's scripts are passed over. With no application frame at all (a framework-only stack, a browser error without a stack), quoted values stay in the template, since they are then what tells two failures apart. Per group: occurrences, records, different messages, components, first and last seen; selecting one lists its errors. error.repeated counts by group ("…Order {n} not found occurred 3 times with 3 different messages"); exports carry messageTemplate and errorGroups, the report lists the groups with more than one message. Repeats of the exact same error within a minute still collapse into one record with a count. |
| Information | Status | How / why |
|---|---|---|
| What the application's loggers write: level, category, event id and name, message, template and its values, scopes, exception | Available | An ILoggerProvider registered with BevTools (alias BevTools, so Logging:BevTools:LogLevel configures it alone) records each entry at MinimumLogLevel (Information by default) or above that the host's logging configuration let through, in a ring buffer (MaxLogs, 1000), with session-long counts per level. A structured entry's values ({OriginalFormat} and its pairs: LoggerMessage, the source generator, LogInformation("… {Count}", n)) are read through the inspector at the session's value access, redacted by name and by shape; a value redacted by its name is redacted in the message as well, which is then rebuilt from the template. Under Shallow the message is rebuilt from scalar values (an object shows its type, never its ToString); under None it is not kept and the template stands for it, when it has holes. A plain string logged as it is is its own template, redacted like the message - and its values with it when it was interpolated, so under None it is not kept either («hidden»). A hole named twice takes the value in its place, as the logger fills it. Key/value scopes (ASP.NET Core's request scope, a dictionary) are read pair by pair (keys only under None); at most 20 values and 8 scopes per entry. BevTools' own work through application code (SessionResolver.BeginBevToolsWork) and what a value's ToString logs while it is captured are not recorded. |
| Whose entry it is | Available | The session whose renderer's synchronization context or dispatcher is current; else the session whose UI event or navigation the current Activity descends from - the activity flows with the execution context, so an entry written after a library's ConfigureAwait(false) or in a task a handler started still belongs to that circuit; else, in a single-user host (WebAssembly, Hybrid), the only session. In a server process an entry written outside every session (the request pipeline, a hosted service, a lifecycle method's continuation off the dispatcher with no event above it) is nobody's: no guess about another user's data. |
| The component and UI event that logged | Available | The component method on the stack (a logger is called synchronously, so the caller is there unless the code went asynchronous first) and the application method between them: ProductRow.OnParametersSet, OrderRow.OnInitializedAsync via OrderService.Load. One stack walk per recorded entry (CaptureLogCallers, on by default). The UI event or navigation running then is linked (During) and is the entry's parent in the timeline. |
| Trace correlation | Available | The W3C trace and span id of the Activity current when the entry was written: what an OpenTelemetry backend files the server's spans and logs under, so an entry can be found there. Shown, exported and filterable (trace:). |
| Timeline | Available | Each entry is a Log event (an application entry at Error or above, or a Blazor warning, is the error's event instead, so nothing is recorded twice). Not recorded while capture is paused; an error still is. |
| One template logged over and over | Available | log.flood: one category's template (whatever its values) written LogFloodCount (100) times within ten seconds, with the senders - logging on a hot path, such as a render or a list row. |
| Panels and frontends | Available | Logs (entries newest first, filter level: - a minimum - category:, event:, trace: and text; Categories with warnings and failures per category; an entry's template, values, scopes, sender, UI event, trace and its error), the palette's search, exports (logs, logCounts), the Markdown report (warnings and errors), the performance trace (a Logs track), the companion and the extension. |
| Database commands (Entity Framework Core) | Available, from EF's logs | No reference to EF and no interceptor: the entries of Microsoft.EntityFrameworkCore.Database.Command at Information, events 20101 (executed) and 20102 (failed), whose values carry the SQL and the elapsed milliseconds (formatted with the current culture's separators; only digits are read). The SQL is kept at Full and Shallow access, redacted, and left out under None (the time is kept). Logs → Database: one row per SQL (whitespace collapsed) with executions, total, p95, max, failures and senders; a row opens its executions. db.per-item (DbPerItemCount, 10 within two seconds: N+1) with the senders and the UI event; db.slow (SlowDbCommandMs, 500). A failed command's error entry is an error. Exported (dbCommand per entry, dbCommands), in the companion. Needs that category logged at Information; on a server, a command a lifecycle method awaited after leaving the dispatcher is nobody's (see Whose entry it is). Verified against EF Core 10 and 11 on SQLite in the tests. |
| Entries before BevTools attached, or written by the host outside a session | N/A | Providers receive entries as they are written; nothing is replayed. |
| Information | Status | How / why |
|---|---|---|
| Registrations, lifetimes, implementation, keyed | Available | IServiceCollection captured at AddBitBevToolsServices time (enumerated lazily, so later registrations appear). |
| Constructor dependencies, consumers, cycles, captive dependencies, large graphs | Available | Static analysis of implementation constructors. Factory/instance registrations have no visible dependencies. |
| Runtime resolutions | N/A | The container has no resolution events. |
| Instance values | N/A | Never read (configuration/secrets). |
| Information | Status | How / why |
|---|---|---|
| Lifecycle, connection up/down, disconnect count, duration | Instrumentation | CircuitHandler registered by AddBitBevToolsServerServices. |
| Inbound message count and processing time (slow messages flagged) | Instrumentation | CircuitHandler.CreateInboundActivityHandler. |
| Active circuits in the process (aggregates only) | Available | Singleton registry; ids truncated to 8 characters; only lifecycle counters of other circuits are visible — their components, events and state stay private to their own sessions. On a shared development server this still means other developers' circuit activity is visible; the panel says so. |
| Reconnect attempts | Instrumentation | Blazor's components-reconnect-state-changed event, which it dispatches - without bubbling, so the hub listens in the capture phase - on the app's own reconnect dialog (#components-reconnect-modal, in the .NET 10 templates). The states of a dropped connection (show, retrying) cannot reach a circuit while it is down: the hub holds them and delivers them, each with its age, when it is back. With Blazor's default dialog there is no event, and only the circuit's Connection down and Connection up (from the CircuitHandler) are recorded. |
| Message sizes, render batch sizes, the browser's time to apply a batch | Instrumentation | Counted in the browser bridge (Network → WebSockets), not on the server. |
| Network latency, payloads | N/A | Not exposed by the framework; payloads are never read. |
| Information | Status |
|---|---|
| Online/offline, visibility, user agent, JS heap (Chromium), navigation timing, resource count | Instrumentation (bridge) |
| Enhanced navigation (Blazor Web App) | Instrumentation (Blazor's enhancedload event) |
| Several BevTools runtimes on one page | Available: the page hub keeps one target per runtime; page facts reach all, JS → .NET calls the runtime that received them |
| localStorage / sessionStorage inventory (on request) | Instrumentation (bridge). Values are redacted in the browser when the key matches a sensitive pattern or the value looks like a token (JWT shape), so a secret never reaches the .NET side at all. |
| Cookies, IndexedDB, Cache Storage, service workers, the origin's quota (on request) | Instrumentation (bridge), Browser → Read storage. Structure and sizes only: cookie names and value lengths - values are never read, a cookie is where a session id lives, and HttpOnly cookies are invisible to the page anyway; IndexedDB databases by name and version where the browser can list them (indexedDB.databases()), never opened, so an upgrade the app runs is never blocked; Cache Storage caches with how many requests each holds, never a response; service worker registrations with their active, waiting and installing scripts (URLs redacted) and the one controlling the page; navigator.storage.estimate() usage, quota and usage per kind (Chromium), and persisted(). Each part that fails or times out (3 s) says why instead of failing the read. Exports carry the same (browser.storage). pwa.update-waiting: a service worker installed and waiting - the page runs the previous version until every tab closes. |
| IndexedDB contents, generic DOM events | N/A by design (values; use the browser's DevTools) |
| Interaction latency: each click, tap or key press to the next paint, as input delay, processing and presentation; the session's Interaction to Next Paint | Available where the browser has Event Timing (Chromium, Firefox, current WebKit; the Browser panel says when it has not). The hub observes event entries (threshold 16 ms, the lowest the API takes) and merges the entries of one interaction - pointerdown, pointerup, click - into one: from the first input to the paint after the last, its processing spanning every handler, named after the event whose handlers took longest; the element is described by tag, id and class, never by its text. Interactions on BevTools' own UI are left out. Reported once a second with their ages (the page's clock is not the runtime's); on a circuit an entry is dated as the report's arrival less its age, so the report's way to the server puts it that much later - milliseconds, against the minute or two the rules look back over. An interaction whose entries two reports carried is put back together by its interactionId. Each of 50 ms or more is a timeline event. INP is the slowest interaction ignoring one in every 50, as the metric is defined, over what BevTools saw (the newest 10,000 interactions; the export says over how many, interactionToNextPaintOver). On WebAssembly - .NET runs on the browser's main thread - the UI event whose handler started inside the interaction's processing is named (Ran; the timeline links it), when exactly one did, within 4 ms; on a circuit the handler runs on the server after the browser has painted, so the time is the browser's part only and nothing is matched (the circuit's own timing is its inbound message's). The browser rounds durations to 8 ms. browser.slow-interaction (SlowInteractionMs, 200): per kind of interaction and element, with the part that was slow. TrackResponsiveness (on). |
| Long frames: the main thread busy past a frame, with what ran in it | Available in Chromium (long animation frames: duration, blocking time, the three longest scripts with invoker, file and function); Partial where only long tasks exist (durations, no scripts); N/A in Firefox and WebKit, which report neither. On WebAssembly the UI events, navigations and render batches that started inside the frame are named. browser.long-frame (LongFrameBlockingMs, 150): the frames of the last minute that blocked past it, with the longest. On WebAssembly a frame at least half of which was BevTools' own panel rendering is not recorded at all - not in the timeline, the totals or the export (its cost is in About → overhead); a frame with less of it says how much - or the least it spent, when the panel rendered more often around the frame than the 256 renders BevTools keeps count of. Frames without script attribution say only that. |
| Information | Status | How / why |
|---|---|---|
Automated checks (Browser → Accessibility, a11y.page) |
Available, on request | In the browser: controls and links with no accessible name (4.1.2), form fields with no label - a placeholder is not one (1.3.1, 4.1.2), images with no alt (1.1.1), focusable content inside aria-hidden (4.1.2), tabindex above 0 (2.4.3), for/ARIA references to duplicated ids (4.1.2), empty headings (2.4.6), the page's language (3.1.1). Hidden elements are skipped. Each finding names the component that rendered the element (the path mapping of picking) or says it is static markup, and Show outlines it. Elements are described by tag, id or class, never by their content. Names are computed from what a screen reader reads - aria-hidden and hidden parts left out, shadow roots walked, an aria-labelledby target's own aria-label used - so an aria-hidden glyph is no name and a design system's shadow-DOM button text is one. hidden-focusable reports only what a keyboard reaches (not tabindex="-1", hidden or inert content). A placeholder-only field is reported on purpose although browsers use the placeholder as its name (it disappears as soon as something is typed). An element whose component could not be looked up says not found, not static markup. The findings are dropped when the page navigates and on Clear. |
| Contrast, focus order, meaningful names, screen-reader behaviour | Manual | Not decidable from markup without false alarms; the panel says so. Use the browser's accessibility tools and a screen reader. |
| BevTools' own UI | Tested | Named controls on every tab (a bUnit test across all tabs; the same page check run on BevTools' own UI in a real browser finds nothing), a labelled region, a tab list with arrow keys/Home/End and one tab stop, focus moved into the panel when it opens and back to the badge when it closes (Escape closes it), the palette a dialog with a combobox and a listbox, announced notifications, AA contrast for the event-kind chips in both themes, forced-colors and prefers-reduced-motion support. Escape in the command palette closes the palette, not the panel; Tab stays inside it; closing it returns focus to its opener, running a command to the active tab, and a link that shows another tab moves focus to that tab (only from inside the panel, never out of the app). Escape in a filter input closes the panel, as anywhere in it. |
| Information | Status |
|---|---|
| Overhead: bookkeeping per render, UI render time, inspection, diagnostics, tree refresh | Available (About panel) |
| Value access level | Available. Full, Shallow or None (BitBevToolsOptions.ValueAccess); None by default outside Development in a process that serves many users. Shown in the Runtime panel and in every export. |
| Performance trace | Available. BevTraceExporter writes Chrome Trace Event JSON for ui.perfetto.dev and the Chrome DevTools Performance panel. |
| Companion window, browser extension | Available. The same frontend over BevToolsProtocol, through the page hub; the extension needs no permission and no content script. Network lists the endpoints with their senders and each request's Sent from; Errors lists the groups that merged more than one message. A batch's cost tree and time axis are asked for by batch (protocol batch) on a live page; renders per page are in-app only. Network shows bodies and protocols when captured, Logs the database commands. |
| Runtime facts per platform | Available. Server process (thread pool, lock contention, working set, GC mode: shared by every circuit), WebAssembly runtime (JIT or not, threads, metrics support, globalization), .NET heap and collections. |
| Pause / resume capture | Available. While paused no render, timeline, HTTP, JS interop or state-change record is written; errors are still collected, failed HTTP requests and JS interop calls included (their timeline link says the capture was paused). Renders that happen while paused are counted as skipped so the next recorded render keeps its correct cause. |
| Profiler baseline ("since reset") | Available. Raw counters keep running; the profiler, inspector and export show the delta since the reset on request. |
| Export | Available. BevSessionExporter.ToJson (bit-bevtools/1) and ToMarkdownReport contain exactly what the panels show, with the same redaction; browser storage previews are omitted. The rules run before anything is written when anything was captured since they last ran, so the findings are up to date with the rest of the file. That cannot recover what a rule's window (most look back 10 to 60 seconds) no longer reaches: by default the rules run only while the panel is open, a frontend reads the session or it is exported, so for a run that is exported later and checked, set Diagnostics.EvaluateWhileClosed and they run as data arrives (at most once a second, with the panel component on the page, open or not). Call the exporter on the session's dispatcher. |
| AI coding agents (MCP) | Available, over exports. bevtools mcp <export.json>… is a Model Context Protocol server over stdio that serves the exports it was started with and nothing else - no running app (that needs an authenticated transport the page hub deliberately does not offer), no other file, no writes. Tools: list_sessions, get_findings, get_errors, get_error_groups, get_slowest_components, get_component, get_http, get_http_endpoints, get_timeline, get_logs, check, compare; every answer bounded and saying what it truncated. Exports are redacted at capture, so an agent sees what the panels show. |
| CI gate | Available. BevSessionCheck.Check(json) reads an export's findings: failing at a severity (warnings by default; null lists only), with rules to ignore, dismissed findings counted but never failing, and recorded errors failing only when asked; an export without its diagnostics section - or without its errors section when errors are to fail it - is refused, not passed. Its caveats say what limits a pass: capture paused at export, disabled rules, rules that ran only when someone looked (EvaluateWhileClosed off). The bevtools .NET tool (Bit.BevTools.Cli, published with the other packages) runs it and BevProfileComparison over files - bevtools check, bevtools compare - writing Markdown, JSON, SARIF 2.1.0 or GitHub Actions annotations (--format), and exiting 1 on a failure, 2 on input it cannot read; compare takes the allocation thresholds too. It reads files only, never a running app. |
| Session isolation | Available. One session per circuit/app. Ambient activity (HTTP, logs, metrics) is routed by the renderer's synchronization context, then by Dispatcher.CheckAccess(), then by the UI event or navigation the current Activity descends from (work that provably began in that session's event and left its dispatcher). In a server process (any ASP.NET Core host: Blazor Server circuits, static SSR, prerendering) there is deliberately no "only live session" fallback, whatever the session's platform reports: attributing background work to whichever circuit happens to be connected would be a guess, and a guess about another user's data. |
When BevTools is opened on demand (Bit.BevTools.Loader, BitBevTools.Attach) instead of registered at startup, the
About panel says which of these applied. Everything from before the attach is missing either way.
| Information | Through Bit.BevTools.Loader | Through a bare BitBevTools.Attach |
|---|---|---|
| Components that already existed | Instrumentation: read from the renderer's state map; one that already rendered is not reported as rendering for the first time | Same |
| Components created after | Available: the activator hook the loader installed at startup | Instrumentation: the renderer's IComponentActivator is replaced through reflection |
| UI-event render causes, lifecycle timings | Runtime: the loader registers the framework ActivitySource and meters at startup | Runtime, only if the app registered them at startup (AddComponentsTracing, AddComponentsMetrics) |
IHttpClientFactory requests |
Available: the loader's pass-through handlers | N/A: those pipelines were built without BevTools, unless the app installed the loader's hooks |
The HttpClient registered in DI |
Available when its pipeline holds BevToolsHttpHookHandler, or it comes from IHttpClientFactory; otherwise as with a bare attach |
Instrumentation: its handler is wrapped through reflection, unless it is transient |
| DI registrations | Available: the service collection the loader kept | N/A: the container was built before the attach, unless the app installed the loader's hooks |
| Circuit diagnostics | N/A | N/A |
| End of the session | With the scope (circuit) | Same |
| Enabled in a process that serves many users | Development, or AllowInMultiUserHost |
Development, or BitBevToolsOptions.Enabled = true |