blog

Blog

My thoughts on something

Adding Instrumentation to Static Hugo Website using Azure Application Insights

As part of building my website I want to have some analyics available as to who if anyone is visiting in and what they are doing. In part one of this two part post I will add logging to Azure Application Insights and consider some of the security concerns around logging from clientside javascript. I will explore the free analytics that Microsoft provide.

In the second post I will extend this logging by capturing search terms and click throughs with my integrated Azure Search search capability.

My source material is:

I’ve already created an Applications Insights resource on Azure and noted it’s INSTRUMENTATION_KEY

I’ve added a parameter to config.toml:

 [params] 
  azureAppInsightsKey = "INSTRUMENTATION_KEY"

I’ve created an html fragment in layouts\partials\azure-appinsights.html to add the basic logging that microsoft recommend in their article:

{{ 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 }}

Finally, we need to ensure that this gets called on every page of our website. To do that we need to add this fragment to into the <head> section of the layouts\_default\baseof.html file. Remember, if you can’t find this file then copy it from your theme’s equivalent file first and then edit it.Add the following as the first script in the <head> section:

{{ partial "azure-appinsights.html" . }}

That’s all there is to it. Now when you visit pages on your site you’ll get the info logged to AppInsights. For example:

Tracked user navigations

What we’ve seen above is the approach implemented by a few people who have blogged about adding AppInsights tracking to websites. One problem is that INSTRUMENTATION_KEY is available in clear text on the client side - and anyone viewing the page source can extract that and misuse it. Since you pay for the AppInsights resource based on the amount of data ingested this is open to abuse and could be costly.

There are some articles written in relation to this but I haven’t found anything satisfactory. One thought I have had is as follows:

  1. After Hugo generates the site, generate MD5 hashes of every page.
  2. As part of the AppInsights call generate an MD5 hash of the page loaded (need to consider computational expense of this…)
  3. The MD5 hash is sent as a property that can be used by AppInsights builtin message filtering functionality.
  4. If the submitted hash isn’t in the list previously generated then do not log the message.

If the above cannot be delivered completely using AppInsights then I could write an Azure Function to do the filtering - although this will add some cost. We will also need to consider what performance hit this could cause of the client side.

I may tackle this at a later date. Initially I will probably just place a budget cap on my App Insights resource of £5/month.