SharePoint Online Usage Statistics with JavaScript Injection Add-in

Iron Contributor

Hi

 

I  want to add Google analytics to every site on our Office 365 (Classic Look" ) intranet / site collection.

 

The following post mentions using Google Custom Dimensions which I have now set up to track usage on behalf of my client. 

https://allcloud.com/blog/optimizing-google-analytics-for-sharepoint-and-office-365/

 

Rather rather hack the master page  ( see I was paying attention to Vesa and Waldek) , I am esssentially deploying the Core.EmbedJavaScript Sample with a different payload. 

 

In scenaro1.js I have added this globaly 

   

var googleAnalyticsKey = 'UA-12345678-1';  // key set up for my client 

 

function JavaScript_Embed() {

    loadScript(jQuery, function () {
        $(document).ready(function () {
            var message = "<img src='/_Layouts/Images/STS_ListItem_43216.gif' align='absmiddle'> <font color='#AA0000'>Daniel is attempting to inject GoogleAnalytics into this site... it could get <i>messy</i>!</font>";

 // Execute status setter only after SP.JS has been loaded
 SP.SOD.executeOrDelayUntilScriptLoaded(function () { SetStatusBar(message); }, 'sp.js');


 // Inject my function after SP.JS has been loaded
// TODO need to check/Refactor to ensure if SP.Runtime.js is also loaded before executing the function below
            SP.SOD.executeOrDelayUntilScriptLoaded(SharePointGoogleAnalyticsV2, 'sp.js');

Here is my refactored (hacked!) function based on the orginal in the above post. 

SharePointGoogleAnalyticsV2

// DW
// rewritten to hopefully use SP.SOD.executeOrDelayUntilScriptLoaded()
function SharePointGoogleAnalyticsV2() {


    // Google Analytics funtion for base tracking
    (function (i, s, o, g, r, a, m) {
        i['GoogleAnalyticsObject'] = r; i[r] = i[r] || function () {
            (i[r].q = i[r].q || []).push(arguments)
        }, i[r].l = 1 * new Date(); a = s.createElement(o),
            m = s.getElementsByTagName(o)[0]; a.async = 1; a.src=g; m.parentNode.insertBefore(a, m)
    })(window, document, 'script', 'https://www.google-analytics.com/analytics.js', 'ga');


    // custom code to seed Diminension with metadata from visits to each: Site/Page/List/Item per User
    var clientContext = new SP.ClientContext();
    var site = clientContext.get_site();
    var rootWeb = site.get_rootWeb();
    var web = clientContext.get_web();
    var user = web.get_currentUser();
    clientContext.load(site);
    clientContext.load(rootWeb);
    clientContext.load(web);
    clientContext.load(user);

    var listId = _spPageContextInfo.pageListId;
    var itemId = _spPageContextInfo.pageItemId;
    var list = null;
    var item = null;
    if (listId != null) {
        list = web.get_lists().getById(listId);
        clientContext.load(list, 'Title', 'DefaultViewUrl');

        if (itemId != null) {
            item = list.getItemById(itemId);
            clientContext.load(item, 'Id', 'DisplayName');
        }
    }

    function SPGASend() {
        ga('create', googleAnalyticsKey, 'auto');

        var userNameParts = user.get_loginName().split('|');

        ga('send', 'pageview', {
            'dimension1': rootWeb.get_title(),
            'dimension2': site.get_url(),
            'dimension3': web.get_title(),
            'dimension4': web.get_url(),
            'dimension5': listId,
            'dimension6': (list ? list.get_title() : ''),
            'dimension7': (list ? list.get_defaultViewUrl() : ''),
            'dimension8': itemId,
            'dimension9': (item ? item.get_displayName() : ''),
            'dimension10': (item ? (list.get_defaultViewUrl() + '?ID=' + item.get_id()) : ''),
            'dimension11': user.get_title(),
            'dimension12': userNameParts[userNameParts.length - 1]
        });
    }

    function SPGAError(errObj, errMsg) {
        // Optional error handling.
    }

    clientContext.executeQueryAsync(Function.createDelegate(this, SPGASend), Function.createDelegate(this, SPGAError));
        
  
}

This now compiles and deploys to my Developer site . The Google Analytics Page for my client's tenant shows 1 active user (see below) which hopefully is me.

 

  1. Does my refactor look ok and will insert the script block(s) as per the post - Chrome Developer tools shows scenario1.js is being loaded? I am just not 100% sure the function is working correctly. Normally the analytics can be captured after a few days so I need to make sure all is well before deploying to the Intranet.
  2. If 1) looks good can I deploy the the Add-in to a standard Site collection, so not just the Developer Collection
  3. Will I need to deploy the Add-In to each team site in my standard site collection as essentially each master page on each Team Site will need to be running the script block.  If can I use a provisioning template to add or remove my Add-in on each Team Site?

 

 GoogleAnalytics.PNG

 

 

14 Replies

Instead of using that Add-in model, I would look at using this open source solution, which is based on the PnP responsive UI project and works on the new modern doc libraries also.

 

https://github.com/eoverfield/SP-Custom-Script-Action

 

It will inject your Javascript using the User Custom Action method. Just modify the 2 XML PnP Provisioning template files to reference your .js file. The PS script will upload the .js file to the location you specify in template file and then inject it into every page in the site collection.

 

So you will have to run this PS against each site collection

 

Thanks

Hi

 

Thanks for replying.  I have now cloned and modfied the sample Custom-Script-Action.js as mentionded in your reply  to include the main parts of my Scenario1.js ... I am not 100% this correct as in do I need to wait for sp.js ( and  runtime.js) to be loaded.  

 

 

// embedding of jQuery, and initialization of responsiveness when ready 
// changed to use a CDN
loadScript("https://ajax.aspnetcdn.com/ajax/jQuery/jquery-2.0.2.min.js", function () {
    console.log("jQuery loaded and ready for custom JS");

    // Inject my function after SP.JS has been loaded
    // TODO need to check/Refactor to ensure if SP.Runtime.js is also loaded before executing the function below
    SP.SOD.executeOrDelayUntilScriptLoaded(SharePointGoogleAnalyticsV2, 'sp.js');



});

I will test out shortly as I am meant to also be writing a training course ;-(

 

 

you don't necessarily need that load script for jQuery - that was just a sample they put in.

I assume your function 

(SharePointGoogleAnalyticsV2)

is in the custom.js file that is being deployed by the PS script?

That should work then 

Steve

 

 

I have kept the JQuery load in for the time being and as you you say I have a custom JS which is loaded by the script Action.  I am getting much further thanks to your help.

 

I can now see some of my random log statements (!) and it looks like some of the csom code that

updates the google dimensions is working such as "User" and "List". I will need to debug this to see why the "Web" isn't being loaded .   "Worse case" I could use SPJSCore library but I think this overkill given my modest requirements .

 

 

issues with CustomscriptAction.PNG

Hi 

 

 

I thought I would rewrite the jsom calls to use promises 

 

  // custom code to seed Diminension with metadata from visits to each: Site/Page/List/Item per User
    var clientContext = SP.ClientContext.get_current();
    //
    // get the site collection details
    //


    var site = clientContext.get_site();
    var rootWeb = site.get_rootWeb();
    var web = clientContext.get_web();
    var user = web.get_currentUser();


    clientContext.load(site);
    var promise = clientContext.executeQuery(); // look!  :-)
    promise.done(function () {
        console.log(site.get_title());
    });

    promise.then(function (sArgs) {
        //sArgs[0] == success callback sender
        //sArgs[1] == success callback args
        console.log(site.get_url());


        //
        // get web details
        //
        clientContext.load(web);
        var promise = clientContext.executeQuery(); // look!  :-)
        promise.done(function () {
            console.log(web.get_title());
        });

        promise.then(function (sArgs) {
            //sArgs[0] == success callback sender
            //sArgs[1] == success callback args

            //
            // get User details
            //

            clientContext.load(user);
            var promise = clientContext.executeQuery(); // look!  :-)
            promise.done(function () {



                console.log(user.get_title());
            })
            promise.then(function (sArgs) {
                //sArgs[0] == success callback sender
                //sArgs[1] == success callback args
            }, function (fArgs) {
                //fArgs[0] == fail callback sender
                //fArgs[1] == fail callback args.
                //in JSOM the callback args aren't used much - 
                //the only useful one is probably the get_message() 
                //on the fail callback
                var failmessage = fArgs[1].get_message();
                console.log(failmessage);
            });




        }, function (fArgs) {
            //fArgs[0] == fail callback sender
            //fArgs[1] == fail callback args.
            //in JSOM the callback args aren't used much - 
            //the only useful one is probably the get_message() 
            //on the fail callback
            var failmessage = fArgs[1].get_message();
            console.log(failmessage);
        });

    }, function (fArgs) {
        //fArgs[0] == fail callback sender
        //fArgs[1] == fail callback args.
        //in JSOM the callback args aren't used much - 
        //the only useful one is probably the get_message() 
        //on the fail callback
        var failmessage = fArgs[1].get_message();
        console.log(failmessage);
    });

But sigh .. I get yet another runtime error

 

GoogleAnalytics error again.PNG

Hi, I would first start by making sure sp.js is loaded - add this line above that code

SP.SOD.executeFunc('sp.js', 'SP.ClientContext', sharePointReady);

Where 'sharePointReady' is the function name around your code :

function sharePointReady() {
    clientContext = SP.ClientContext.get_current();
    website = clientContext.get_web();

    clientContext.load(website);
    clientContext.executeQueryAsync(onRequestSucceeded, onRequestFailed);
}

Also, you are calling clientContext.executeQuery (which is not supported using JSOM, you should use executeQueryAsync()

clientContext.load(website);
    clientContext.executeQueryAsync(onRequestSucceeded, onRequestFailed);
}
function onRequestSucceeded() {
    alert(website.get_url());
}
function onRequestFailed(sender, args) {
    alert('Error: ' + args.get_message());
}

If you want to use promises - I have seen some articles where they create a prototype of executeQueryAsync

http://johnliu.net/blog/2015/12/convert-sharepoint-jsoms-executequeryasync-to-promise-in-the-prototy...

 

Another example:

http://blog.qumsieh.ca/2013/10/31/using-jquery-promises-deferreds-with-sharepoint-2013-jsom/

 

Thanks

 

 

Steve,

 

 

  1. Thanks for your reply, I had based my rewrite on John Liu's example which baffled me why I was getting my error

You raise an interesting point my calling loadscript function is currently 

 

loadScript("https://ajax.aspnetcdn.com/ajax/jQuery/jquery-2.0.2.min.js", function () {
    console.log("jQuery loaded but who cares as we are applying Google Analytics.... Yeah!");

    // Inject my function after SP.JS has been loaded
    // TODO need to check/Refactor to ensure if SP.Runtime.js is also loaded before executing the function below
    SP.SOD.executeOrDelayUntilScriptLoaded(SharePointGoogleAnalyticsV3, 'sp.js');



});

I am wondering if I sould 

 

change the sp.SOD to 

 

// embedding of jQuery, and initialization of responsiveness when ready 
// changed to use a CDN
loadScript("https://ajax.aspnetcdn.com/ajax/jQuery/jquery-2.0.2.min.js", function () {
    console.log("jQuery loaded but who cares as we are applying Google Analytics.... Yeah!");

    // Inject my function after SP.JS has been loaded
    // TODO need to check/Refactor to ensure if SP.Runtime.js is also loaded before executing the function below
  
SP.SOD.executeFunc('sp.js', 'SP.ClientContext', SharePointGoogleAnalyticsV3);

});

..  one thing I have now noticed is the log is showing a message which I have commented out in my function with promises.  In the morrning I will manually remove the script I deploy or use powershell to remove old if exists ...  

Steve

 

 I can finally report some success. To make this easier I created two functions, one based on John's example and one I put together by looking at the link you gave and seeing if there is another way to write it.  my second function works and gives no errors in the console.   The PS works like a charm and simply replaces my customSciptAction when I call my set up function.

 

function debugit2() {

    console.log("Debugit2 Started....");
    var ctx = SP.ClientContext.get_current();
    var web = ctx.get_web();
    functionToCall().done(function (returnedValue) {

        // do something with the returned value
        console.log(web.get_title());
    });

    function functionToCall() {

        var dfd = $.Deferred(function () {

            

            ctx.load(web);

            ctx.executeQueryAsync(function () {

                dfd.resolve(web);

            },

                function (sender, args) {

                    //throw an error
                    var failmessage = args[1].get_message();
                    console.log(" oh no not again " + failmessage);
                });

        });

        return dfd.promise( /* do I chain here to say get my user or site or list objects */     );

    }

}

The only thing I need to do now is chain my promises to get the jsom objects: user, list, site  etc, rename my function calls and then re-add my google delegate code.  I think I hit the pain barrier on this ;-( 

 

@Steve Borgwardt

 

 

Just a quickup update ,   

 

I have now tried a few methods for injecting my analytics script into my site or web.   Looks like a script link is better than a script block .  Also I am getting mixed results with a heavily branded site. Thanks for your previous actions as I did use it to inect a simpel script block into my iste

 

Got really stuck on John Liu's  http://johnliu.net/blog/2015/12/convert-sharepoint-jsoms-executequeryasync-to-promise-in-the-prototy...  ...  I seem never to get passed the error 'Protoype not defined' . Spoke to Hugh from Rencore about his custom SOD loader for sp.js and runtime.js but his answer gave me a headache trying to understand it lol.... Level 400 JavaScript springs to mind!

 

Annyway John kindly pointed at this one page add your script .....

 

http://johnliu.net/blog/2015/12/the-safest-future-proof-way-to-brand-your-sharepoint-and-sharepoint-...

 

 

Ok before I am ready to inject my final script I do need to remove the first script action with the client id shown ( see attachment) . It is part of the PnP CoreJavaScriptInjection which despite sucessfully deploying via VS2017 and removing via the UI , I can't find a way of remove this Action link.

Hi, Daniel

 

Glad to hear you are getting closer with your solution. In regards to that first client script action (PnP), I believe you still need that. But you may want to clean everything up first (remove all script actions) and then uses the PnP injection script method.

 

Thanks

@Steve Borgwardt

 

 

I applied the script, yesterday evening on our branded test site .  The good news I am able to see some of my Custom Dimensions on our https://analytics.google.com portal. The bad news is we experienced some unfortunate side effects ...  No ribbon  bar on list settings, some web part pages not showing content.  

 

Before I think of a rewriting say using JSCore & batching I think I will change sequence and also deal wth the clash shown below - this is an Add-in installed years ago that nobody seems to remember anything about ;-(.    As anybody experienced similar issues with Custom Actions? 

 

 

 

 

ExtraCAre Intranet User Custom Actions ECSharePointGoogleAnalytics Installed v2.png

 

I haven't seen that happen unless you are doing something in your script that overrides or manipulates the SP DOM.

Maybe you are right about the sequence number and try that.

 

Thanks

Does this usage analytics inject javascript to only web pages ? Would this work for usage analytics of documents being looked in office online/browser ?

The links I provided are generic for injecting any javascript or css into web pages. It doesn't provide any injection or tracking of documents (online/offline). You will need to check with the analytics vendor on how to tracking documents - probably modifying the links on the pages to include a tracking code, but that is beyond the scope of this thread.