Retrieve Analytics data in Laravel via AJAX call to the Google API

by   PHP JavaScript


Friday, 20 January 2017



Link to Github repo View Demo

When you want to get insights about your website traffic, you generally head over to your Google Analytics account and navigate through the various graphs and metrics. That's all fine, but what if you could bring the most relevant data directly to your backend portal? This certainly adds some convenience because you're likely to visit your backend portal on a more regular basis than your Analytics account. You will also be able to share chosen metrics to all editors of your website without having to disclose any of your secret keys. In this tutorial, I will demonstrate how you can do that in a very simple way with PHP and JavaScript.

Quick note: You will need a Google Analytics account to follow this tutorial. If you haven't already, visit this page to create such an account, it's free.

Context

Google Analytics offers convenient guidelines on how to make your first API requests to their servers. The provided documentation should actually be well enough to get you started, so have a look at it first. Here, we'll simply extend the HelloAnalytics.php example file to retrieve more data than just the profile name and the number of sessions. We will also integrate the whole process into Laravel and make AJAX requests instead of synchronous ones. Why this last move? Well, it appears that retrieving Analytics data can take up its time (we'll come to that later on), and we aren't willing to freeze our app during the process, are we?

Process

Generate public/private key pair

The very first move, as mentionned in the documentation, is to create a client ID. This can be done in 3 simple steps documented under Step 1: Enable the Analytics API. So I leave that to you. Once you have downloaded the json file, place it in hidden google folder that you'll create at the root of your app, like so:

emplacement json file

Install the API through composer

To install the API client, type the following command (Step 2 in the official doc):

composer require google/apiclient:^2.0

At this stage, a question we may ask ourselves is where to save the core HelloAnalytics.php file that will send the api requests? Well, there are various ways to accomplish this. I've decided to place the file inside a new folder named Functions under the app/ directory and rename the file googleAnalytics.php. Since everything within the app/ directory is auto loaded using PSR-4, this makes things quite convenient. Naturally, you are free to come up with your own (better?) solution.

emplacement php file

Set up googleAnalytics.php

Quick tip
To get started with composing your calls to the Google Analytics API, I invite you to play around the query explorer.

As I mentioned, this file is a modified version of the HelloAnalytics.php you'll find in the official doc. It is composed of three functions:

  1. initializeAnalytics(). As its name suggests, this step sets up the connection to the API service.

  2. getFirstProfileId() makes use of the newly created $analytics object to retrieve name and id of the profile view.

  3. getResult(). Here comes the interesting part. We define 3 arrays that we will contain various types of data. The first array ($data1) contains profile info. The second array ($data2) is composed of 5 metrics on 4 different timeframes. The third array ($data3) contains a single metric with spatial dimensions. We'll use a loop to retrieve the data for the 4 different timeframes. Finally we'll gather all our data in a single variable ($data) to be returned to the controller.

What are the most relevant metrics to keep track of on your website?
While there exist numerous blogs and papers out there trying to list what you absolutely need to monitor, I tend to believe that it just narrows down to your application specific goals. So let me avoid the discussion here, as it's off topic for today. For the sake of this tutorial, let's grab five common metrics that I believe are essentials to any blogging application (it also happens that those are the ones I track down for this this blog).

app/Functions/googleAnalytics.php

<?php

// Load the Google API PHP Client Library.
require_once __DIR__.'/../../vendor/autoload.php';

$analytics = initializeAnalytics();
$profile = getFirstProfileId($analytics);
$results = getResults($analytics, $profile);
// printResults($results);

function initializeAnalytics()
{
    // Creates and returns the Analytics Reporting service object.

    // Use the developers console and download your service account
    // credentials in JSON format. Place them in this directory or
    // change the key file location if necessary.
    $KEY_FILE_LOCATION = __DIR__ .'/../../.google/service-account-credentials.json';

    // Create and configure a new client object.
    $client = new Google_Client();
    $client->setApplicationName("Hello Analytics Reporting");
    $client->setAuthConfig($KEY_FILE_LOCATION);
    $client->setScopes(['https://www.googleapis.com/auth/analytics.readonly']);
    $analytics = new Google_Service_Analytics($client);

    return $analytics;
}

function getFirstProfileId($analytics) {
    // Get the list of accounts for the authorized user.
    $accounts = $analytics->management_accounts->listManagementAccounts();

    if (count($accounts->getItems()) > 0) {
        $items = $accounts->getItems();
        $firstAccountId = $items[0]->getId();
        $firstAccountName = $items[0]->getName();

        // Get the list of properties for the authorized user.
        $properties = $analytics->management_webproperties
            ->listManagementWebproperties($firstAccountId);

        if (count($properties->getItems()) > 0) {
            $items = $properties->getItems();
            $firstPropertyId = $items[0]->getId();
            $firstPropertyName = $items[0]->getName();

            // Get the list of views (profiles) for the authorized user.
            $profiles = $analytics->management_profiles
                ->listManagementProfiles($firstAccountId, $firstPropertyId);

            if (count($profiles->getItems()) > 0) {
                $items = $profiles->getItems();
                $profileInfo = [];
                $profileName = $items[0]->getName();
                $profileId = $items[0]->getId();

                $profileInfo = [$profileId, $profileName, $firstPropertyName, $firstAccountName];
                return $profileInfo;

            } else {
                throw new Exception('No views (profiles) found for this user.');
            }
        } else {
            throw new Exception('No properties found for this user.');
        }
    } else {
        throw new Exception('No accounts found for this user.');
    }
}

function getResults($analytics, $profileInfo) {

    // Calls the Core Reporting API and queries for the number of sessions, users, new users, average session duration and bounce rate for the last month (main data).

    // Get profile info
    $data1 = array_slice($profileInfo, 1, 2);

    $analyticsViewId    = 'ga:' . $profileInfo[0];
    $startDate = ['7daysAgo', '14daysAgo', '21daysAgo', '28daysAgo'];
    $endDate = ['today', '7daysAgo', '14daysAgo', '21daysAgo'];
    $metrics1 = 'ga:sessions, ga:users, ga:newUsers, ga:avgSessionDuration, ga:bounceRate';
    $metrics2 = 'ga:sessions';

    // Get main data
    for ($i = 0, $size = count($startDate); $i < $size; $i++) {
        $data2[$i] = $analytics->data_ga->get($analyticsViewId, $startDate[$i], $endDate[$i], $metrics1);
    }

    // Get spatial data
    for ($i = 0, $size = count($startDate); $i < $size; $i++) {
        $data3[$i] = $analytics->data_ga->get($analyticsViewId, $startDate[$i], $endDate[$i], $metrics2, array(
            'dimensions'    => 'ga:country',
            'sort'          => '-ga:sessions',
            'max-results'   => '5'
        ));
    }

    // Gather in an array
    $data = [$data1, $data2, $data3];

    // Return variable to controller
    return $data;
}

Note that we dropped the original printResults() function as we are not interested in printing the results directly from there. Instead, following a typical MVC pattern, we'll call this file from a controller and send the results to the view:

diagram

Controller

Inside our controller the getData() method will be responsible for including the googleAnalytics.php file and returning its results to the view in the form of a single variable. Notice how minimalistic the code is, just 2 lines!

app/Http/Controllers/AnalyticsController.php

<?php namespace App\Http\Controllers;

use Illuminate\Http\Request;

use View;

class AnalyticsController extends Controller
{
    public function index() {

        return View::make('analytics');

    }


    public function getData() {

        include(app_path() . '/Functions/googleAnalytics.php');

        return $results;
    }
}

We obviously also need to define the corresponding route, so add this line to your routing file:

routes/web.php

Route::get('getAnalyticsData', array('as' => 'google.statistics.index', 'uses' => 'AnalyticsController@index'));
Route::post('getAnalyticsData', array('as' => 'google.statistics.ajax', 'uses' => 'AnalyticsController@getData'));

View

As previously stated, we're not going to call Google Analytics synchronously, because this will drastically slow down the loading time of the page. And that's just too bad if you have other elements to display. So, instead we're going to make an AJAX call to the controller and continue on with browsing the page while waiting for the response. The HTML is pretty basic, we simply define all three div elements for the content display.

resources/views/statistics.blade.php

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="utf-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <!-- CSFR token for ajax call -->
        <meta name="_token" content="{{ csrf_token() }}"/>

        <title>Google Analytics</title>

        <link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css" rel="stylesheet" type="text/css">

        <!-- Fonts -->
        <link href="https://fonts.googleapis.com/css?family=Raleway:100,600" rel="stylesheet" type="text/css">
    </head>
    <body>
        <div class="col-md-12">
            <h3 class="text-center">Google Analytics - Website Traffic</h3>
            <p class="text-center"><a href="https://www.google.com/analytics/" target="_blank">Link to Google Analytics</a></p>
            <br/>
            <div class="text-center">
                <button class="btn btn-success send-btn">Get data</button>
            </div>
            <br />
            <div id="analytics_title" class="text-center"></div><br />
            <table class="table" id="analytics_table"></table><br />
            <div id="analytics_country"></div>
        </div><!-- /.col-md-12 -->

        <div id="loader" class="text-center hidden"><img src="{{ asset('ajax-loader.gif') }}" /></div>

    </body>
</html>

I am using Bootstrap to add some styling to the various elements. Nice looking static view, isn't it? Let's append the AJAX logic on the same page. A click on should trigger an asynchronous call to the getAnalyticsData() method in the controller. Once data is retrieved, it is sent back to the view is the form of a single array $results. There simply remains to parse the values to the various div elements for a nice looking display.

resources/views/statistics.blade.php

<script src="//cdnjs.cloudflare.com/ajax/libs/jquery/1.8.0/jquery.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.0.1/js/bootstrap.min.js"></script>

<script>
    $.ajaxSetup({
       headers: { 'X-CSRF-Token' : $('meta[name=_token]').attr('content') }
    });
</script>
<script type="text/javascript" src="https://www.gstatic.com/charts/loader.js"></script>
<script>
    $(document).ready(function(){
        $(".send-btn").click(function() {
            var start = new Date().getTime();
            $(".send-btn").attr("disabled", true);
            $("#loader").removeClass("hidden");
            $.ajax({
                url: "getAnalyticsData",
                type: 'POST',
                success: function(data) {
                    var end = new Date().getTime();

                    // General info (title)
                    title = document.getElementById("analytics_title");
                    title.innerHTML = "<b>Property: </b>" + data[0][1] + " <b>| View: </b>" + data[0][0] + " <b>| Elasped time: </b>" + (end-start)/1000 + " seconds to retrieve data";

                    // Main data (First table)
                    table1 = document.getElementById("analytics_table");
                    var numbers = data[1];
                    table1.innerHTML = "<thead><tr><th></th><th>Sessions</th><th>Users</th><th>New Users</th><th>Average session duration (min)</th><th>Bounce rate</th></thead>";
                    var timeframes = ['Last 7 days', '2 weeks ago', '3 weeks ago', '4 weeks ago', ''];
                    var colors = ['success', 'danger', 'info', 'warning'];
                    function myFunction(item, index) {
                        if (item['rows'] !== null) {
                            table1.innerHTML = table1.innerHTML + "<tr class='" + colors[index] + "'><th>" + timeframes[index] + "</th><td>" + item['rows'][0][0] + "</td><td>" + item['rows'][0][1] + "</td><td>" +item['rows'][0][2] + "</td><td>" + ((item['rows'][0][3])/60).toFixed(2) + "</td><td>" + parseFloat(item['rows'][0][4]).toFixed(2) + "</td></tr>";
                        }
                    }
                    numbers.forEach(myFunction);

                    // Data by country (Second table)
                    var table2 = document.createElement('table'), thead, tbody, th, tr, td, row, cell;
                    table2.className = "table";
                    var timeframes = ['Last 7 days', '2 weeks ago', '3 weeks ago', '4 weeks ago'];
                    var colors = ['success', 'danger', 'info', 'warning'];
                    var headers = ['', '#1 Sessions', '#2 Sessions', '#3 Sessions', '#4 Sessions', '#5 Sessions'];
                    var byCountry = data[2];
                    thead = document.createElement('thead');
                    tr = document.createElement('tr');

                    for (i = 0; i < headers.length; i++) {
                        th = document.createElement('th');
                        thead.appendChild(tr);
                        tr.appendChild(th);
                        th.innerHTML = headers[i];
                    }
                    table2.appendChild(thead);

                    tbody = document.createElement('tbody');
                    for (row = 0; row < byCountry.length; row++) {
                        if (byCountry[row]["rows"] !== null) {
                            var dataByTimeframe = byCountry[row]["rows"];
                            var dataByTimeframeLength = dataByTimeframe.length;

                            tr = document.createElement('tr');
                            tr.className = colors[row];
                            tbody.appendChild(tr);
                            th = document.createElement('th');
                            tr.appendChild(th);
                            th.innerHTML = timeframes[row];
                            for (cell = 0; cell < dataByTimeframeLength; cell++) {
                                td = document.createElement('td');
                                tr.appendChild(td);
                                td.innerHTML = dataByTimeframe[cell][0] + ": " + dataByTimeframe[cell][1];
                            }
                            table2.appendChild(tbody);
                        }      
                    }
                    document.getElementById('analytics_country').appendChild(table2);

                    $('#loader').hide();
                },
                error: function (xhr, ajaxOptions, thrownError) {
                    // empty
                }
            });
        });
    });
</script>

Now, I agree with you that the code looks a bit messy at first sight, mainly because we are generating HTML elements from JavaScript. To help make things a bit clearer, recall that we have 3 different data arrays:

$data[0]: General info about our Analytics account. This helps identifying where the data comes from. It is particularly helpful if you have multiple Analytics accounts, properties or views.

$data[1]: Main data that consists of the sessions, users, new users, average session duration and bounce rate values for the last 4 weeks.

$data[2]: Spatial data that consists of the numbers of sessions in the top 5 countries for the last 4 weeks.

I will not go over the details of the creation of the tables, just be aware that both tables will be created even if you don't have 4 weeks of historical data to supply with.

Conclusion

The Google API is a powerful tool to quickly gain insights about your website traffic. Its implementation is easy in Laravel and it is a perfect candidate for AJAX requests due to the relatively long response time. If you would like to test this code, I have prepared all the relevant files in this Github repo. Just add your own analytics credentials to the root of the project and you are ready to go. Have a look also at the demo, where I graciously share with you some real traffic data from the website you're currently visiting. Thanks for adding to my pageview counter!

Blog Search

About

Hi there! My name is Jean-Marc Kleger and I'm a web developer. Welcome to my blog where a share some tips on how to deal with a selection of challenges encountered in my day-to-day coding workflow. Most articles are related to their own Github repo so that you can quickly experiment the code. Don't hesitate also to make use of the comment section at the end of each post to share your knowledge on the topic. Enjoy the visit!