I have a server-side blazor app that i want to monitor using azure application insights. At the moment i only see request sent when starting the app and closing it, nothing in between... I cant find any documentation from microsoft that supports Blazor.
Is it possible to get Azure Application Insights working fully with server-side Blazor? And if so, does anyone know of any guides about this?
Select File | New Project. Select Visual C# | . NET Core | ASP.NET Core Web Application, ensure to mark the checkbox Add Application Insights to Project. Click Ok on the next screen.
We published a GitHub example that shows how to deploy an XAF Blazor application to a Kubernetes cluster with horizontal autoscaling. We tested the application in two types of clusters: locally-run K3s and Azure Kubernetes Service (AKS). The maximum pod replica number (20) allowed around 300 concurrent users.
Open your project in Visual Studio. Go to Project > Add Application Insights Telemetry. Choose Azure Application Insights, then select Next. Choose your subscription and Application Insights instance (or create a new instance with Create new), then select Next.
You've got two approaches to this - the JavaScript-based approach is ideal if you're ever planning on migrating to WASM version since it'll require very few changes to convert, but for purposes of addressing @liero's concerns, I've also included a server-side approach at the bottom.
We use the community-provided BlazorApplicationInsights package in Blazor server-side to fill this need. Yes, the instructions at the GitHub repo match that of Blazor WASM, but I'd be happy to translate for you (it works just as well on server-side). Note that paths are provided to the files based on those used in the Blazor server-side template.
Further, I've created a Git repo with a project that implements the following steps for your review here. You'll still need to set the Application Insights instrumentation key as specified in step 4 for this to work.
Install the NuGet package BlazorApplicationInsights
to your Blazor server-side project.
In Startup.cs
within the ConfigureServices()
method, add the following line:
services.AddBlazorApplicationInsights();
You may want to augment the setup to reflect a custom role and instance value. You can use the following instead to handle this (same place):
services.AddBlazorApplicationInsights(async appInsights => {
var telemetryItem = new TelemetryItem
{
Tags = new Dictionary<string, object>
{
{"ai.cloud.role", "SPA"},
{"ai.cloud.roleInstance", "Blazor server-side"}
}
};
await appInsights.AddTelemetryInitializer(telemetryItem);
});
_Imports.razor
:@using BlazorApplicationInsights
Pages/_Host.cshtml
file. Script pulled from here. Note the line towards the end where you'll need to replace the string value with your Application Insights instrumentation key.<head>
<!-- ... -->
<script type="text/javascript">
!function (T, l, y) { var S = T.location, k = "script", D = "instrumentationKey", C = "ingestionendpoint", I = "disableExceptionTracking", E = "ai.device.", b = "toLowerCase", w = "crossOrigin", N = "POST", e = "appInsightsSDK", t = y.name || "appInsights"; (y.name || T[e]) && (T[e] = t); var n = T[t] || function (d) { var g = !1, f = !1, m = { initialize: !0, queue: [], sv: "5", version: 2, config: d }; function v(e, t) { var n = {}, a = "Browser"; return n[E + "id"] = a[b](), n[E + "type"] = a, n["ai.operation.name"] = S && S.pathname || "_unknown_", n["ai.internal.sdkVersion"] = "javascript:snippet_" + (m.sv || m.version), { time: function () { var e = new Date; function t(e) { var t = "" + e; return 1 === t.length && (t = "0" + t), t } return e.getUTCFullYear() + "-" + t(1 + e.getUTCMonth()) + "-" + t(e.getUTCDate()) + "T" + t(e.getUTCHours()) + ":" + t(e.getUTCMinutes()) + ":" + t(e.getUTCSeconds()) + "." + ((e.getUTCMilliseconds() / 1e3).toFixed(3) + "").slice(2, 5) + "Z" }(), iKey: e, name: "Microsoft.ApplicationInsights." + e.replace(/-/g, "") + "." + t, sampleRate: 100, tags: n, data: { baseData: { ver: 2 } } } } var h = d.url || y.src; if (h) { function a(e) { var t, n, a, i, r, o, s, c, u, p, l; g = !0, m.queue = [], f || (f = !0, t = h, s = function () { var e = {}, t = d.connectionString; if (t) for (var n = t.split(";"), a = 0; a < n.length; a++) { var i = n[a].split("="); 2 === i.length && (e[i[0][b]()] = i[1]) } if (!e[C]) { var r = e.endpointsuffix, o = r ? e.location : null; e[C] = "https://" + (o ? o + "." : "") + "dc." + (r || "services.visualstudio.com") } return e }(), c = s[D] || d[D] || "", u = s[C], p = u ? u + "/v2/track" : d.endpointUrl, (l = []).push((n = "SDK LOAD Failure: Failed to load Application Insights SDK script (See stack for details)", a = t, i = p, (o = (r = v(c, "Exception")).data).baseType = "ExceptionData", o.baseData.exceptions = [{ typeName: "SDKLoadFailed", message: n.replace(/\./g, "-"), hasFullStack: !1, stack: n + "\nSnippet failed to load [" + a + "] -- Telemetry is disabled\nHelp Link: https://go.microsoft.com/fwlink/?linkid=2128109\nHost: " + (S && S.pathname || "_unknown_") + "\nEndpoint: " + i, parsedStack: [] }], r)), l.push(function (e, t, n, a) { var i = v(c, "Message"), r = i.data; r.baseType = "MessageData"; var o = r.baseData; return o.message = 'AI (Internal): 99 message:"' + ("SDK LOAD Failure: Failed to load Application Insights SDK script (See stack for details) (" + n + ")").replace(/\"/g, "") + '"', o.properties = { endpoint: a }, i }(0, 0, t, p)), function (e, t) { if (JSON) { var n = T.fetch; if (n && !y.useXhr) n(t, { method: N, body: JSON.stringify(e), mode: "cors" }); else if (XMLHttpRequest) { var a = new XMLHttpRequest; a.open(N, t), a.setRequestHeader("Content-type", "application/json"), a.send(JSON.stringify(e)) } } }(l, p)) } function i(e, t) { f || setTimeout(function () { !t && m.core || a() }, 500) } var e = function () { var n = l.createElement(k); n.src = h; var e = y[w]; return !e && "" !== e || "undefined" == n[w] || (n[w] = e), n.onload = i, n.onerror = a, n.onreadystatechange = function (e, t) { "loaded" !== n.readyState && "complete" !== n.readyState || i(0, t) }, n }(); y.ld < 0 ? l.getElementsByTagName("head")[0].appendChild(e) : setTimeout(function () { l.getElementsByTagName(k)[0].parentNode.appendChild(e) }, y.ld || 0) } try { m.cookie = l.cookie } catch (p) { } function t(e) { for (; e.length;)!function (t) { m[t] = function () { var e = arguments; g || m.queue.push(function () { m[t].apply(m, e) }) } }(e.pop()) } var n = "track", r = "TrackPage", o = "TrackEvent"; t([n + "Event", n + "PageView", n + "Exception", n + "Trace", n + "DependencyData", n + "Metric", n + "PageViewPerformance", "start" + r, "stop" + r, "start" + o, "stop" + o, "addTelemetryInitializer", "setAuthenticatedUserContext", "clearAuthenticatedUserContext", "flush"]), m.SeverityLevel = { Verbose: 0, Information: 1, Warning: 2, Error: 3, Critical: 4 }; var s = (d.extensionConfig || {}).ApplicationInsightsAnalytics || {}; if (!0 !== d[I] && !0 !== s[I]) { var c = "onerror"; t(["_" + c]); var u = T[c]; T[c] = function (e, t, n, a, i) { var r = u && u(e, t, n, a, i); return !0 !== r && m["_" + c]({ message: e, url: t, lineNumber: n, columnNumber: a, error: i }), r }, d.autoExceptionInstrumented = !0 } return m }(y.cfg); function a() { y.onInit && y.onInit(n) } (T[t] = n).queue && 0 === n.queue.length ? (n.queue.push(a), n.trackPageView({})) : a() }(window, document, {
src: "https://js.monitor.azure.com/scripts/b/ai.2.min.js", // The SDK URL Source
// name: "appInsights", // Global SDK Instance name defaults to "appInsights" when not supplied
// ld: 0, // Defines the load delay (in ms) before attempting to load the sdk. -1 = block page load and add to head. (default) = 0ms load after timeout,
// useXhr: 1, // Use XHR instead of fetch to report failures (if available),
crossOrigin: "anonymous", // When supplied this will add the provided value as the cross origin attribute on the script tag
// onInit: null, // Once the application insights instance has loaded and initialized this callback function will be called with 1 argument -- the sdk instance (DO NOT ADD anything to the sdk.queue -- As they won't get called)
cfg: { // Application Insights Configuration
instrumentationKey: "INSTRUMENTATION_KEY"
}
});
</script>
</head>
Pages/_Host.cshtml
file before the closing </body>
tag:<body>
<!-- ... ->
<script src="_content/BlazorApplicationInsights/JsInterop.js"></script>
</body>
App.razor
file to support automatic navigation tracking:<ApplicationInsightsComponent />
@page "/"
@inject IApplicationInsights AppInsights
<button class="btn btn-primary" @onclick="TrackEvent">Track Event</button>
<button class="btn btn-secondary" @onclick="TrackMetric">Track Metric</button>
@code {
private async Task TrackEvent()
{
await AppInsights.TrackEvent("My Event");
}
private async Task TrackMetric()
{
await AppInsights.TrackMetric("myMetric", 100, 200, 1, 200, new Dictionary<string,object> {{"customProperty", "customValue"}});
await AppInsights.Flush();
}
}
You'll note support for TrackPageViewPerformance using the PageViewPerformanceTelemtry in the sample code. Rather than binding to a button, you'll likely instead want to call this in your @code block like the following:
protected override async Task OnAfterRenderAsync(bool firstRender)
{
await AppInsights.TrackPageViewPerformance(new PageViewPerformanceTelemetry());
}
Install the NuGet package Microsoft.ApplicationInsights.AspNetCore
to your Blazor server-side project.
In Startup.cs
within the ConfigureServices()
method, add the following line, replacing the string argument with your App Insights instrumentation key:
services.AddBlazorApplicationInsights("INSERT YOUR INSTRUMENTATION KEY HERE");
Alternatively, you can eliminate the string and store the key in either of the following environment variables as well:
You can find additional documentation here describing the various options you can alternatively pass in here regarding other telemetry modules you may wish to enable.
@using
statement to _Imports.razor
:@using Microsoft.ApplicationInsights
First, we'll need to create the host component to track the various navigation operations. Create a new CS file called ApplicationInsightsComponent.cs
in your Shared directory and populate with the following:
using System;
using Microsoft.ApplicationInsights;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Routing;
namespace ApplicationInsightsServerSample.Shared
{
public class ApplicationInsightsComponent : ComponentBase, IDisposable
{
[Inject]
private TelemetryClient _telemetryClient { get; init; }
[Inject]
private NavigationManager _navigationManager { get; init; }
protected override void OnAfterRender(bool firstRender)
{
if (firstRender)
{
_navigationManager.LocationChanged += NavigationManagerOnLocationChanged;
}
}
private void NavigationManagerOnLocationChanged(object? sender, LocationChangedEventArgs e)
{
_telemetryClient.TrackPageView(e.Location); //Set the argument to whatever you'd like to name the page
}
public void Dispose()
{
_navigationManager.LocationChanged -= NavigationManagerOnLocationChanged;
}
}
}
In your App.razor
file, add the following to the top:
<ApplicationInsightsComponent />
Let's also implement something to catch any exceptions thrown in the app. Create
another file in your Shared directory called Error.razor
and populate with the following:
@inject TelemetryClient _telemetryClient
<CascadingValue Value=this>
@ChildContent
</CascadingValue>
@code
{
[Parameter]
public RenderFragment ChildContent { get; set; }
public void ProcessError(Exception ex)
{
_telemetryClient.TrackException(ex);
}
}
Going back to your App.razor
file, wrap the Router with this new component as in the following:
<Error>
<Router ...>
...
</Router>
</Error>
In any components then, specify the Error component as a cascading parameter in the code block with the following:
@code {
[CascadingParameter]
public Error Error {get; set;}
}
When you otherwise wrap an operation with a try/catch block, call Error.ProcessError(ex)
within your catch to propagate the exception up to our error component:
try
{
// ...
}
catch (Exception ex)
{
Error.ProcessError(ex);
}
Beyond that, you'll undoubtedly want to track any number of events on your various pages, so I'll wrap up with a TrackEvent example. As in the other answer let's repurpose the page example with its buttons and re-work it for compatibility here:
@page "/"
@inject TelemetryClient _telemetryClient
<button class="btn btn-primary" @onclick="TrackEvent">Track Event</button>
<button class="btn btn-secondary" @onclick="TrackMetric">Track Metric</button>
@code {
private void TrackEvent()
{
await _telemetryClient.TrackEvent("My Event");
}
private void TrackMetric()
{
_telemetryClient.TrackMetric("myMetric", 100, new Dictionary<string,string> {{"customProperty", "customValue"}});
}
}
I hope this helps, but I'm happy to update this answer with any other detail you're looking for!
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With