Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 37 additions & 1 deletion src/frontend/config/sidebar/dashboard.topics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,20 @@ export const dashboardTopics: StarlightSidebarTopicsUserConfig = {
uk: 'Увімкнути телеметрію браузера',
'zh-CN': '启用浏览器遥测',
},
slug: 'dashboard/enable-browser-telemetry',
items: [
{
label: 'Overview',
slug: 'dashboard/enable-browser-telemetry',
},
{
label: 'Browser app configuration',
slug: 'dashboard/enable-browser-telemetry/browser-app-configuration',
},
{
label: 'Blazor WebAssembly integration',
slug: 'dashboard/enable-browser-telemetry/blazor-webassembly',
},
],
},
{
label: 'Microsoft telemetry',
Expand All @@ -244,5 +257,28 @@ export const dashboardTopics: StarlightSidebarTopicsUserConfig = {
},
slug: 'dashboard/microsoft-collected-dashboard-telemetry',
},
{
label: 'Telemetry after deployment',
translations: {
da: 'Telemetri efter implementering',
de: 'Telemetrie nach der Bereitstellung',
en: 'Telemetry after deployment',
es: 'Telemetría después de la implementación',
fr: 'Télémétrie après le déploiement',
hi: 'परिनियोजन के बाद टेलीमेट्री',
id: 'Telemetri setelah penerapan',
it: 'Telemetria dopo la distribuzione',
ja: 'デプロイ後のテレメトリ',
ko: '배포 후 텔레메트리',
pt: 'Telemetria após a implantação',
'pt-BR': 'Telemetria após a implantação',
'pt-PT': 'Telemetria após a implementação',
ru: 'Телеметрия после развертывания',
tr: 'Dağıtımdan sonra telemetri',
uk: 'Телеметрія після розгортання',
'zh-CN': '部署后的遥测',
},
slug: 'dashboard/telemetry-after-deployment',
},
],
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
---
title: Blazor WebAssembly integration
description: Learn how to integrate browser telemetry in a Blazor WebAssembly app using JavaScript interop.
---

import LearnMore from '@components/LearnMore.astro';
import { Aside } from '@astrojs/starlight/components';

Blazor WebAssembly (WASM) apps run entirely in the browser using a .NET runtime compiled to WebAssembly. Like other browser apps, Blazor WASM apps use the [JavaScript OTEL SDK](https://opentelemetry.io/docs/languages/js/getting-started/browser/) to send telemetry to the Aspire dashboard via JavaScript interop. The dashboard configuration for [OTLP HTTP](/dashboard/enable-browser-telemetry/#otlp-configuration) and [CORS](/dashboard/enable-browser-telemetry/#cors-configuration) described in [Enable browser telemetry](/dashboard/enable-browser-telemetry/) applies to Blazor WASM apps as well.

## Provide OTEL configuration to the browser

Blazor WASM apps can't read server-side environment variables directly. When the app is hosted by an ASP.NET Core server (for example, a Blazor Web App or hosted Blazor WASM project), expose the OTEL configuration through an API endpoint on the server:

```csharp title="C# — Program.cs (Server)"
using Microsoft.AspNetCore.Authorization;

app.MapGet("/api/telemetry-config", [Authorize] (IConfiguration config) => new
{
Endpoint = config.GetValue<string>("OTEL_EXPORTER_OTLP_ENDPOINT", ""),
Headers = config.GetValue<string>("OTEL_EXPORTER_OTLP_HEADERS", ""),
ResourceAttributes = config.GetValue<string>("OTEL_RESOURCE_ATTRIBUTES", "")
});
```

<Aside type="caution">
This approach exposes the OTLP API key to the browser in the HTTP response. For environments where the API key must remain secret, use an [authenticated OTEL proxy](#authenticated-otel-proxy) instead. In production, consider adding authentication or authorization to restrict access to this endpoint.
</Aside>

## Initialize OTEL from a Blazor component

In the Blazor WASM app, call the JavaScript `initializeTelemetry` function using `IJSRuntime` after the component has rendered. Add the [JavaScript OTEL SDK initialization code](/dashboard/enable-browser-telemetry/browser-app-configuration/#example-browser-telemetry-code) to a JavaScript file bundled with your app, then call it from a Razor component:

```razor title="Razor — TelemetryInitializer.razor (Client)"
@inject IJSRuntime JS
@inject HttpClient Http

@code {
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
var config = await Http.GetFromJsonAsync<TelemetryConfig>("/api/telemetry-config");
if (config is { Endpoint.Length: > 0 })
{
await JS.InvokeVoidAsync(
"initializeTelemetry",
config.Endpoint,
config.Headers,
config.ResourceAttributes);
}
}
}

private sealed record TelemetryConfig(
string Endpoint,
string Headers,
string ResourceAttributes);
}
```

Include this component in your `App.razor` or another top-level component so that telemetry is initialized when the app loads.

## Backend-to-frontend trace correlation

To correlate browser spans with server-side traces, include the current trace context in the server-rendered HTML. When the Blazor WASM app is hosted within a server-rendered page, such as a Blazor Web App with prerendering, the server can write the `traceparent` meta tag during prerender:

```razor title="Razor — App.razor (Server prerender)"
@using System.Diagnostics
<!DOCTYPE html>
<html lang="en">
<head>
@if (Activity.Current is { } currentActivity)
{
<meta name="traceparent" content="@currentActivity.Id" />
}
<!-- Other elements omitted for brevity... -->
</head>
```

The JavaScript OTEL SDK reads this `traceparent` value automatically when `DocumentLoadInstrumentation` is registered, linking the browser spans to the originating server trace.

## Authenticated OTEL proxy

When the Aspire dashboard's OTLP API key must not be exposed to the browser, route telemetry through a server-side proxy endpoint. The browser sends telemetry to the proxy, and the proxy forwards it to the dashboard with the API key included as a server-side secret:

```csharp title="C# — Program.cs (Server proxy endpoint)"
// Register an HttpClient for the OTEL proxy
builder.Services.AddHttpClient("otel-proxy");

// ...

app.MapPost("/api/telemetry/{**path}", async (
string path,
HttpContext context,
IHttpClientFactory httpClientFactory) =>
{
var dashboardEndpoint = Environment.GetEnvironmentVariable("OTEL_EXPORTER_OTLP_ENDPOINT");
if (string.IsNullOrEmpty(dashboardEndpoint))
{
return Results.NotFound();
}

var client = httpClientFactory.CreateClient("otel-proxy");

using var requestBody = new StreamContent(context.Request.Body);
requestBody.Headers.ContentType = System.Net.Http.Headers.MediaTypeHeaderValue
.Parse(context.Request.ContentType ?? "application/x-protobuf");

using var request = new HttpRequestMessage(
HttpMethod.Post,
$"{dashboardEndpoint.TrimEnd('/')}/{path}")
{
Content = requestBody
};

// Copy OTLP API key from server environment to the forwarded request
var headersEnv = Environment.GetEnvironmentVariable("OTEL_EXPORTER_OTLP_HEADERS") ?? string.Empty;
foreach (var header in headersEnv.Split(',', StringSplitOptions.RemoveEmptyEntries))
{
var parts = header.Split('=', 2);
if (parts.Length == 2)
{
request.Headers.TryAddWithoutValidation(parts[0].Trim(), parts[1].Trim());
}
}

var response = await client.SendAsync(request);

context.Response.StatusCode = (int)response.StatusCode;
await response.Content.CopyToAsync(context.Response.Body);
return Results.Empty;
});
```

Configure the JavaScript OTEL SDK in the Blazor WASM app to point to the proxy endpoint instead of the dashboard directly:

```javascript title="JavaScript — telemetry.js (Client)"
export function initializeTelemetry(resourceAttributes) {
const otlpOptions = {
url: '/api/telemetry/v1/traces' // Proxy endpoint, same origin - no CORS needed
};
// ... rest of SDK initialization
}
```

This pattern eliminates the need for CORS configuration on the dashboard because the browser communicates only with the same-origin server. The API key stays on the server and is never visible to the browser.

## See also

- [Enable browser telemetry overview](/dashboard/enable-browser-telemetry/)
- [Browser app configuration](/dashboard/enable-browser-telemetry/browser-app-configuration/)
- [Aspire dashboard configuration](/dashboard/configuration/)
- [Standalone Aspire dashboard](/dashboard/standalone/)
- [Telemetry after deployment](/dashboard/telemetry-after-deployment/)
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
---
title: Browser app configuration
description: Learn how to configure the JavaScript OTEL SDK in your browser app to send telemetry to the Aspire dashboard.
---

import LearnMore from '@components/LearnMore.astro';
import { Aside } from '@astrojs/starlight/components';

A browser app uses the [JavaScript OTEL SDK](https://opentelemetry.io/docs/languages/js/getting-started/browser/) to send telemetry to the dashboard. Successfully sending telemetry to the dashboard requires the SDK to be correctly configured.

Before configuring the browser app, ensure the dashboard is configured with an [OTLP HTTP endpoint and CORS](/dashboard/enable-browser-telemetry/). For Blazor WebAssembly apps, see [Blazor WebAssembly integration](/dashboard/enable-browser-telemetry/blazor-webassembly/).

## OTLP exporter

OTLP exporters must be included in the browser app and configured with the SDK. For example, exporting distributed tracing with OTLP uses the [@opentelemetry/exporter-trace-otlp-proto](https://www.npmjs.com/package/@opentelemetry/exporter-trace-otlp-proto) package.

When OTLP is added to the SDK, OTLP options must be specified. OTLP options includes:

- `url`: The address that HTTP OTLP requests are made to. The address should be the dashboard HTTP OTLP endpoint and the path to the OTLP HTTP API. For example, `https://localhost:4318/v1/traces` for the trace OTLP exporter. If the browser app is launched by the AppHost then the HTTP OTLP endpoint is available from the `OTEL_EXPORTER_OTLP_ENDPOINT` environment variable.

- `headers`: The headers sent with requests. If OTLP endpoint API key authentication is enabled the `x-otlp-api-key` header must be sent with OTLP requests. If the browser app is launched by the AppHost then the API key is available from the `OTEL_EXPORTER_OTLP_HEADERS` environment variable.

## Browser metadata

When a browser app is configured to collect distributed traces, the browser app can set the trace parent a browser's spans using the `meta` element in the HTML. The value of the `name="traceparent"` meta element should correspond to the current trace.

In a .NET app, for example, the trace parent value would likely be assigned from the `Activity.Current` and passing its `Activity.Id` value as the `content`. For example, consider the following Razor code:

```razor
<head>
@if (Activity.Current is { } currentActivity)
{
<meta name="traceparent" content="@currentActivity.Id" />
}
<!-- Other elements omitted for brevity... -->
</head>
```

The preceding code sets the `traceparent` meta element to the current activity ID.

## Example browser telemetry code

The following JavaScript code demonstrates the initialization of the OpenTelemetry JavaScript SDK and the sending of telemetry data to the dashboard:

```javascript
import { ConsoleSpanExporter, SimpleSpanProcessor } from '@opentelemetry/sdk-trace-base';
import { DocumentLoadInstrumentation } from '@opentelemetry/instrumentation-document-load';
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-proto';
import { registerInstrumentations } from '@opentelemetry/instrumentation';
import { resourceFromAttributes } from '@opentelemetry/resources';
import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions';
import { WebTracerProvider } from '@opentelemetry/sdk-trace-web';
import { ZoneContextManager } from '@opentelemetry/context-zone';

export function initializeTelemetry(otlpUrl, headers, resourceAttributes) {
const otlpOptions = {
url: `${otlpUrl}/v1/traces`,
headers: parseDelimitedValues(headers)
};

const attributes = parseDelimitedValues(resourceAttributes);
attributes[SemanticResourceAttributes.SERVICE_NAME] = 'browser';

const provider = new WebTracerProvider({
resource: resourceFromAttributes(attributes),
});
provider.addSpanProcessor(new SimpleSpanProcessor(new ConsoleSpanExporter()));
provider.addSpanProcessor(new SimpleSpanProcessor(new OTLPTraceExporter(otlpOptions)));

provider.register({
// Prefer ZoneContextManager: supports asynchronous operations
contextManager: new ZoneContextManager(),
});

// Registering instrumentations
registerInstrumentations({
instrumentations: [new DocumentLoadInstrumentation()],
});
}

function parseDelimitedValues(s) {
const headers = s.split(','); // Split by comma
const result = {};

headers.forEach(header => {
const [key, value] = header.split('='); // Split by equal sign
result[key.trim()] = value.trim(); // Add to the object, trimming spaces
});

return result;
}
```

The preceding JavaScript code defines an `initializeTelemetry` function that expects the OTLP endpoint URL, the headers, and the resource attributes. These parameters are provided by the consuming browser app that pulls them from the environment variables set by the app host. Consider the following Razor code:

```razor {32-39}
@using System.Diagnostics
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>@ViewData["Title"] - BrowserTelemetry</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet"
integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
<link rel="stylesheet" href="~/css/site.css" asp-append-version="true" />

@if (Activity.Current is { } currentActivity)
{
<meta name="traceparent" content="@currentActivity.Id" />
}
</head>
<body>
<header>
<nav class="navbar navbar-expand-sm navbar-toggleable-sm navbar-light bg-white border-bottom box-shadow mb-3">
<div class="container">
<a class="navbar-brand" asp-area="" asp-page="/Index">BrowserTelemetry</a>
</div>
</nav>
</header>
<div class="container">
<main role="main" class="pb-3">
@RenderBody()
</main>
</div>
@await RenderSectionAsync("Scripts", required: false)
<script src="scripts/bundle.js"></script>
@if (Environment.GetEnvironmentVariable("OTEL_EXPORTER_OTLP_ENDPOINT") is { Length: > 0 } endpointUrl)
{
var headers = Environment.GetEnvironmentVariable("OTEL_EXPORTER_OTLP_HEADERS");
var attributes = Environment.GetEnvironmentVariable("OTEL_RESOURCE_ATTRIBUTES");
<script>
BrowserTelemetry.initializeTelemetry('@endpointUrl', '@headers', '@attributes');
</script>
}
</body>
</html>
```

<Aside type="tip">
The bundling and minification of the JavaScript code is beyond the scope of this article.
</Aside>

For the complete working example of how to configure the JavaScript OTEL SDK to send telemetry to the dashboard, see the [browser telemetry sample](https://github.com/microsoft/aspire/tree/main/playground/BrowserTelemetry).

## See also

- [Enable browser telemetry overview](/dashboard/enable-browser-telemetry/)
- [Blazor WebAssembly integration](/dashboard/enable-browser-telemetry/blazor-webassembly/)
- [Aspire dashboard configuration](/dashboard/configuration/)
- [Standalone Aspire dashboard](/dashboard/standalone/)
- [Telemetry after deployment](/dashboard/telemetry-after-deployment/)
Loading
Loading