16

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?

Jotten
  • 161
  • 1
  • 3
  • There is an opened [GitHub Issue](https://github.com/dotnet/aspnetcore/issues/30972) for this. Please vote on github to prioritize. – Liero Jun 11 '21 at 06:46

2 Answers2

23

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.

JavaScript-Based Approach (Blazor WASM or Server-side)

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.

  1. Install the NuGet package BlazorApplicationInsights to your Blazor server-side project.

  2. 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);
});
  1. Add the following @using statement to _Imports.razor:
@using BlazorApplicationInsights
  1. Add the reference to the Application Insights JS file to the head of your 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>
  1. Add the JS interop to the bottom of your Pages/_Host.cshtml file before the closing </body> tag:
<body>
  <!-- ... ->
  <script src="_content/BlazorApplicationInsights/JsInterop.js"></script>
</body>
  1. Add the following to the top of your App.razor file to support automatic navigation tracking:
<ApplicationInsightsComponent />
  1. This library will automatically trigger "Track Page View" on route changes, but anything else will require you to manually fire off the events. Here's an example you might have in one of your pages (see more sample methods here):
@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());
}

Application Insights SDK Approach (Blazor server-side)

  1. Install the NuGet package Microsoft.ApplicationInsights.AspNetCore to your Blazor server-side project.

  2. 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:

  • APPINSIGHTS_INSTRUMENTATIONKEY
  • ApplicationInsights:InstrumentationKey

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.

  1. Add the following @using statement to _Imports.razor:
@using Microsoft.ApplicationInsights
  1. Because Application Insights does not feature any native integration with Blazor, you'll need to hook up everything you're wanting to track on your own. We'll look at some ways of doing this in the remainder of this section.

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!

Whit Waldo
  • 4,806
  • 4
  • 48
  • 70
  • Thanks, but why don't you just use TelemetryClient to do the logging on the server? You might use NavigaionManager.OnNavigate to track page views. You are doing a unnecessary roundtrip to javascript for every log, which is suboptimal. – Liero Mar 18 '21 at 08:44
  • The original asker doesn't specify if there's something specific they're looking to use that's available in App Insights and thus only available via the JS interop, so I opted for a solution that favors that broader/simpler integration. There's not enough information in the original ask to provide a more narrowed answer that yields exactly what the asker wants in the most performant manner. – Whit Waldo Mar 18 '21 at 17:59
  • Nevertheless, this is pretty bad solution for Blazor Server app I highly recommend not to use it. – Liero Mar 18 '21 at 19:02
  • Given you are the one that placed the bounty on the question, do you have any more specific requirements? I'd be happy to re-answer the question if you can narrow the scope a bit more, but at the end of the day, there is no automated support for most of what Blazor does (yet), so if you're looking to hook navigation events or track specific things, you're going to have to inject the TelemetryClient to the page/component and wire it up manually. – Whit Waldo Mar 19 '21 at 00:14
  • @Liero Updated the answer to reflect the JavaScript method (compatible with WASM or server-side) and the SDK approach (only compatible with server-side, but more performant, as you note). – Whit Waldo Mar 19 '21 at 01:04
  • I think anybody who has ever used AppInsights in ASP.NET would expect simmilar metrics in Blazor Server Side as well, e.g: https://github.com/dotnet/aspnetcore/issues/30972 – Liero Mar 19 '21 at 10:22
  • 1
    I don't disagree, but that's a bit out of scope for the inquiry. As it stands, there exists no built-in support, but it's being tracked. Note though that per the linked ticket https://github.com/dotnet/aspnetcore/issues/5461 the general consensus is to use the JS interop approach I first covered. – Whit Waldo Mar 19 '21 at 17:25
-1

Blazor server app is essentially an asp.net core app. So you should setup app insights integration following this guide.

  1. Add 'Microsoft.ApplicationInsights.AspNetCore' nuget in the project.
    <ItemGroup>
      <PackageReference Include="Microsoft.ApplicationInsights.AspNetCore" Version="2.13.1" />
    </ItemGroup>
  1. Add services.AddApplicationInsightsTelemetry(); in ConfigureServices method of Startup.cs.
    // This method gets called by the runtime. Use this method to add services to the container.
    public void ConfigureServices(IServiceCollection services)
    {
        // The following line enables Application Insights telemetry collection.
        services.AddApplicationInsightsTelemetry();

        // This code adds other services for your application.
        ...
    }
  1. Add instrumentation key in appsettins.json.
    {
      "ApplicationInsights": {
        "InstrumentationKey": "putinstrumentationkeyhere"
      },
      "Logging": {
        "LogLevel": {
          "Default": "Warning"
        }
      }
    }

NOTE: Client side logging is currently not supported out of the box. https://github.com/microsoft/ApplicationInsights-dotnet/issues/2143. You can however use community project https://github.com/IvanJosipovic/BlazorApplicationInsights

krishg
  • 5,935
  • 2
  • 12
  • 19
  • Unfortunately this guide is absolutely insufficient. You won't get much usefull metrics, any page view or response time, etc. – Liero Mar 15 '21 at 09:43
  • page view requires client side logging which is currently not out of the box. https://github.com/microsoft/ApplicationInsights-dotnet/issues/2143 . You can however use community project https://github.com/IvanJosipovic/BlazorApplicationInsights – krishg Mar 15 '21 at 11:46
  • Page view for Blazor Server Side should not require client side logging as the router lives on server. Besides, there is a ton of issues Blazor related issues with AppInsights. This answer is simply not enough for anybody looking for AppInsights integration with Blazor. – Liero Mar 16 '21 at 11:45
  • The links you have posted are for wasm, not server side – Liero Mar 16 '21 at 11:46