Standalone WebAssembly
A standalone Blazor WebAssembly app has no server that renders its head, so there is no
<BelemetryScript />: one registration in Program.cs and
<BelemetryInitializer /> in the root component are the whole setup. Both halves -
the browser script and the .NET pipeline - run in the reader's browser.
Register in Program.cs
The host kind is detected: no ASP.NET Core hosting in the container and a browser underneath means
WebAssembly, so the .NET pipeline runs here and exports with the browser's own fetch.
The same exporters are handed to the browser script, unless an exporter sets
IncludeInBrowser = false.
using Bit.Belemetry;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("#app");
builder.Services.AddBitBelemetryServices(options =>
{
options.ServiceName = "MyApp";
options.Environment = builder.HostEnvironment.Environment;
options.AddOtlpExporter(o =>
{
o.Endpoint = new Uri("https://otel.example.com");
o.Protocol = BelemetryOtlpProtocol.HttpProtobuf;
});
options.Traces.Sources.Add("MyApp*");
options.Metrics.Meters.Add("MyApp");
});
await builder.Build().RunAsync();The initializer in App.razor
<BelemetryInitializer /> imports the script, starts it with the configuration from
Program.cs, and connects it to .NET: consent and the session are shared, and the
unload flush is wired up. Place it once, next to the router.
@* App.razor *@
<Router AppAssembly="typeof(App).Assembly">
<Found Context="routeData">
<RouteView RouteData="routeData" DefaultLayout="typeof(MainLayout)" />
</Found>
</Router>
<BelemetryInitializer />The optional script tag
Adding the module to index.html makes the browser download it in parallel with the
.NET runtime instead of after it, and puts window.BitBelemetry on the page before Blazor
starts. The script itself starts when <BelemetryInitializer /> hands it the
configuration.
<!-- wwwroot/index.html -->
<head>
...
<script type="module" src="_content/Bit.Belemetry/bit-belemetry.js"></script>
</head>
Starting late costs less than it sounds: web vitals, the document-load span, resource timing and long tasks are read from the browser's buffered performance entries, so the script sees them whenever it starts. What it cannot see is a JavaScript error thrown before it started.
Endpoints and CORS
Everything is exported from the page's origin to the collector's, so the receiver has to allow the
app's origin. For the OpenTelemetry Collector that is cors.allowed_origins on the OTLP
HTTP receiver; for the .NET Aspire dashboard, DASHBOARD__OTLP__CORS__ALLOWEDORIGINS.
A WebAssembly app's configuration is downloaded by every visitor, so exporter headers - API keys included - are public. For a vendor that needs a key, put a collector in between that adds it.
What runs where
| Browser instrumentation | The script: page views, clicks, web vitals, errors, fetch and XHR spans |
| .NET logs, traces, metrics | In the browser, exported every Batch.FlushInterval and Metrics.ExportInterval. Metrics rely on System.Diagnostics.Metrics, which the WebAssembly SDK turns off by default and the package turns back on (MetricsSupport) |
| Unload | On pagehide or when the tab is hidden, the script asks .NET for its pending records already serialized, and sends them with fetch(..., { keepalive: true }) - async .NET work does not survive the page going away |
| Offline persistence | localStorage, up to Persistence.BrowserMaxBytes (1 MB) |
| Consent | App-wide: one decision governs the script and the .NET pipeline alike |
OTEL_* variables | Ignored - there is no process environment in the browser |
HTTP calls made with .NET's HttpClient are traced by the .NET pipeline (the
System.Net.Http source is on by default) and already carry a traceparent;
the script sees that header and leaves those requests alone, so each call is recorded once.
API
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. |
BelemetryOtlpExporterOptions
Class
Settings of an OTLP exporter - the vendor-neutral OpenTelemetry protocol understood by the OpenTelemetry Collector, the Aspire dashboard, Grafana (Tempo/Loki/Mimir), Jaeger, Honeycomb, Datadog, New Relic, Elastic and many more - over HTTP (protobuf or JSON) or gRPC.
| Name | Type and default | Description |
|---|---|---|
Endpoint |
Uri |
The base endpoint; /v1/traces, /v1/logs and /v1/metrics (or the gRPC service methods) are appended to it. Defaults to OTEL_EXPORTER_OTLP_ENDPOINT when set (outside the browser), else http://localhost:4318 for OTLP/HTTP and http://localhost:4317 for gRPC. |
TracesEndpoint |
Uri |
A full traces endpoint, used verbatim instead of BelemetryOtlpExporterOptions.Endpoint + /v1/traces. Defaults to OTEL_EXPORTER_OTLP_TRACES_ENDPOINT when set (outside the browser). With gRPC it is a base address (the service method is appended), as the specification defines the variable. |
LogsEndpoint |
Uri |
A full logs endpoint, used verbatim instead of BelemetryOtlpExporterOptions.Endpoint + /v1/logs. Defaults to OTEL_EXPORTER_OTLP_LOGS_ENDPOINT when set. |
MetricsEndpoint |
Uri |
A full metrics endpoint, used verbatim instead of BelemetryOtlpExporterOptions.Endpoint + /v1/metrics. Defaults to OTEL_EXPORTER_OTLP_METRICS_ENDPOINT when set. |
BrowserEndpoint |
Uri |
The endpoint the browser script exports to, when it differs from BelemetryOtlpExporterOptions.Endpoint - typically a public, CORS-enabled route of a collector while the .NET side talks to it over an internal address. With BelemetryOtlpProtocol.Grpc this is the only way the browser exports: it posts OTLP/HTTP protobuf here, since a page cannot speak gRPC; in a WebAssembly app the .NET pipeline does the same, so the endpoint must be absolute there. |
Protocol |
BelemetryOtlpProtocol= BelemetryOtlpProtocol.HttpProtobuf |
The payload encoding. Defaults to OTEL_EXPORTER_OTLP_PROTOCOL when set (outside the browser), else BelemetryOtlpProtocol.HttpProtobuf. A value set in code always wins over the variable. |
Headers |
Dictionary<string, string>= {} |
Extra request headers (e.g. an API key). Merged with OTEL_EXPORTER_OTLP_HEADERS outside the browser. |
Compression |
BelemetryCompression= BelemetryCompression.None |
The request-body compression (for gRPC, the message compression with grpc-encoding). Defaults to OTEL_EXPORTER_OTLP_COMPRESSION when set (outside the browser), else none. |
Timeout |
TimeSpan= 00:00:10 |
The maximum time one export request may take. Defaults to OTEL_EXPORTER_OTLP_TIMEOUT (milliseconds) when set (outside the browser), else 10 seconds (the OTLP default). |
IncludeInBrowser |
bool= true |
Whether the browser script exports to this endpoint too (with BelemetryBrowserTransport.Direct). Defaults to true. |
Signals |
BelemetrySignals= BelemetrySignals.All |
The signals sent to this endpoint. Defaults to all three. |
MinimumLogLevel |
LogLevel? |
The least severe log level this exporter receives from the .NET pipeline (its own logs and the records the browser relays); plain log records below it are left out of this exporter's batches only. Events - page views, interactions, web vitals, custom events, exceptions - are not logs to a log level and always pass, as do spans and metrics. null (the default) sends every collected record. Use it when one backend should get errors only while another gets everything, as a per-provider logging level would with two logging providers. |