A Study on using KeenIO with Laravel for Action Tracking

Posted by Adam Engebretson on September 27, 2013

KeenIO is a powerful, yet easy-to-use custom analytics engine. In essence, they serve as an event log for your apps. The greatest part is that you can create your event types and properties on the fly! Let's take a look at how I chose to utilize this functionality in Laravel .

Installing KeenIO

The first step is to get yourself an account at KeenIO. Once you have an account, you'll be presented with a brand new "Project". The KeenIO FAQs defines a Project as follows:

A project amounts to a data silo. The information in one project isn’t available to other projects. Practically speaking, in the mobile world, a project is an app.

You should keep events inside this project isolated from any other applications that you use.

Next, it's time to install KeenIO. I created a config file in my Laravel installation to house my KeenIO project ID, write key, and read key. I thought it wise to not include the master API key, but do so if you please. KeenIO has a php library that's community-contributed and provides the basic functions for interacting with the KeenIO API. You can install it through composer:

"keen-io/keen-io": "0.9"

I then added a Class Alias to my /app/config/app.php file:

'KeenIO' => 'KeenIO\Service\KeenIO',

The KeenIO class is conveniently designed so that you only need to instantiate and configure it once in your app. I did this inside my App::before() filter:

// Instantiate KeenIO
KeenIO::configure(
    Config::get('keen.project_id'),
    Config::get('keen.write_key'),
    Config::get('keen.read_key')
);

Once you have instantiated KeenIO, you can simply use the addEvent() method to create events. Again, creating event collections and parameters on-the-fly is a no-brainer. There's no need to configure them in KeenIO's UI at all.

Track Events

I decided to create model event listeners for each of the models I'd like to track in my app, and threw them all in a file called /app/keen.php. In order for this file to be loaded in your app, all you need to do is require it in /start/global.php:

require app_path().'/keen.php';

The contents of this file are here:

<?php

$models = [
  'Branch',
  'Category',
  'Club',
  'CMSApp',
  'CMSAppModel',
  'CMSAppRecord',
  'Content',
  'Group',
  'Layout',
  'Menu',
  'Sheet',
  'Submission',
  'Tracker',
  'User',
  'Widget',
];

foreach($models as $model) {
  $model::created(function($m) use ($model) {
    KeenIO::addEvent('actions', [
      'action' => 'create',
      'model' => $model,
      'id' => $m->id,
      'user' => @Sentry::getUser()->id,
      'club' => Session::get('club')
    ]);
  });
  $model::updated(function($m) use ($model) {
    KeenIO::addEvent('actions', [
      'action' => 'update',
      'model' => $model,
      'id' => $m->id,
      'user' => @Sentry::getUser()->id,
      'club' => Session::get('club')
    ]);
  });
  $model::deleted(function($m) use ($model) {
    KeenIO::addEvent('actions', [
      'action' => 'delete',
      'model' => $model,
      'id' => $m->id,
      'user' => @Sentry::getUser()->id,
      'club' => Session::get('club')
    ]);
  });
  $model::restored(function($m) use ($model) {
    KeenIO::addEvent('actions', [
      'action' => 'restore',
      'model' => $model,
      'id' => $m->id,
      'user' => @Sentry::getUser()->id,
      'club' => Session::get('club')
    ]);
  });
}

As you can see, I'm simply creating event listeners for each of the Eloquent models in my app, and telling KeenIO to add an event to the actions collection with the following parameters:

  • action
  • model
  • id
  • user
  • club

Display Analytics

Finally, I added some analytics to my app's dashboard with some simple javascript:

<script type="text/javascript">
var Keen=Keen||{configure:function(e){this._cf=e},addEvent:function(e,t,n,i){this._eq=this._eq||[],this._eq.push([e,t,n,i])},setGlobalProperties:function(e){this._gp=e},onChartsReady:function(e){this._ocrq=this._ocrq||[],this._ocrq.push(e)}};(function(){var e=document.createElement("script");e.type="text/javascript",e.async=!0,e.src=("https:"==document.location.protocol?"https://":"http://")+"dc8na2hxrj29i.cloudfront.net/code/keen-2.1.0-min.js";var t=document.getElementsByTagName("script")[0];t.parentNode.insertBefore(e,t)})();

// Configure the Keen object with your Project ID and (optional) access keys.
Keen.configure({
  projectId: "{{Config::get('aaacms.keen.project_id')}}",
  writeKey: "{{Config::get('aaacms.keen.write_key')}}", // required for sending events
  readKey: "{{Config::get('aaacms.keen.read_key')}}"    // required for doing analysis
});
Keen.onChartsReady(function() {
  var userActivity = new Keen.Series("actions", {
    analysisType: "count",
    timeframe: "this_week",
    interval: "daily",
    groupBy: "user"
  });
  userActivity.draw(document.getElementById("userActivity"), {
    title: "User Activity This Week",
    width: "100%",
    labelMapping: {
    @foreach(User::all() as $user)
      {{$user->id}}: "{{strtok($user->name, ' ')}}",
    @endforeach
    },
    backgroundColor: "#f7f7f7",
    lineWidth: "2"
  });

  var contentCreation = new Keen.Series("actions", {
    analysisType: "count_unique",
    timeframe: "this_week",
    interval: "daily",
    targetProperty: "id",
    filters: [{"property_name":"action","operator":"eq","property_value":"create"},{"property_name":"model","operator":"eq","property_value":"Content"}]
  });
  contentCreation.draw(document.getElementById("contentCreation"), {
    title: "Content Creation This Week",
    width: "100%",
    showLegend: false,
    chartAreaWidth: "75%",
    backgroundColor: "#f7f7f7",
    lineWidth: "2"
  });

  var formSubmissions = new Keen.Series("actions", {
    analysisType: "count",
    timeframe: "this_7_days",
    interval: "daily",
    filters: [{"property_name":"action","operator":"eq","property_value":"create"},{"property_name":"model","operator":"eq","property_value":"Submission"}]
  });
  formSubmissions.draw(document.getElementById("formSubmissions"), {
    title: "Form Submissions This Week",
    width: "100%",
    showLegend: false,
    contentAreaWidth: "75%",
    backgroundColor: "#f7f7f7",
    lineWidth: "2"
  });
});

There was virtually no markup required for the HTML, other than to style my graphs:

<div class="widget fluid">
  <div class="whead">
    <h6>Statistics</h6>
    <div class="clear"></div>
  </div>
  <div class="formRow">
    <div class="grid6">
      <div id="userActivity"></div>
    </div>
    <div class="grid6">
      <div id="contentCreation"></div>
    </div>
    <div class="clear"></div>
  </div>
  <div class="formRow">
    <div class="grid12">
      <div id="formSubmissions"></div>
    </div>
    <div class="clear"></div>
  </div>
</div>

Now, we can easily see graphs for the various queries that I've configured here.