Skip to content

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 IBelemetryProcessor sees 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.

OptionDefault
Persistence.Directory{LocalApplicationData}/bit-belemetry/{service.name}
Persistence.MaxBytes20 MB; the oldest requests are evicted past it
Persistence.MaxAge48 hours
Persistence.RetryInterval1 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.
An unhandled error has occurred. Reload 🗙