Blazor Hybrid
In MAUI, WPF and WinForms the components run in the app process and render into a WebView. Belemetry instruments both: the script in the WebView records page views, clicks, web vitals and errors, and relays them over JS interop to the .NET pipeline, which exports them together with the app's logs, traces and metrics - and keeps them in files while the device is offline.
MAUI
Register Belemetry next to the BlazorWebView. The host kind is detected - no ASP.NET Core hosting and no browser underneath means Hybrid - and with it come the Hybrid defaults: the Relay transport and file persistence.
// MauiProgram.cs
using Bit.Belemetry;
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
builder.UseMauiApp<App>();
builder.Services.AddMauiBlazorWebView();
builder.Services.AddBitBelemetryServices(options =>
{
options.ServiceName = "MyApp.Maui";
options.AddOtlpExporter("https://otel.example.com");
options.Traces.Sources.Add("MyApp*");
options.Metrics.Meters.Add("MyApp");
});
return builder.Build();
}
There is no server-rendered head in a Hybrid app, so there is no <BelemetryScript />.
<BelemetryInitializer /> imports and starts the script once the WebView is up,
and connects it to .NET.
@* Routes.razor (or Main.razor) *@
<Router AppAssembly="typeof(MauiProgram).Assembly">
<Found Context="routeData">
<RouteView RouteData="routeData" DefaultLayout="typeof(MainLayout)" />
</Found>
</Router>
<BelemetryInitializer />Flush before the app is suspended
A mobile OS may suspend a backgrounded app and end it later without another callback, so export what
is queued as soon as the app goes to the background. IBelemetry is scoped (one per
BlazorWebView); the queue it flushes is the app-wide pipeline, so any scope will do.
// App.xaml.cs
public partial class App : Application
{
private readonly IServiceProvider _services;
public App(IServiceProvider services)
{
InitializeComponent();
_services = services;
}
protected override async void OnSleep()
{
base.OnSleep();
try
{
await using var scope = _services.CreateAsyncScope();
await scope.ServiceProvider.GetRequiredService<IBelemetry>().FlushAsync();
}
catch (Exception ex)
{
// async void: an exception here would crash the app on its way to the background.
System.Diagnostics.Debug.WriteLine($"Flushing telemetry failed: {ex}");
}
}
}WPF and WinForms
The same registration, on the service collection the BlazorWebView is given. A desktop app is not
suspended, but it does exit: flush on the way out, bounded so a missing network cannot hold the
window open. The repository's Samples/WinForms is this setup in full, and the hybrid
browser suite runs the same WebView core through a Windows Forms host.
// WPF - MainWindow.xaml.cs
public partial class MainWindow : Window
{
private readonly ServiceProvider _services;
public MainWindow()
{
InitializeComponent();
var services = new ServiceCollection();
services.AddWpfBlazorWebView();
services.AddBitBelemetryServices(options =>
{
options.ServiceName = "MyApp.Wpf";
options.AddOtlpExporter("https://otel.example.com");
});
_services = services.BuildServiceProvider();
Resources.Add("services", _services); // <blazor:BlazorWebView Services="{DynamicResource services}" ...>
}
protected override void OnClosed(EventArgs e)
{
// Run on the thread pool and wait a bounded time: the window is going away, and waiting on
// the UI thread for work that may want the UI thread back would never finish.
Task.Run(async () =>
{
await using var scope = _services.CreateAsyncScope();
await scope.ServiceProvider.GetRequiredService<IBelemetry>().FlushAsync();
}).Wait(TimeSpan.FromSeconds(5));
base.OnClosed(e);
}
}
Relay: no CORS, no credentials in the WebView
options.Browser.Transport defaults to Auto, which is Relay in
Hybrid. The script never calls an exporter itself; it hands its records to .NET, where they go through
the same consent, privacy rules and processors as the app's own records. That means:
- No CORS configuration on the collector - the app process is not a web page.
- Exporter headers and connection strings stay in the app process, never in the WebView.
- An
IBelemetryProcessorsees the browser's records too.
More on the Transport page.
Offline, in files
With the Persistence feature granted, exports that fail with a network error or a
retryable status are written to files and retried every RetryInterval, for up to
MaxAge. The consent decision is remembered in a file too, so it survives restarts even
when the app never shows a page that could keep it in localStorage.
| Option | Default |
|---|---|
Persistence.Directory | {LocalApplicationData}/bit-belemetry/{service.name} |
Persistence.MaxBytes | 20 MB; the oldest requests are evicted past it |
Persistence.MaxAge | 48 hours |
Persistence.RetryInterval | 1 minute |
Endpoints from a device
The app process exports, so the endpoint has to be reachable from the device. The Android emulator
reaches the development machine's localhost at 10.0.2.2. Outside the browser,
the standard OTEL_* environment variables and
APPLICATIONINSIGHTS_CONNECTION_STRING fill in what the code leaves unset
(UseEnvironmentVariables, on by default) - useful for a desktop app started by the
Aspire AppHost.
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. |
BelemetryPersistenceOptions
Class
Offline persistence: export requests that fail with a network error or a retryable status are kept on the device and retried later, as long as the user granted BelemetryFeatures.Persistence.
| Name | Type and default | Description |
|---|---|---|
Enabled |
bool= true |
Whether persistence is used at all. Defaults to true (still subject to consent). |
Directory |
string |
The folder the .NET side stores requests in (Hybrid and Server). Defaults to {LocalApplicationData}/bit-belemetry/{service.name}. In WebAssembly the browser storage is used. |
MaxBytes |
long= 20971520 |
The maximum size of the .NET store; the oldest requests are evicted past it. Defaults to 20 MB. |
BrowserMaxBytes |
long= 1048576 |
The maximum size of the browser store (localStorage is small). Defaults to 1 MB. |
MaxAge |
TimeSpan= 2.00:00:00 |
How long a stored request is kept before it is discarded unsent. Defaults to 48 hours. |
RetryInterval |
TimeSpan= 00:01:00 |
How often stored requests are retried. Defaults to 1 minute. |
IBelemetry
Interface
Manual telemetry from C#: custom events, page views and handled exceptions, the authenticated user, and flushing. Everything else (logs, traces, metrics, browser instrumentation) is collected automatically.
| Name | Type and default | Description |
|---|---|---|
TrackEvent(string name, IReadOnlyDictionary<string, object>? attributes = null) |
void |
Records a custom event (feature BelemetryFeatures.Events). |
TrackPageView(string? name = null, string? url = null, IReadOnlyDictionary<string, object>? attributes = null) |
void |
Records a page view (feature BelemetryFeatures.PageViews). Navigation is already tracked automatically; use this for virtual pages such as dialogs or wizard steps. |
TrackException(Exception exception, IReadOnlyDictionary<string, object>? attributes = null) |
void |
Records a handled exception (feature BelemetryFeatures.Errors). |
SetUserAsync(string? userId) |
ValueTask |
Sets (or with null clears) the authenticated user id sent as user.id. Only kept while BelemetryFeatures.UserTracking is granted. |
FlushAsync(CancellationToken cancellationToken = null) |
ValueTask |
Exports everything queued now - e.g. before a Hybrid app is suspended. |