Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to communicate between an observer and the controller

I understand that a Rails observer should not have direct access to the controller. That makes sense, there is no telling what context the observer is going to be called from. However I have a case that I think merits indirect communication between the two and I'm wondering how to achieve it.

logging and writing analytics events

I would like to use an observer to trigger certain events in Google Analytics. The way this currently works is that the application controller has a method that logs the event and then the application.html.erb template prints the relevant javascript into the page:

class ApplicationController < ActionController::Base

  def logGAEvent category, action, opt_hash={}
    event = { :category => category,
              :action => action,
              :label => opt_hash[:label],
              :value => opt_hash[:value]}
    (session[:ga_events] ||= []) << event
  end

end

Application.html.erb

<html>
  <head>
    ...

    <script type="text/javascript">
      <%= print_ga_events_js %>
    </script>

  </head>
  ...
</html>

Example event:

class UsersController < ApplicationController
  ...

  def create
     ...
     if @new_user
       logGAEvent('user', 'signup')
     end
  end
end

Why I would like to communicate between an observer and the controller

At the moment the logGAEvent method is called in controllers after certain noteworthy events (someone signs up, creates a new profile etc).

It would be a far nicer pattern to abstract the majority of these events into an observer. This would tidy up the controller and would also make the tracking less ad-hoc. However, once they go into the observer, there still needs to be a way for the template to access the observer's data and print it out.

What I would like to be able to do

Since the observer shouldn't really know about the controller, what I would like to do is record these events into a one-time buffer so that they are flushed at the end of each call but they are also accessible to the controller to write into the document:

class UserObserver < ActiveRecord::Observer
  after_create user
    # I have no idea what would constitue request.buffer but this is
    # the pattern I'm looking for
    request.buffer.ga_events << createGAEvent('user', 'create')
  end
end
end

application.html.erb (using buffer)

Application.html.erb

<html>
  <head>
    ...
    <script type="text/javascript">
    <%= print_ga_events_js(request.buffer.ga_events) %>
    </script>

  </head>
  ...
</html>

Is this in some way possible? It doesn't seem like an unreasonable design pattern to me and it would make the app much cleaner.

like image 905
Peter Nixey Avatar asked Sep 17 '11 11:09

Peter Nixey


1 Answers

I had a similar issue with adding mixpanel events to an application. The way I solved it was I used Controller filters instead of observers. You get very similar sorts of behavior to an observer but with a filter you actually have access to the request and response objects.

Here's an example filter, loosely adapted from mine but with enough changes I wouldn't call it tested code:

module GoogleAnalytics
  class TrackCreation   
    def self.for(*args)
        self.new(*args)
    end

    # pass in the models we want to track creation of
    def initialize(*args)
        @models = args.map do |name|
            name.to_s.tableize.singularize
        end
    end

    # This is called after the create action has happened at the controller level
    def filter(controller)
        #
        controller.params.select{ |model, model_params| filter_for.include? model }.each do |model, model_params|
            #you may want to perform some sort of validation that the creation was successful - say check the response code
            track_event(model, "create", model_params)
        end
  end

    def track_event(category, action, *args)
        flash[:ga] ||= []
        flash[:ga] << ["_trackEvent", type, args]
    end

    def filter_for
        @models
    end
  end
end

class ApplicationController < ActionController::Base
after_filter GoogleAnalytics::TrackCreation.for(:foo, :bar), :only => :create
end

Within the filter I just set up a custom flash: flash[:mixpanel] ||= []. Flash is nice because it will automagically clear contents between requests. There are a couple gotchas I ran into:

  1. Be aware of the difference between flash and flash.now. Flash is great when you are tracking a resource being created, destroyed, or any other situation where a redirect will happen. But if you actually want to track an event in the immediate response you'll want to use flash.now.
  2. An after_filter has access to the response, but you don't get to change that response. So if you want to spit out tracking events for the current request you need to do it before the response is built.

To actually output the analytics code itself just update your flash partial to take each element in flash[:ga] and write it out.

like image 188
Yosem Sweet Avatar answered Oct 11 '22 23:10

Yosem Sweet