Skip to content

Transport

The browser script records page views, clicks, web vitals, errors and fetches - and then has to get them somewhere. options.Browser.Transport decides how: Direct, where the page posts to your endpoints itself, or Relay, where it hands its records to .NET over JS interop and the .NET pipeline exports them. The choice is about where the network call happens - and therefore about CORS and about who can see your exporter credentials.

Auto, Direct and Relay

ValueWhat happens
Auto (default) Relay in Blazor Hybrid, Direct everywhere else. The host kind is detected (or set with options.Host): WebAssembly when running in the browser, Server when ASP.NET Core hosting is registered, Hybrid otherwise.
Direct The script exports to the configured endpoints itself, with fetch (and keepalive when the page is hidden). Works on every page, static SSR included, and needs no .NET at all once the page is served.
Relay The script hands its records to .NET over JS interop, and the .NET pipeline exports them with the same exporters, processors and persistence as its own records. Needs <BelemetryInitializer />; on Blazor Server the records travel over the circuit.
builder.Services.AddBitBelemetryServices(options =>
{
    options.AddOtlpExporter("https://otel.example.com", otlp =>
    {
        otlp.Headers["x-api-key"] = builder.Configuration["Otel:ApiKey"]!;
    });

    // Auto: Relay in Hybrid, Direct everywhere else.
    // Relay keeps the API key above on the server (Blazor Server) or in the app (Hybrid).
    options.Browser.Transport = BelemetryBrowserTransport.Relay;
});

Direct: CORS and public endpoints

With Direct, the request to your collector comes from the page's origin, so the receiver must allow that origin - a browser blocks a cross-origin POST with a Content-Type of application/x-protobuf or application/json unless the preflight says yes. For the OpenTelemetry Collector that is the cors block of its HTTP receiver; for the .NET Aspire dashboard, DASHBOARD__OTLP__CORS__ALLOWEDORIGINS.

The .NET side often reaches the collector over an internal address the browser cannot. Give the OTLP exporter a BrowserEndpoint - a public, CORS-enabled route - and the script uses that instead of Endpoint. It may be relative: this site uses /otlp, so the browser always posts to the origin it was served from, behind any proxy.

Everything the script exports with is in the page. The browser configuration is written into the HTML by <BelemetryScript />, so an exporter's Headers - a vendor API key, say - are readable by anyone who opens it. For browser traffic, use a collector that needs no credentials, set IncludeInBrowser = false on the exporter that carries them, or switch to Relay. An Azure Monitor exporter with an AccessTokenProvider is never handed to the script.

Relay: no credentials in the page

With Relay, the page never talks to a collector. Each batch goes to the Belemetry.Relay callback of <BelemetryInitializer />, and from there into the same pipeline that exports .NET's own logs and traces - so processors run on browser records too, and custom exporters (an in-memory viewer, your own IBelemetryExporter) receive them. The user's consent was already applied in the page, so a relayed record is not filtered a second time.

HostWhere the relayed records are exported from
Blazor Hybrid (the Auto default) The app process. No CORS - the WebView's origin is not a web origin anyone can allow - and file persistence while offline.
Blazor Server, or a Server-rendered page of an Auto app The server, over the circuit. No CORS and no credentials in the page; the server pipeline does not have to be on for relayed records to be exported.
WebAssembly, or a WebAssembly-rendered page of an Auto app The .NET pipeline running in the browser. The export is still a request from the page, so CORS still applies and the configuration is still downloadable: Relay here buys processors and custom exporters, not secrecy.

Relay depends on an interactive component. Until <BelemetryInitializer /> attaches, the script keeps its records queued in memory; a static SSR page that never becomes interactive has nothing to hand them to. If part of your app is static SSR, keep that traffic on Direct.

This site

The demo runs as a Blazor Web App with the WebAssembly render mode and leaves Transport on Auto, which resolves to Direct: the script posts OTLP/JSON to /otlp on this same origin, so there is no CORS to configure and no credential to hide. Send an event and watch it arrive - it went from the page to the server's receiver with a plain fetch.

LiveDirect
The live feed starts once the page is interactive.

Nothing yet - send an event (it needs the Events feature granted).

API

BelemetryBrowserTransport Enum

How browser telemetry leaves the page.

Value Number Description
Auto 0 BelemetryBrowserTransport.Relay in Blazor Hybrid, BelemetryBrowserTransport.Direct everywhere else.
Direct 1 The script exports straight to the configured endpoints with fetch. The endpoints must allow the page origin (CORS).
Relay 2 The script hands its records to .NET over JS interop and the .NET pipeline exports them. Needs an interactive component (<BelemetryInitializer />); avoids CORS and keeps exporter credentials out of the page. On Blazor Server the records travel over the circuit.
An unhandled error has occurred. Reload 🗙