Google Analytics for SP Modern Search

04 March 2020

ga

PnP Modern Search is an excellent bunch of WebParts that allow searching across the SharePoint. The missing bit is to check what exactly users search on the site, what result was helpful for them, and what filter they used to have satisfying results. To not extend the current code, that in future could be difficult to merge with any changes, with help come SPFx Extension: Application Customer.

Setup PnP Modern Search

From PnP Modern Search, I focused on three webparts: Search Box, Search Results and Search Refiners.

Track all sites where Extension is installed

First, I wanted to track all the sites where the application is installed.

Google Analytics already created a sample Website Tracking (gtag.js) that can be easily use in our extension:

<!-- Global site tag (gtag.js) - Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=UA-159314959-1"></script>
<script>
  window.dataLayer = window.dataLayer || [];
  function gtag(){dataLayer.push(arguments);}
  gtag('js', new Date());

  gtag('config', 'UA-159314959-1');
</script>

After little modification, the script will be appended in the head just after the extension will be loaded on the site.

@override
  public onInit(): Promise<void> {

    var gtag = document.createElement('script');
    gtag.type = 'text/javascript';
    gtag.src = 'https://www.googletagmanager.com/gtag/js?id=UA-159314959-1';
    gtag.async = true;
  
    document.head.appendChild(gtag);

    eval(`
      window.dataLayer = window.dataLayer || [];
      function gtag(){dataLayer.push(arguments);}
      gtag('js', new Date());

      gtag('config', 'UA-159314959-1');
    `)

    return Promise.resolve();
  }

When I debugg application, Google Analytics is showing that an extension is working correctly.

Google Check

The other way to check that tracking code is installed correctly can help the chrome extension: Tag Assistant (by Google). It can show that tracking code is correct, analyse the site and as well record our behaviour on the site to check all events tag, site clicks and other measures that we will add to the site.

Tag Analysis Result

Now, when Google Analytics is on the site, I can start to track events.

Extend code to use analytics.js as well

To track search events I used jQuery to manipulate DOM (get form DOM all information that helped me to track events) To track them I used as well the ga function.

I've added extra code to:

  • Initial ga as a global variable

    declare var ga: any;
  • Created a HTMLScriptElement

    private gaScript: HTMLScriptElement = document.createElement("script");
  • Added code to use analytics.js as well

        if (typeof ga != 'function') {
        this.gaScript.text += `
         (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');`;
      }
  • Initial script on the site

    this.gaScript.text += `
      ga('create', '${this.properties.trackingId}', 'auto');
      ga('require', 'displayfeatures');
      ga('set', 'page', '${this.getFreshCurrentPage()}');
      ga('send', 'pageview');`;
  • Appended this script to the html head

    this.gaScript.type = "text/javascript";
    document.getElementsByTagName("head")[0].appendChild(this.gaScript);

Track events

To track events, I started to use a simple function to send event data into Google Analytics:

    if (typeof ga === 'function') {
        ga('send', {
          'hitType': 'event',
          'eventCategory': 'Search',
          'eventAction': `${eventAction}`,
          'eventLabel': `${eventLabel}`
        });
      }

Search Terms

The value I want to track is on the dynamic field (in my case) with id="TextField50".

<input type="text" id="TextField50" class="ms-TextField-field field-642" placeholder="Enter your search terms..." aria-invalid="false" value="">

but the class use data-sp-feature-tag that can be used to get value when the user clicks the search button or press enter

<div class="Canvas-slideUpIn" data-sp-feature-tag="SearchBoxWebPart web part (Search Box)" data-sp-feature-instance-id="b855fd24-8144-4cb6-a8ec-d825a366f71b" style="opacity: 0;">

To do that I created a two function to get value from the input:

  • onClick (Search button)

      $("[class*='searchBtn']").on('click', (e) => {
        var searchText = $("[data-sp-feature-tag*='SearchBoxWebPart'] input").val();
      });
  • onEnter

    $("[data-sp-feature-tag*='SearchBoxWebPart'] input").on('keypress', (e) => {
        let searchText = $("[data-sp-feature-tag*='SearchBoxWebPart'] input").val();
        if (e.keyCode == 13) {
        }
    });

Search Result

Search result showing on the side with some delay, so I added code to wait until all links render and another wait to be sure that all data will be sent to Google Analytics. All Search result stay in one container, so here was the easy call and loop to get all of them.

    setTimeout(() => {
      this.getSearchRefiners();
      var tags = document.querySelectorAll('.template_contentContainer a');
      tags.forEach((element, i) => {
        setTimeout(() => {
          this.sendEventToGa("Search Result", `${i + 1}: ${element.textContent} - ${element.getAttribute("href")}`);
        }, 500);
      });
    }, 3000);

Search Result Clicked

Inside the Search Result function I've added the event handler to get whatever the user click any link. And here become a challenge as SharePoint ignore any click listeners and is almost impossible to overwrite it by using SPFx Extension. Instead than on using click, mouseup doing similar work for clicking the link.

auxclick is a middle-click to open the site on new tab/window.

The extra code that was added insde the Search Result:

 let clickHandler = (ev: MouseEvent) => {
    this.sendEventToGa(`Search Result Clicked`, `${element.textContent} -  ${element.getAttribute("href")}`);
  };

  element.addEventListener("mouseup", clickHandler);
  element.addEventListener("auxclick", clickHandler);

Search Refiners

The last event I wanted to cover was what search refiners user use and what results back on the window. But I wanted to have only those results that were applied, not showing them during the change. The panel of Search Refiner when one open is always there, but hidden for the user. The filter is applied every time user choose any checkbox, and the search result is rerendered. Send to Google Analytics should be done in this case, when the user closes the panel.

filter Applied

     $("[class*='filterResultBtn']").on('click', (e) => {
  
        document.body.addEventListener('click', elem => {
          var panel = $("[class*='ms-Panel-scrollableContent']");
          var intervalTimer = setInterval(() => {
            if (panel.css("visibility") != "visible") {
              var filters = $("[class*='linkPanelLayout__selectedFilters'] label");
              filters.each((_, elem) => {
                this.sendEventToGa(eventAction, elem.textContent);
                console.log('Search Filter: ', elem.textContent);
              });
  
              clearInterval(intervalTimer);
              this.getSearchResults();
            }
          });
        });
      });

Google Analytics Event Result In Real-Time, we can see all events, and we can create reports based on user behaviour on the PnP Modern Search. Event Result

Github Repository The full code is available on my repo: GAPnPModernSearch

Helpful links