In a previous article, I talked about how you can use ActiveRecord session store to display promotions to your site’s visitors. Since that article was written, the ActiveRecord session store code has been refactored from Rails into its own gem, along with a few other changes that make the code incompatible with Rails 4 or higher.

This is an update to that article, with a more complete tutorial. You can find the code for this article hosted on Github here.

Getting Started

To demonstrate this concept, let’s start by creating a new Rails application:

$ rails new flash_marketing
$ cd flash_marketing
$ rake db:create

Next, install activerecord-session_store:

$ echo "gem 'activerecord-session_store'" >> Gemfile
$ bundle
$ rails g active_record:session_migration
$ rake db:migrate

Then edit config/initializers/session_store.rb and update it to the following:

Rails.application.config.session_store :active_record_store

Custom Session Model

Now we can create a Session model that descends from ActiveRecord::SessionStore::Session. This will allow us to access every user’s session outside of the scope of the current user.

Create app/models/session.rb and add the following content:

class Session < ActiveRecord::SessionStore::Session
end

Normally we can only access a single user’s session data in the views or controllers. Using this model we have access to every user’s session, not just the current user, and from anywhere in our application. This includes background processes, which is what we will be using later on in this tutorial.

Flash Messages

Next, we need to be able to communicate with our site’s users. For that we’ll be taking advantage of Rails’ built in flash notification system. Let’s begin by adding a flash method to our Session model:

class Session < ActiveRecord::SessionStore::Session
  def flash(id, msg)
    data['flash'] ||= {'flashes' => {}, 'discard' => []}
    data['flash']['flashes'][id.to_sym] = msg
  end
end

Now we can display flash messages to any of our site’s visitors. Just keep in mind that we’re not actually using ActionDispatch::Flash, which is what is used in the controllers.

Next, let’s update the application’s layout to display flash messages. Add these lines to app/views/application.html.erb somewhere in the body:

<% flash.each do |id, msg| %>
  <%= content_tag :div, msg, class: "flash_#{id}" %>
<% end %>

See it in Action

So we can actually test this out, let’s remove the default Rails view by adding our own home page:

$ rails g controller home show

And then add that as our root path in config/routes.rb:

Rails.application.routes.draw do
  root to: 'home#show'
end

To see it action, open up the application in your browser. Doing so will automatically create a new session record. Next, open a Rails console so you can see what’s going on:

session = Session.first
# => #<Session id: 1, session_id: "f28ae9f7dd2066023be808c3b337e390", data: "BAh7BkkiEF9jc3JmX3Rva2VuBjoGRUZJIjFZYnFUb25sSlFXVV...", created_at: "2014-06-16 19:49:07", updated_at: "2014-06-16 19:49:07">

If you inspect session.data, you’ll notice that it returns a Hash:

session.data
# => {"_csrf_token"=>"YbqTonlJQWUQKTU/jLQFZVha7EiqN+uFlGO8g5nFgkg="}

This is because it’s automatically serialized for you on the fly. Basically session.data can be treated the same as the session variable in a controller or view, except that you’ll have to call session.save to persist the data. Go ahead and try it out now by calling our custom flash method:

session.flash :notice, "hello world"
session.save

Now refresh your browser, and you’ll see the message appear. Refresh it again, and it won’t appear twice, just like any normal flash message.

Promotions

Let’s put this feature to use by creating a Promotion model that we can use to display promotions to our site’s visitors.

$ rails g model promotion name call_to_action:string active:boolean
$ rake db:migrate

The call_to_action will be the message displayed to the user. Setting active to false will disable the promotion. You can, of course, customize it however you’d like. For example, you might want to have a start and end date, or some prerequisites for targeting specific visitors. For this example we’re keeping it purposefully simple.

Let’s seed in a couple promotions. Add the following content to db/seeds.rb:

Promotion.create!(
  :name => "Clearance",
  :call_to_action => "Save up to 90% on select items! Discount code: OVERSTOCK",
  :active => true
)

Promotion.create!(
  :name => "Upsale",
  :call_to_action => "Add another widget to your cart now for a 25% discount",
  :active => true
)

Make sure to run rake db:seed to load the promotions into your database.

Now let’s edit our Session model and add a method we can use to display the promotions to our site’s visitors:

class Session < ActiveRecord::SessionStore::Session
  def flash(id, msg)
    data['flash'] ||= {'flashes' => {}, 'discard' => []}
    data['flash']['flashes'][id.to_sym] = msg
  end

  def display_promotion(id, track=true)
    viewed_promotions << id if track
    promotion = Promotion.find(id)
    flash :promotion, promotion.call_to_action
  end

  def viewed_promotions
    data['viewed_promotions'] ||= []
  end
end

Go ahead and test that out now if you’d like. You should still have the Rails server and console running. Enter this into the console:

reload!
promotion = Promotion.all.sample
Session.first.tap do |session|
  session.display_promotion(promotion.id, false)
  session.save!
end

Now refresh your browser, and you should see the promotion appear. Refresh it again, and it goes away as expected.

Notice I set track to false for this example. This is because we don’t really want to start keeping track of the promotions while testing it out in the console. Otherwise we’d have to clear our session if we want to test it out more than once.

Running the Promotions

To run the promotions we can use a background processor. For this example I’ll be using the clockwork gem.

Start by installing the gem:

$ echo "gem 'clockwork'" >> Gemfile
$ bundle

Next, create a file called lib/promotions.rb and add the following content:

require 'clockwork'

require_relative '../config/boot'
require_relative '../config/environment'

module Clockwork
  every(1.minute, 'random.promotions') {
    Session.all.each {|session|
      promotion = Promotion.all.sample
      next if session.viewed_promotions.include?(promotion.id)
      session.display_promotion(promotion.id)
      session.save!
    }
  }
end

I won’t go over this file in depth. If you’d like to know exactly how it works you can read the clockwork documentation. Basically what it does is display a random promotion to every visitor on your site every 1 minute.

Run this command to try it out:

$ bundle exec clockwork lib/promotions.rb

Note that bundle exec is required in this case.

The way clockwork functions, the promotion will be run immediately, and then every minute thereafter.

Tracking Active Vistors

By adding data to the sessions, we’re not actually targetting active visitors. The session could have been created days ago, and the visitor might not actually be on the site at the moment.

So let’s fix that. Add the following code to app/controllers/application_controller.rb:

before_action { session[:last_seen_at] = Time.now }

We can now keep track of when the last time a visitor actually used the application. Let’s add a scope for this to the Session model:

class Session < ActiveRecord::SessionStore::Session
  scope :active, lambda { all.select(&:active?) }

  def active?
    data['last_seen_at'] && data['last_seen_at'] > 1.minute.ago
  end
end

Now we can update our clockwork process to target only the active visitors:

require 'clockwork'

require_relative '../config/boot'
require_relative '../config/environment'

module Clockwork
  every(1.minute, 'random.promotions') {
    Session.active.each {|session|
      promotion = Promotion.all.sample
      next if session.viewed_promotions.include?(promotion.id)
      session.display_promotion(promotion.id)
      session.save!
    }
  }
end

Conclusion

This method allows you to push data to your users’ sessions. Alternatively, you could do the same exact thing by adding a few more columns to the Promotion model, and some code to the ApplicationController. It would be much simpler, as you wouldn’t need a background process, however, you’d be pulling the data instead of pushing it.

What’s the difference between pushing and pulling data?

The difference is that the server is directly communicating with your users when you push data. This opens up a lot of possibilities. For example, if you wanted to put your server into maintenance mode, you could have a process that you can run to let all of your site’s active visitors know that you’re about to shut them down.

For example. Add a bin/maintenance file to the application with the following code:

#!/usr/bin/env ruby
APP_PATH = File.expand_path('../../config/application',  __FILE__)
require_relative '../config/boot'
require_relative '../config/environment'

ttl = 30

until ttl == 0 do
  puts "#{ttl}"

  Session.active.each {|session|
    session.flash :alert, "Server shutting down in #{ttl} seconds."
    session.save
  }

  sleep(1)

  ttl -= 1
end

Session.active.each {|session|
  # clear flash messages so visitors don't
  # see the last one next time they sign on
  session.data['flash'] = nil
  session.save
}

Then make it an executable:

$ chmod +x bin/maintenance

Now your site’s active visitors will have a nice timed warning the next time you go to shut down the server. All you have to do is run bin/maintenance before you shut it down. To accomplish the same task via pulling data from the server would be considerably more complex, as you can imagine.

comments powered by Disqus