Blazor Web App
A Blazor Web App has two projects and up to four ways of rendering a page - Server, WebAssembly,
Auto and static SSR. Belemetry is set up the same way for all of them: one registration in each
project, <BelemetryScript /> in the document head, and
<BelemetryInitializer /> in Routes.razor. What differs between the
render modes is where the .NET half runs, and that is decided for you.
Install
One package, referenced by the project that renders App.razor (the server) and by
the client project when there is one. It brings the browser script with it as a static web
asset, at _content/Bit.Belemetry/bit-belemetry.js.
dotnet add package Bit.BelemetryRegister in both projects
Call AddBitBelemetryServices in the server's Program.cs and in the
client's. Every component is instantiated twice - once on the server for the prerender pass and
again in the browser - so whatever a page injects (IBelemetry,
IBelemetryConsent) has to resolve in both containers. The call is idempotent and
follows the Options pattern: every configure callback, across calls, runs in order on the same
BelemetryOptions, which makes a shared configuration method the natural way to keep
the two in step.
// In the client project, so both Program.cs files can call it.
using Bit.Belemetry;
public static class AppTelemetry
{
public static void Configure(BelemetryOptions options)
{
options.ServiceName = "MyApp";
options.AddOtlpExporter("https://otel.example.com");
options.Consent.Default = BelemetryFeatures.None; // opt-in until the user decides
options.Traces.Sources.Add("MyApp*");
options.Metrics.Meters.Add("MyApp");
}
}
// Server/Program.cs
builder.Services.AddRazorComponents()
.AddInteractiveServerComponents()
.AddInteractiveWebAssemblyComponents();
builder.Services.AddBitBelemetryServices(options =>
{
AppTelemetry.Configure(options);
options.Environment = builder.Environment.EnvironmentName;
});
// Client/Program.cs
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.Services.AddBitBelemetryServices(options =>
{
AppTelemetry.Configure(options);
options.Environment = builder.HostEnvironment.Environment;
});
await builder.Build().RunAsync();
Each container has its own job:
| Container | What Belemetry does there |
|---|---|
| Server |
Builds the browser script's configuration from its options and writes it
into the page with <BelemetryScript /> - so the exporters, the
consent defaults and every options.Browser.* setting the browser sees come
from the server registration. Resolves IBelemetry and
IBelemetryConsent during prerendering and for interactive Server
circuits. Collects the server's own .NET telemetry only if you opt in (below).
|
| Client |
Runs the .NET pipeline in the browser: the WebAssembly app's ILogger,
Activity and Meter records are collected and exported from
there, and flushed with keepalive when the page unloads. Its metrics rely on
System.Diagnostics.Metrics, which the WebAssembly SDK turns off by default and the
Bit.Belemetry package turns back on (MetricsSupport).
|
The script in App.razor
<BelemetryScript /> renders the configuration as an inline JSON element and the
module after it. Because it is static markup in the head, the script starts before Blazor boots -
it sees the initial page view, the web vitals and errors thrown while the runtime downloads - and
it also covers static SSR pages that never become interactive.
@* Server/Components/App.razor *@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<base href="/" />
<BelemetryScript />
<HeadOutlet @rendermode="InteractiveAuto" />
</head>
<body>
<Routes @rendermode="InteractiveAuto" />
<script src="_framework/blazor.web.js"></script>
</body>
</html>
With MapStaticAssets and fingerprinted assets, pass the fingerprinted path:
<BelemetryScript Src="@Assets["_content/Bit.Belemetry/bit-belemetry.js"]" />.
Under a Content Security Policy, pass Nonce; it is added to both script elements.
The initializer in Routes.razor
<BelemetryInitializer /> connects the script to .NET once the app is interactive:
consent and the session flow both ways, browser records can be relayed to .NET, and the WebAssembly
pipeline gets its unload flush. It renders nothing. Place it once, in Routes.razor or
the main layout, inside the interactive part of the app.
@* Client/Routes.razor *@
<Router AppAssembly="typeof(Program).Assembly">
<Found Context="routeData">
<RouteView RouteData="routeData" DefaultLayout="typeof(Layout.MainLayout)" />
</Found>
</Router>
<BelemetryInitializer />Per render mode
The setup above does not change between render modes. What changes is where .NET telemetry is
collected and how IBelemetry calls travel:
| Render mode | Browser instrumentation | .NET logs, traces, metrics | IBelemetry and consent |
|---|---|---|---|
| InteractiveWebAssembly | The script, from the head | In the browser, exported by the client registration | Through the WebAssembly pipeline; consent is app-wide |
| InteractiveServer | The script, from the head | Opt-in: the server pipeline | Routed through the circuit's browser, so that user's consent applies |
| InteractiveAuto | The script, from the head - one script for the whole visit | On the server while a page runs on a circuit (opt-in), in the browser once Auto switches to WebAssembly | As Server, then as WebAssembly |
| Static SSR | The script, from the head | Opt-in: the server pipeline | No interactivity: consent is given in the browser, through window.BitBelemetry |
On a static SSR page there is no circuit and no WebAssembly runtime, so
IBelemetryConsent has nobody to talk to. The script is still running, and a plain
HTML banner can record the choice with its JavaScript API - the same
bit-belemetry-consent entry the .NET API writes, so an interactive page later reads
the same decision:
The server pipeline
On an ASP.NET Core host Belemetry's .NET pipeline is off by default - such hosts usually have an
OpenTelemetry setup of their own - and the server registration only renders and serves the script.
Turn it on to collect the server's logs, traces (including the Microsoft.AspNetCore
request spans) and metrics with the same exporters. It is process-wide, serving every user at
once, so it follows Server.Features - an operator decision - and not any one user's
consent. With Server and Auto, this is also where an interactive circuit's ILogger
output is collected.
builder.Services.AddBitBelemetryServices(options =>
{
AppTelemetry.Configure(options);
options.Server.CollectDotnetTelemetry = true;
options.Server.Features = BelemetryFeatures.Errors | BelemetryFeatures.Logs
| BelemetryFeatures.Traces | BelemetryFeatures.Metrics; // the default
});
Browser fetch spans add a traceparent to same-origin requests, so the server's
request span becomes a child of the browser's span and the two land in one trace. More on the
Server pipeline page.
What this site does
This documentation site is a Blazor Web App in the InteractiveWebAssembly render mode, prerendered,
and it is set up exactly as above: AddBitBelemetryServices in both projects through a
shared DemoTelemetry.ConfigureShared, <BelemetryScript /> in
App.razor, <BelemetryInitializer /> in Routes.razor,
and the server pipeline turned on. It exports OTLP/JSON to itself, and an in-process receiver
hands each reader their own records back.
Answer the consent banner, then open Live telemetry: the page views, clicks, web vitals and .NET logs you produce on this site arrive there within a couple of seconds, with the demo's real configuration printed underneath.
API
BelemetryScript
Component
Renders the browser script with its configuration inline, so it starts as early as possible - before Blazor boots, and on static SSR pages that never become interactive. Place it in the <head> of App.razor.
| Name | Type and default | Description |
|---|---|---|
Src |
string |
The script URL. Defaults to _content/Bit.Belemetry/bit-belemetry.js; set it to the fingerprinted asset path when using @Assets[...]. |
Nonce |
string |
A CSP nonce added to both script elements. |
BelemetryInitializer
Component
Connects the browser script and .NET once the app is interactive: starts the script if BelemetryScript has not, synchronizes consent and the user context, enables relaying (Hybrid) and the unload flush (WebAssembly). Place it once in Routes.razor or the main layout. Renders nothing.
| Name | Type and default | Description |
|---|---|---|
OnReady |
EventCallback |
Raised once the script is connected. |
BelemetryServerOptions
Class
The server side (Blazor Server, static SSR, the server half of Auto). By default Belemetry only serves the browser script there - an ASP.NET Core host usually has its own OpenTelemetry setup - and its .NET pipeline stays off.
| Name | Type and default | Description |
|---|---|---|
CollectDotnetTelemetry |
bool= false |
Whether the .NET pipeline (ILogger, Activity, Meter collection and export) also runs on the server. Defaults to false. |
Features |
BelemetryFeatures= BelemetryFeatures.Errors | BelemetryFeatures.Logs | BelemetryFeatures.Traces | BelemetryFeatures.Metrics |
The features the server pipeline collects when BelemetryServerOptions.CollectDotnetTelemetry is on. Defaults to errors, logs, traces and metrics. |
IgnoredRequestPaths |
List<string>= [] |
Path prefixes (case-insensitive) of requests whose Microsoft.AspNetCore server span is dropped, e.g. /health, /alive, /_framework, /_content and /_blazor for health probes and Blazor's own traffic. Matched against url.path (.NET 10) or, failing that, http.route, which on .NET 8 and 9 is all a request span carries: there, a request that matched no endpoint (a static file) cannot be matched either. The span is dropped when it ends, so a child it had (a database check behind /health) is still exported, with a parent that never arrives. Empty by default. |