Segment.io is a tool that lets developers identify users and track their activity through an application. Armed with this information, developers can understand usage patterns through Segment.io’s integrations with Mixpanel, Klaviyo, Google Analytics, and many others.
Segment.io provides a Ruby gem, AnalyticsRuby, to make it
easy to track custom events in Rails applications. In a project I worked on
recently, we chose to encapsulate interaction with AnalyticsRuby
within one
class. Rather than always passing the user’s id, email, name, city, and state
to AnalyticsRuby
and conditionally sending the identifier from Google
Analytics, our Analytics
facade manages it for us.
# app/models/analytics.rb
class Analytics
class_attribute :backend
self.backend = AnalyticsRuby
def initialize(user, client_id = nil)
@user = user
@client_id = client_id
end
def track_user_creation
identify
track(
{
user_id: user.id,
event: 'Create User',
properties: {
city_state: user.zip.to_region
}
}
)
end
def track_user_sign_in
identify
track(
{
user_id: user.id,
event: 'Sign In User'
}
)
end
private
def identify
backend.identify(identify_params)
end
attr_reader :user, :client_id
def identify_params
{
user_id: user.id,
traits: user_traits
}
end
def user_traits
{
email: user.email,
first_name: user.first_name,
last_name: user.last_name,
city_state: user.city_state,
}.reject { |key, value| value.blank? }
end
def track(options)
if client_id.present?
options.merge!(
context: {
'Google Analytics' => {
clientId: client_id
}
}
)
end
backend.track(options)
end
end
Because the application had very little JavaScript and no client-side events we needed to track, we decided to use the Ruby library. Depending on the goals and types of events, oftentimes using both Ruby and JavaScript Segment.io libraries makes sense.
To use Analytics
, we first defined a handful of methods on
ApplicationController
:
class ApplicationController < ActionController::Base
include Clearance::Controller
def current_user
super || Guest.new
end
def analytics
@analytics ||= Analytics.new(current_user, google_analytics_client_id)
end
def google_analytics_client_id
google_analytics_cookie.gsub(/^GA\d\.\d\./, '')
end
def google_analytics_cookie
cookies['_ga'] || ''
end
end
With the analytics
method available on ApplicationController
, we
were free to fire any events we wanted:
class UsersController < Clearance::UsersController
private
def url_after_create
analytics.track_user_creation
super
end
end
class SessionsController < Clearance::SessionsController
private
def url_after_create
analytics.track_user_sign_in
super
end
end
Tracking events for non-GET requests is much easier when using the Ruby
library - imagine conditionally rendering JavaScript within your application
layout using Rails’ flash
!
With a test fake, acceptance testing analytics events becomes a breeze:
# spec/features/user_signs_in_spec.rb
require 'spec_helper'
feature 'User signs in' do
scenario 'successfully' do
user = create :user, password: 'password'
visit root_path
click_on 'Sign in'
fill_in 'Email', with: user.email
fill_in 'Password', with: 'password'
click_button 'Sign in'
expect(analytics).to have_tracked('Sign In User').for_user(user)
expect(analytics).to have_identified(user)
end
end
This spec should look pretty familiar to those who’ve written features with RSpec and Capybara before. After exercising sign in functionality, we verify the analytics backend is tracking events correctly by ensuring we’ve tracked the appropriate event for the right person, as well as properly identified that person.
# spec/support/analytics.rb
RSpec.configure do |config|
config.around :each do |example|
cached_backend = Analytics.backend
example.run
Analytics.backend = cached_backend
end
end
module Features
def analytics
Analytics.backend
end
end
# spec/spec_helper.rb
RSpec.configure do |config|
config.before :each, type: :feature do
Analytics.backend = FakeAnalyticsRuby.new
end
end
Here, we configure the analytics
and ensure it’s using a
fake, FakeAnalyticsRuby
, which we define:
# lib/fake_analytics_ruby.rb
class FakeAnalyticsRuby
def initialize
@identified_users = []
@tracked_events = EventsList.new([])
end
def identify(user)
@identified_users << user
end
def track(options)
@tracked_events << options
end
delegate :tracked_events_for, to: :tracked_events
def has_identified?(user, traits = { email: user.email })
@identified_users.any? do |user_hash|
user_hash[:user_id] == user.id &&
traits.all? do |key, value|
user_hash[:traits][key] == value
end
end
end
private
attr_reader :tracked_events
class EventsList
def initialize(events)
@events = events
end
def <<(event)
@events << event
end
def tracked_events_for(user)
self.class.new(
events.select do |event|
event[:user_id] == user.id
end
)
end
def named(event_name)
self.class.new(
events.select do |event|
event[:event] == event_name
end
)
end
def has_properties?(options)
events.any? do |event|
(options.to_a - event[:properties].to_a).empty?
end
end
private
attr_reader :events
end
end
FakeAnalyticsRuby
implements the part of the same interface as
AnalyticsRuby
(from the Ruby gem), namely #identify
and #track
, and
maintains internal state for our RSpec matchers have_tracked
and
have_identified
.
With the chainable with_properties
, we can ensure additional information,
like the user’s city and state, are passed along when firing the “Create User”
event:
# spec/features/guest_signs_up_spec.rb
require 'spec_helper'
feature 'Guest signs up' do
scenario 'successfully' do
complete_registration city: 'Boston', state: 'MA'
expect(analytics).to have_tracked('Create User').
for_user(User.last).
with_properties({
city_state: 'Boston, MA'
})
end
end
To test Analytics
at a unit level, we stub and spy:
# spec/models/analytics_spec.rb
describe Analytics do
describe '#track_user_sign_in' do
it 'notifies AnalyticsRuby of a user signing in' do
AnalyticsRuby.stub(:track)
user = build_stubbed(:user)
analytics = Analytics.new(user)
analytics.track_user_sign_in
expect(AnalyticsRuby).to have_received(:track).with(
{
user_id: user.id,
event: 'Sign In User'
}
)
end
end
end
In this app, we tracked seven different events and tested each appropriately. This solution worked well for a number of reasons:
- with an injectible fake, we were able to easily test events were triggered with the correct properties
- with a facade to simplify interaction with
AnalyticsRuby
, we were able to layer additional functionality like tracking with Google Analytics with relative ease - with two custom RSpec matchers, we were able to add expressive assertions against fired events
- with no JavaScript event requirements, we were able to use Ruby and avoid
patterns like using
flash
to conditionally render JavaScript triggering Segment.io events
Segment.io is a solid platform upon which we can develop robust event tracking, and with a bit of work, integrating this tracking into a Rails app is a great way to gain insight into your customers.