SessionLogger

The session logger is simply a observer that automatically writes data from the session to models with matching columns.

What it is good for

Session Logger is great for recording session based activites you want to group together.

Questions session logger can help answer:

  • How many users came in from a given ad campaign?

  • How many invites did those users from that campaign send?

  • How many purchases came when we directed people to this landing page?

  • Pretty much anything about users activity over a single session…

Setup

First install the gem:

gem install session_logger

And/or add it to your Gemfile

gem session_logger

Then create the initializer:

rails generate session_logger:initializer

Finally add ‘enable_session_logger’ to Application controller (or any controller you want logging to occur)

ApplicationController < ActionController::Base
  enable_session_logging
end

Usage

1 Create metadata columns on the models you want metadata recorded on

add_column :users, :sl_campaign, :string
add_column :users, :sl_source, :string
add_column :users, :sl_medium, :string
add_column :users, :sl_term, :string
add_column :users, :sl_content, :string
add_column :users, :sl_landing_page, :string
add_column :users, :sl_session_id, :string

2 Be sure to prefix metadata columns with the model prefix (“sl_” by default)

SessionLogger.configure do |config|
config.model_prefix = "sl_" # means columns are like: sl_XXXX
end

3 Simply set that same prefixed name in the session and that metadata will be stored anytime a observed record is created during that session.

ApplicationController < ActionController::Base
enable_session_logging
before_filter :pull_campaign_data

def pull_campaign_data
  first_time = session['meta_stored'].blank?
  mapping = {"utm_source" => "sl_source", "utm_medium" => "sl_medium",
             "utm_term" => "sl_term", "utm_content" => "sl_content",
             "utm_campaign" =>"sl_campaign"}

  new_values = {}
  mapping.each do |param_key, session_key|
    value = params[param_key]
    if value.present? && session[session_key] != value
      new_values[session_key] = value
    end
  end

  #Override all the campaign values when new ones come in
  #new values will be empty when link doesn't have utm params
  if (new_values.present? || first_time)
    #Store the landingpage with the utm params, and session id
    new_values["sl_landing_uri"] = request.env["REQUEST_URI"]
    new_values["sl_session_id"] = request.session_options[:id]
    session.merge!(new_values)
  end

  session['meta_stored'] = true
end

end

4 Later segment your data naturally using the columns you defined

User.where(:sl_campaign => "facebook_ads_1").count
...

How it works

We create a new observer type that has access to both the controller and the model level call backs that will write metadata found in the session to models at creation time.

When a session key is found that matches a prefixed column for the model that is being created we simply append that data before save to the model effectively appending the metadata for that users session.

Gotchas

If you start monitoring a ton of metadata around the user you have to switch to db sessions instead of cookie store.

Don’t clear your whole session haphazardly. Thats where the metadata is stored

Choice to use columns instead of polymorphic table

One of the primary conerns we had when we were developing this gem was the trade offs between adding additional namespaced columns on the records and creating a seperate polymorphic join table for the logging information.

At the end of the day we choose to implement the logging solution with namespaced columns. Here are the pros and cons.

Columns solution:

Pros:

  • Speed and simplicity in creation of reports.

  • Speed of writes of those rows. (one write call vs two)

  • Customization of metadata per model without Null columns

Cons:

  • Pain to add so many repeated columns to potentially many tables

  • (OK) Slow down in read of individual rows (can be mitigated with scoped select statements)

  • (OK) Metadata doesn’t have its own timestamp (not really applicable as it should be the same as the write time of the original record)

The only real con that we couldn’t work around is the initial number of columns you will need to add to all your models. Many of them repeat in practice and this will give you a bad taste in your mouth but it seems like a better solution overall.

Optimizations

As a performance optimization you can reduce the list of models to observe in the initializer:

config.session_logger.logged_models = [MODEL_CONSTANTS,...]

License

This project rocks and uses MIT-LICENSE.