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.
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.
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.