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.
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:
To actually output the analytics code itself just update your flash partial to take each element in flash[:ga] and write it out.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With