Extending Ember with Analytics

Alex DiLiberto

@alex_diliberto

alexdiliberto.com

Web Analytics?

Things to track:

  1. Page Views
    • {{link-to}}
    • this.transitionTo()
    • this.replaceWith()
    • /post/1 → /post/2
    • URL
  2. Actions
    • {{action}}
    • this.send()
    • this.triggerAction()
    • this.sendAction()
    • Simple actions

Page View Tracking

Naïve approach

              
    Ember.Analytics = Ember.Mixin.create({
      trackPageView: function() {
        Ember.run.next(function() {
          var loc = window.location,
              page = loc.hash ? loc.hash.substring(1) : loc.pathname + loc.search;
          ga('send', 'pageview', page);
        });
      }.observes('currentPath')
    });
              
            
http://emberjs.jsbin.com/puzax/12/edit

Bad Idea...

  • Must create previously auto-generated controller:application
  • Even worse, inject this Mixin onto each specifc controller you want to track. #UGHHH!
  • Does not scale well
  • Manually grabbing the URL via window.location
  • Do not have access to the internal router object within the scope of this Mixin
  • Does NOT work for dynamic routes with model changes
  • Does NOT work for loading state or error state
Tomster smelly fish

Winning approach

              
    App.ApplicationRoute = Ember.Route.extend({
      actions: {
        didTransition: function() {
          Ember.run.once(this, function() {
            ga('send', 'pageview', this.router.get('url'));
          });
        }
      }
    });
              
            
http://emberjs.jsbin.com/puzax/14/edit

Much Better!

  • Clean
  • Simply use the auto-generated controller:application
  • Scales well
  • Grabbing the URL via the internal router object
  • Works great in all conditions
    • Route changes
    • Dynamic model changes
    • Loading state
    • Error state

Action Tracking

Naïve approach

              
    
    
              
              
    App.ApplicationRoute = Ember.Route.extend({
      actions: {
        track: function(data) {
          ga('send', 'event', event.target.nodeName, event.type,
             event.target.className, data);
        }
      }
    });
              
            
http://emberjs.jsbin.com/puzax/18/edit

Bad Idea...

  • Litters templates with analytics-only actions
  • No encapsulation
  • Code begins to fragment extremely quickly
  • Not a robust solution
  • Does not scale well
  • Does NOT allow easy-to-use/integrated programmatic action handling
Tomster smelly fish

Winning approach - Part 1

              
    var analyticsObject = Ember.Object.create({
      // Default data to be passed on every request
      _: { site: "My Webapp Name" },
      // Non route-based/global or fallback actions
      _global: { _trackPromise: function(r, s) { return { route: r, status: s }; },
      },
      // Route-specific - e.g. {{action "baz"}} in foo.bar
      //     foo: { //Route name
      //       bar: {  //Leaf-most route name
      //         baz: { var: 'borf' } //Action name
      //       }
      //     }
      products: {
        product: {
          vote: function(v, c, p) { return { vote: v, color: c, product: p }; },
          otherStuff: { var1: 'other-stuff' }
        }
      }
    });
              
            
http://emberjs.jsbin.com/puzax/47/edit

Winning approach - Part 2

              
    // Handles existing template {{action}}'s as well as programattic send()'s
    //  {{action 'transferFunds'}}
    //  this.send('_trackAppEvent', '_trackPromise', 'accounts#model', 'reject');
    Ember.ActionHandler.reopen({
      send: function(actionName) {
        analyticsHandler.apply(this, arguments);
      }
    });
              
            
http://emberjs.jsbin.com/puzax/47/edit

Winning approach - Part 3

              
  // Condensed analyticsHandler
  var analyticsHandler = function(actionName) {
    var router = this.target ? this.target.router : this.router.router,
      activeTrans = router.activeTransition && router.activeTransition.targetName,
      curHandlerInfos = router.currentHandlerInfos,
      activeLeafMostRoute = curHandlerInfos[curHandlerInfos.length - 1].name,
      routeName = activeTrans || activeLeafMostRoute,
      trackObj = aObj.get(routeName+'.'+actionName) || aObj.get('_global.'+actionName);

    if (typeof trackObj == 'function') {
      trackObj = trackObj.apply(this, [].slice.call(arguments, 1));
    }
    if (trackObj) {
      analyticsSendHandlers.action(Em.Object.create(aObj.get('_'), trackObj));
    }
    if (actionName.indexOf('_track') !== 0) {
      this._super.apply(this, arguments);
    }
  };
              
            
http://emberjs.jsbin.com/puzax/47/edit

Much Better!

  • Fully declarative analytics pattern
  • No more littered templates with analytics-specific actions
  • Works for template as well as programmatic based actions
    • {{action}}
    • this.send()
    • this.triggerAction()
    • this.sendAction()
  • Flexibility / Encapsulation
  • Decouples templates from analytics handling
  • Scales extremely well
  • Centralized processing
  • Leverages existing lower level API
Tomster mascot with a hardhat

References

Thanks!

Alex DiLiberto

@alex_diliberto

alexdiliberto.com