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 8, 9 and 10. The Runtime rows need .NET 10: on .NET 8 and 9 the framework emits none of
those activities and meters, so the About panel reports them as Requires runtime support and says "Requires .NET 10"
rather than "not observed yet" (renders inside events are then labeled StateHasChanged). The render mode name needs
.NET 9 (ComponentBase.AssignedRenderMode); on .NET 8 the renderer is inferred from the JS runtime the host provides.
| 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 (.NET 8+). 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. |
| 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. |
| 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 (.NET 8 and 9, or framework instrumentation off) the panel dates it from the render batch the router just ran. Pages are named as their navigation was (route templates on .NET 10, concrete paths before; 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) |
| 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. |
| Time travel (restoring state) | N/A | Deliberately not implemented: restoring arbitrary object graphs is unsafe. History browsing of snapshots is available. |
| 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. |
| Bodies | N/A | Never captured. |
| 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+) |
| 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 |
|---|---|---|
| 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. |
| IndexedDB, generic DOM events | N/A by design (no Blazor-specific value; use browser DevTools) |
| 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. The profiler's batch tree, time axis and renders per page are in-app only: they read every retained sample, which snapshots do not carry. |
| 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. |
| 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) runs it and BevProfileComparison over files - bevtools check, bevtools compare - writing Markdown and exiting 1 on a failure, 2 on input it cannot read. 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(). 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 |