---
title: Segment.io and Ruby
teaser: Use Segment.io's Ruby library to track usage patterns within a Rails application.
tags: web,rails,testing,analytics
author: Josh Clayton
published_on: 2014-07-30
---

[Segment.io][segment] 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][analytics-ruby], 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][facade-pattern] manages it for us.

```ruby
# 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`:

```ruby
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:

```ruby
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][fake-object], acceptance testing analytics events becomes a breeze:

```ruby
# 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.

```ruby
# 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][fake-object], `FakeAnalyticsRuby`, which we define:

```ruby
# 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`][rspec-matchers].

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:

```ruby
# 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:

```ruby
# 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.

[segment]: https://segment.io
[analytics-ruby]: https://github.com/segmentio/analytics-ruby
[facade-pattern]: http://c2.com/cgi/wiki?FacadePattern
[fake-object]: https://thoughtbot.com/blog/fake-it
[rspec-matchers]: https://gist.github.com/joshuaclayton/79987a3273ff125069dd
