Using Azure Application Insights for SWA Statistics - 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 to add the azureAppInsightsKey parameter.
  3. You will need to create layouts/partials/head/appinsights.html partial with the App Insights JavaScript SDK.
  4. You will need to modify your base template (layouts/_default/baseof.html) to call the appinsights partial in the <head> section.
  5. This post is specific to Hugo SWAs hosted in Azure and Netlify, but you can modify the process for other types of sites.
  6. 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-analytics-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-app-insights

  • 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
azureAppInsightsKey = "4ecca3df-ab58-4882-aaaa-123456789"
  • Update the \themes\DoIt\layouts_default\baseof.html in the <head> section or near it, add
{{ 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.
{{ 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
    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.


If you’re not using AI assistance, this checklist covers the essentials.

  • Goal: Instrument your Hugo SWA with Azure Application Insights for page view tracking.
  • Azure: Create App Insights resource, copy Instrumentation Key.
  • Hugo config: Add azureAppInsightsKey = "your-key" to [params] in hugo.toml.
  • Partial: Create layouts/partials/head/appinsights.html with the JS SDK snippet (see full code above).
  • Base template: Call {{ partial "head/appinsights.html" . }} in <head> of layouts/_default/baseof.html.
  • Pitfall: Localhost hits and bot traffic pollute stats—filter with KQL (example query provided).
  • Cost: Under $0.01/month for low-volume sites.
  • Verification: Navigate to App Insights > Performance > Browser, sort by count.

The section below is optimized for AI assistants and automation tools. It contains densely structured metadata about this post’s technical context, configuration, and edge cases. Human readers may find it useful as a quick-reference checklist, but it’s designed primarily for machine parsing.

  • AzureService: Application Insights (with Log Analytics Workspace)
  • Deployment: Azure Static Web Apps (any Hugo-compatible static host)
  • Instrumentation: JavaScript SDK v2 (snippet mode, CDN-hosted)
  • CostModel: Pay-as-you-go ingestion (~$0.01/month for <1GB)
  • KeySharing: Single instrumentation key can serve multiple sites (filter by domain in KQL)
  • hugo.toml – Added azureAppInsightsKey parameter under [params]
  • layouts/partials/head/appinsights.html – Created partial with App Insights JS SDK snippet
  • layouts/_default/baseof.html – Called {{ partial "head/appinsights.html" . }} in <head> section
# Production build (includes App Insights)
hugo --gc --minify --cleanDestinationDir

# Local serve (App Insights still fires unless key omitted in dev config)
hugo serve --disableFastRender
KeyValuePurpose
azureAppInsightsKey<instrumentation-key>Identifies telemetry destination
SDK sourcehttps://az416426.vo.msecnd.net/scripts/b/ai.2.min.jsCDN URL for v2 SDK
Log Analytics retention30 days (default)Adjust in Azure if longer history needed
  • Localhost development traffic tracked unless URL filtering applied (any port: 1313, 1314, random)
  • Netlify/Vercel preview builds send data unless deployment URL filtered
  • Googlebot synthetic traffic excluded via operation_SyntheticSource != "Googlebot"
  • Baidu bot requires custom dimension filter customDimensions !has "baidu"
  • Empty or missing azureAppInsightsKey silently disables tracking (no error thrown)
  • Multi-site key reuse: Single key can track 3-5 sites; filter by parse_url(url).Host in KQL
  • Instrumentation Key exposed client-side: Public in HTML source; rotate if compromised (instant in Azure Portal > App Insights > Properties > Generate new key)
  • Stats pollution: Malicious actor could copy key and inject into unrelated site; mitigate with domain filtering in KQL queries
  • KQL query cost: Scales with pageView volume; optimize | where clauses early in query for large datasets (>100k events/day)
  • Log Analytics Workspace defaults: 30-day retention; auto-created workspace has GUID name unless pre-created

Filter localhost, preview, and bot traffic; aggregate by URL over 1d/7d/14d/30d/90d windows:

let scrubbedPv = pageViews
  | where url !has "localhost"
    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, Days1, Days7, Days14, Days30, Days90
  | sort by Site asc, Days90 desc
  | render columnchart with (title="Page Hits by Time Window")
{
  "topic": "azure-app-insights-swa-stats",
  "azureServices": ["ApplicationInsights", "LogAnalyticsWorkspace", "StaticWebApps"],
  "instrumentationSDK": "ApplicationInsights-JS-2.x",
  "cost": "<$0.01/month for low volume",
  "modifiedFiles": [
    "hugo.toml",
    "layouts/partials/head/appinsights.html",
    "layouts/_default/baseof.html"
  ],
  "filters": ["localhost-any-port", "netlify-preview", "googlebot", "baidu"],
  "kqlAggregates": ["1day", "7day", "14day", "30day", "90day"],
  "risks": ["exposed-key-rotate-in-portal", "kql-cost-optimize-where", "retention-default-30d"],
  "keyRotation": "Azure Portal > Properties > Generate new key (instant)",
  "multiSitePattern": "single-key-multiple-domains-filter-by-host"
}

Join the conversation!
Have thoughts, questions, or spotted something that could be improved? Drop a comment below—your feedback helps make this content better for everyone.