Using Azure Application Insights for SWA Statistcs - Part 1 - Setup

page-hits-chart

Want to see which pages on your site are the most popular? Static Web Applications (SWA) like those generated by Hugo (see Create New Hugo Blog Site in Azure SWA) and any site that can add Javascript to content can send details to Azure where you can view details through many different lenses. For low volume sites, the cost is under a US$0.01 per month.

App Insights also can provide visibility in other metrics such as
  • Unique user counts
  • Users’ page paths through your site to identify high engagement areas and exit points
  • Internal and external referring pages
  • Page load times
  • Sites’ uptimes
Notes
  1. You must use a resource group in a non-free Azure subscription.
  2. You will need to modify your Hugo site’s toml file.
  3. You probably have to make modifications to your theme.
  4. This post is specific to Hugo SWAs hosted in Azure and Netlify, but you can modify the process for other types of sites.
  5. I relied heavily on others’ posts in the References section below. This process works, and I chose some slightly different options for my own needs and preferences.

Each Azure App Insights instance has its own Instrumentation Key which you pass via Javascript to Azure. Follow these steps.

  • Optional - If you don’t like guids in your resource group names, create a new Log Analytics first which takes about a minute to create. Otherwise, you will end up with something like DefaultWorkspace-343bc83-rest-of-a-guid-xxxx. To do this, navigate to Azure Home > Create a Resource > search on log analytics workspace > Create > Fill out the details as desired. https://portal.azure.com/#create/Microsoft.LogAnalyticsOMS

    create-log-analyitcs-workspace

  • Azure Home > Create a resource > Application Insights to navigate to https://portal.azure.com/#create/Microsoft.AppInsights

  • Create

  • I chose the same resource group as my swa sites

    create-log-analyitcs-workspace

  • Review and Create > Create

  • Copy the Instrumentation Key value for this Application Insights instance

    copy-key

If your theme already has a InstrumentationKey or some variant spelling in its hugo.toml or for older (most as I write this in 2024) themes, config.toml, you may be able to uncomment the line add the Instrumentation Key. As I write, the Hugo Doit theme requires adding Instrumentation Key yourself as does the lightbi theme.

For this post, I made the updates based on the Doit and lightbi themes. If you are using a different theme, adjust your paths accordingly. Note that the “lightbi” Hugo theme does not have a separate directory for head, but Doit does, so again, adjust accordingly.

  • In the [params] section of hugo.toml or config.toml, add this line using your key
1
azureAppInsightsKey = "4ecca3df-ab58-4882-aaaa-123456789"
  • Update the \themes\Doit\layouts_default\baseof.html in the <head> section or near it, add
1
{{ partial "appinsights.html" . }}

This is where it ended up in the lightbi theme. To be honest, I forget why I didn’t put it within the <head> section, but it works in <body>.

baseofhtml

  • Create \themes\Doit\layouts\partials\head\appinsights.html or whatever the appropriate path is for your theme with this content.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
{{ if .Site.Params.azureAppInsightsKey }}
    <script type="text/javascript">
        !function(T,l,y){var S=T.location,u="script",k="instrumentationKey",D="ingestionendpoint",C="disableExceptionTracking",E="ai.device.",I="toLowerCase",b="crossOrigin",w="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:"4",version:2,config:d};function v(e,t){var n={},a="Browser";return n[E+"id"]=a[I](),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,p,l,u;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][I]()]=i[1])}if(!e[D]){var r=e.endpointsuffix,o=r?e.location:null;e[D]="https://"+(o?o+".":"")+"dc."+(r||"services.visualstudio.com")}return e}(),c=s[k]||d[k]||"",p=s[D],l=p?p+"/v2/track":config.endpointUrl,(u=[]).push((n="SDK LOAD Failure: Failed to load Application Insights SDK script (See stack for details)",a=t,i=l,(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)),u.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,l)),function(e,t){if(JSON){var n=T.fetch;if(n&&!y.useXhr)n(t,{method:w,body:JSON.stringify(e),mode:"cors"});else if(XMLHttpRequest){var a=new XMLHttpRequest;a.open(w,t),a.setRequestHeader("Content-type","application/json"),a.send(JSON.stringify(e))}}}(u,l))}function i(e,t){f||setTimeout(function(){!t&&m.core||a()},500)}var e=function(){var n=l.createElement(u);n.src=h;var e=y[b];return!e&&""!==e||"undefined"==n[b]||(n[b]=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(u)[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[C]&&!0!==s[C]){method="onerror",t(["_"+method]);var c=T[method];T[method]=function(e,t,n,a,i){var r=c&&c(e,t,n,a,i);return!0!==r&&m["_"+method]({message:e,url:t,lineNumber:n,columnNumber:a,error:i}),r},d.autoExceptionInstrumented=!0}return m}(y.cfg);(T[t]=n).queue&&0===n.queue.length&&n.trackPageView({})}(window,document,{
        src: "https://az416426.vo.msecnd.net/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
        cfg: { // Application Insights Configuration
            instrumentationKey: "{{- .Site.Params.azureAppInsightsKey -}}"
            /* ...Other Configuration Options... */
        }});
    </script>
{{ end }}
  • Publish your site and navigate to it.

My Azure SWAs says App Insights is enabled, but the screen also says, “You don’t have the permissions to update App Insights resource for your app”, and the entire UI is dimmed and unresponsive. I think adding a web function to a SWA can directly enable App Insights on the SWA admin UI, but the above process works.

no-permission

  • In Azure, open your new instance of App Insights > Investigate - Performance > Browser
  • The bottom-left pane shows your pages sorted by average time to load. Click on COUNT to see the most popular pages at the top.

However, this includes non-end user page hits such as Googlebot crawl and previewing your Hugo site on http://localhost:1313/.

Warning
I optimized the query a little, but if you have a busy site with hundreds of thousands of hits per month, this may take some time to run and cost real money.
  • From your App Insights instance, click Monitoring - Logs > Close Queries pop up > Paste this KQL code
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
    let scrubbedPv = pageViews
      | where url !has "1313" and url !has "netlify"and customDimensions !has "baidu" and operation_SyntheticSource != "Googlebot" and timestamp > ago(93d);
    let allPages=	scrubbedPv
      | extend Site = parse_url(url).Host
        | extend Page = operation_Name
        | distinct url, Page, tostring(Site), name;
    let pv01 = scrubbedPv
      | where timestamp > ago(1d)
      | summarize Days1 = count() by url;
    let pv07 = scrubbedPv
      | where timestamp > ago(7d)
      | summarize Days7 = count() by url;
    let pv14 = scrubbedPv
      | where timestamp > ago(14d)
      | summarize Days14 = count() by url;
    let pv30 = scrubbedPv
      | where timestamp > ago(30d)
      | summarize Days30 = count() by url;
    let pv90 = scrubbedPv
      | where timestamp > ago(90d)
      | summarize Days90 = count() by url;
    allPages
    | join kind=leftouter pv01 on url
    | join kind=leftouter pv07 on url
    | join kind=leftouter pv14 on url
    | join kind=leftouter pv30 on url
    | join kind=leftouter pv90 on url
    | project name, Site, Page, Days90, Days1, Days7, Days14, Days30
    | sort by Site asc, Days90 desc
    | render  columnchart  with (title="Page Hits Past 90 Days")
  • Click Run

This filters out unwanted pageviews such as the preview on port 1313 and Googlebot crawls.

page-hits-chart
Clicking Results shows page hits broken out by 1, 7, 14, 30 and 90 days.

In part 2 of this series, I plan to go into more details of viewing usage.