Tracking SharePoint User Properties with Microsoft Application Insights

Out-of-the-box page view tracking is dead simple to get working with every Web Analytic tool I’ve used, and Microsoft’s Application Insights available via the Azure platform is no different.

The Application Insights JavaScript code snippet is straightforward enough, albeit a little strange in that it is adding a script node to the DOM, but that’s fine:


var sdkInstance="appInsightsSDK";window[sdkInstance]="appInsights";var aiName=window[sdkInstance],aisdk=window[aiName]||function(e){function n(e){t[e]=function(){var n=arguments;t.queue.push(function(){t[e].apply(t,n)})}}var t={config:e};t.initialize=!0;var i=document,a=window;setTimeout(function(){var n=i.createElement("script");n.src=e.url||"https://az416426.vo.msecnd.net/scripts/b/ai.2.min.js",i.getElementsByTagName("script")[0].parentNode.appendChild(n)});try{t.cookie=i.cookie}catch(e){}t.queue=[],t.version=2;for(var r=["Event","PageView","Exception","Trace","DependencyData","Metric","PageViewPerformance"];r.length;)n("track"+r.pop());n("startTrackPage"),n("stopTrackPage");var s="Track"+r[0];if(n("start"+s),n("stop"+s),n("addTelemetryInitializer"),n("setAuthenticatedUserContext"),n("clearAuthenticatedUserContext"),n("flush"),t.SeverityLevel={Verbose:0,Information:1,Warning:2,Error:3,Critical:4},!(!0===e.disableExceptionTracking||e.extensionConfig&&e.extensionConfig.ApplicationInsightsAnalytics&&!0===e.extensionConfig.ApplicationInsightsAnalytics.disableExceptionTracking)){n("_"+(r="onerror"));var o=a[r];a[r]=function(e,n,i,a,s){var c=o&&o(e,n,i,a,s);return!0!==c&&t["_"+r]({message:e,url:n,lineNumber:i,columnNumber:a,error:s}),c},e.autoExceptionInstrumented=!0}return t}(
{
  instrumentationKey:""
}
);window[aiName]=aisdk,aisdk.queue&&0===aisdk.queue.length&&aisdk.trackPageView({});

This will get you tracking good metrics on any page view, which you can find best in the Logs section of the Application Insights “blade” in the Azure portal.

SharePoint scripting considerations

For SharePoint, if you want more, and you certainly do because the user data is very rich, it’s a little trickier to setup.

Script firing order is perhaps the least straightforward thing in SharePoint and perhaps this is changing in 2016(?), but usually, the developer is tasked with accounting for the disjoint manner in which scripts are loaded.

In most cases with SharePoint scripts, it’s basically a given that you’ll be layering your function calls under the arcane SP.SOD.executeFunc or SP.SOD.executeOrDelayUntilScriptLoaded functions to handle the unordered loading. For example:

SP.SOD.executeFunc('core.js', 'FollowingCallout', function() { FollowingCallout(); });

In the above, we use the script to declare a script dependency for the object that whats to be utilized in the subsequent function…MESSY!

SharePoint’s namespace challenge

The reason the code snippet is defining a script reference on the fly probably relates to the fact that it is trying to bypass the load order of other scripts and track the page view ASAP. But there’s a problem…

SharePoint uses a script dependency convention of namespaces which disallows JS objects to be defined if they already exist.

The problem for our script then is that both SharePoint and the script referenced by the tracking snippet define a Microsoft object, which is going to cause a collision if App Insights script does so first, and guess what, that’s what it’s trying to do.

The only way around this is to download the script being referenced in the snippet (in my case it is, https://az416426.vo.msecnd.net/scripts/b/ai.2.min.js) and modify the definition of the Microsoft object which occurs early on in the script…

Before (with Microsoft)


/*!
 * Application Insights JavaScript SDK - Web, 2.4.4
 * Copyright (c) Microsoft and contributors. All rights reserved.
 */
! function(e, t) {
    "object" == typeof exports && "undefined" != typeof module ? t(exports) : "function" == typeof define && define.amd ? define(["exports"], t) : t((e.Microsoft = e.Microsoft || {}, e.Microsoft.ApplicationInsights = {}))
}(this, function(e) {
// etc, etc...

After (changed to MSAzure)


/*!
 * Application Insights JavaScript SDK - Web, 2.4.4
 * Copyright (c) Microsoft and contributors. All rights reserved.
 * Modified by  on 
 */
! function(e, t) {
    "object" == typeof exports && "undefined" != typeof module ? t(exports) : "function" == typeof define && define.amd ? define(["exports"], t) : t((e.MSAzure = e.MSAzure || {}, e.MSAzure.ApplicationInsights = {}))
}(this, function(e) {

Then put a copy of the script on the SharePoint server. I recommend putting it under

<YOUR SITE>/SiteAssets/js

Next, change the tracking code snippet to refer to the modified, uploaded script.


var sdkInstance="appInsightsSDK";window[sdkInstance]="appInsights";var aiName=window[sdkInstance],aisdk=window[aiName]||function(e){function n(e){t[e]=function(){var n=arguments;t.queue.push(function(){t[e].apply(t,n)})}}var t={config:e};t.initialize=!0;var i=document,a=window;setTimeout(function(){var n=i.createElement("script");n.src=e.url||"https://yourserver.sharepoint.com/SiteAssets/js/ModifiedAppInsightsScript.js",i.getElementsByTagName("script")[0].parentNode.appendChild(n)});try{t.cookie=i.cookie}catch(e){}t.queue=[],t.version=2;for(var r=["Event","PageView","Exception","Trace","DependencyData","Metric","PageViewPerformance"];r.length;)n("track"+r.pop());n("startTrackPage"),n("stopTrackPage");var s="Track"+r[0];if(n("start"+s),n("stop"+s),n("addTelemetryInitializer"),n("setAuthenticatedUserContext"),n("clearAuthenticatedUserContext"),n("flush"),t.SeverityLevel={Verbose:0,Information:1,Warning:2,Error:3,Critical:4},!(!0===e.disableExceptionTracking||e.extensionConfig&&e.extensionConfig.ApplicationInsightsAnalytics&&!0===e.extensionConfig.ApplicationInsightsAnalytics.disableExceptionTracking)){n("_"+(r="onerror"));var o=a[r];a[r]=function(e,n,i,a,s){var c=o&&o(e,n,i,a,s);return!0!==c&&t["_"+r]({message:e,url:n,lineNumber:i,columnNumber:a,error:s}),c},e.autoExceptionInstrumented=!0}return t}(
{
  instrumentationKey:""
}
);window[aiName]=aisdk,aisdk.queue&&0===aisdk.queue.length&&aisdk.trackPageView({});

Getting and tracking SP User Properties

There’s a way around all this load-order chaos, you can track user properties along with page views. It can be accomplished with a call to SharePoint API endpoint GetMyProperties, with that response then provided to the trackPageView function. The endpoint can be accessed at the following address (keeping in mind that subsites may precede the /api/ part):

/_api/SP.UserProfiles.PeopleManager/GetMyProperties 

The following code snippet makes use of the Azure documentation sample, but with the following modification: I remove the page track call, and put it inside the return callback for a request to SharePoint API endpoint GetMyProperties.


var sdkInstance="appInsightsSDK";window[sdkInstance]="appInsights";var aiName=window[sdkInstance],aisdk=window[aiName]||function(e){function n(e){t[e]=function(){var n=arguments;t.queue.push(function(){t[e].apply(t,n)})}}var t={config:e};t.initialize=!0;var i=document,a=window;setTimeout(function(){var n=i.createElement("script");n.src=e.url||"https://yourserver.sharepoint.com/SiteAssets/js/ModifiedAppInsightsScript.js",i.getElementsByTagName("script")[0].parentNode.appendChild(n)});try{t.cookie=i.cookie}catch(e){}t.queue=[],t.version=2;for(var r=["Event","PageView","Exception","Trace","DependencyData","Metric","PageViewPerformance"];r.length;)n("track"+r.pop());n("startTrackPage"),n("stopTrackPage");var s="Track"+r[0];if(n("start"+s),n("stop"+s),n("addTelemetryInitializer"),n("setAuthenticatedUserContext"),n("clearAuthenticatedUserContext"),n("flush"),t.SeverityLevel={Verbose:0,Information:1,Warning:2,Error:3,Critical:4},!(!0===e.disableExceptionTracking||e.extensionConfig&&e.extensionConfig.ApplicationInsightsAnalytics&&!0===e.extensionConfig.ApplicationInsightsAnalytics.disableExceptionTracking)){n(""+(r="onerror"));var o=a[r];a[r]=function(e,n,i,a,s){var c=o&&o(e,n,i,a,s);return!0!==c&&t""+r,c},e.autoExceptionInstrumented=!0}return t}({
    instrumentationKey:""
});

window[aiName]=aisdk,aisdk.queue&&0===aisdk.queue.length;

jQuery.ajax({
    url: "/_api/SP.UserProfiles.PeopleManager/GetMyProperties",
     type: "GET",
    headers: { "accept": "application/json;odata=verbose" },
    success: function(data){
        var userinfo = {};
        userinfo["Department"] = true;
        userinfo["Office"] = true;
        var properties = data.d;
        if(properties.UserProfileProperties.results != null && properties.UserProfileProperties.results.length > 0)
        {
            for(var p in properties.UserProfileProperties.results)
            {
                if(userinfo[properties.UserProfileProperties.results[p].Key])
                userinfo[properties.UserProfileProperties.results[p].Key] = properties.UserProfileProperties.results[p].Value;
            }
        }
        appInsights.trackPageView({name:document.title, title:window.location.href, properties:userinfo});
    },
    error: function (xhr) { 
        console.error(xhr.status + ': ' + xhr.statusText); 
    } //End error method
}); // end of Ajax call to GetMyProperties

…This approach does not depend on SharePoint script loading order. Even the jQuery use not necessary since it can just as well be a standard JavaScript XmlHttpRequest.

In the above code, the desired fields are called out, rather than simply copying all user fields (which you could do), but the point is that you usually don’t want to track personally-identifiable info (PII) with web stats. So in the above snippet, I’m tracking simple the department and the office location of the user by iterating through all available properties and copying those values out.

I recommend doing this even in you want PII because there can be a lot of useless junk in the user properties, and App Insights does change by data volume.

Which brings me to my final point…

What about cost?

A lot of people go with Google Analytics because you can do all this and it’s free. However, GA doesn’t offer all the capabilites of Application Insights, so that’s something to consider. Furthermore, a lot of times companies avoid Google services simply because they have a, shall we say, dodgy reputation regarding intellectual property. So companies will go with Adobe Analytics, WebTrends, or other offerings. And Microsoft’s Application Insights is a contender here.

With Azure, nearly every resource you spin up now has a fee attached. Currently App Insights presently charges $2.30/GB of tracked data.
The total cost is depends on the amount of website traffic you get and the content load of your SP site. And typically, SharePoint has a ton of fluffy libraries to load.

I’m going to call a medium trafficked site is one that sees about 1000 visits/day. Let’s pad that figure by rounding up to $3 for easy math, and 30 days in a month; so we have $3 * 30 * 12 = $1080/year using Application Insights.

That’s certainly competitive compared to other platforms. But it should be noted that the reporting aspect of Application Insights has more of a learning curve and probably wants a customer engagement to get a decent dashboard setup for analysis.