Skip to content

Commit

Permalink
Track funnel events with Segment.io
Browse files Browse the repository at this point in the history
* Base approach on http://robots.thoughtbot.com/segment-io-and-ruby.
* Add `utm_` campaigns as properties on user creation so cohort analysis can be
  done on a per-campaign basis later on.
* Use special Segment.io `revenue` property and special `campaign` context.
  https://segment.io/docs/tracking-api/track/
* Ignore Redis backup dump file.

https://trello.com/c/gwFd8KCN
  • Loading branch information
Dan Croak committed Aug 21, 2014
1 parent b6daf1f commit 7b3d921
Show file tree
Hide file tree
Showing 20 changed files with 283 additions and 24 deletions.
15 changes: 2 additions & 13 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,17 +1,6 @@
# See http://help.github.com/ignore-files/ for more about ignoring files.
#
# If you find yourself ignoring temporary files generated by your text editor
# or operating system, you probably want to add a global ignore instead:
# git config --global core.excludesfile ~/.gitignore_global

# Ignore bundler config
.env
/.bundle

# Ignore the default SQLite database.
/db/*.sqlite3

# Ignore all logfiles and tempfiles.
/log/*.log
/tmp

.env
dump.rdb
1 change: 1 addition & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ source "https://rubygems.org"
ruby "2.1.2"

gem "active_model_serializers"
gem "analytics-ruby"
gem "angularjs-rails"
gem "bourbon"
gem "coffee-rails"
Expand Down
7 changes: 7 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ GEM
thread_safe (~> 0.1)
tzinfo (~> 0.3.37)
addressable (2.3.6)
analytics-ruby (0.5.4)
faraday (>= 0.8, < 0.10)
faraday_middleware (>= 0.8, < 0.10)
multi_json (~> 1.0)
angularjs-rails (1.2.9)
arel (4.0.2)
ast (2.0.0)
Expand Down Expand Up @@ -76,6 +80,8 @@ GEM
multipart-post (~> 1.2.0)
faraday-http-cache (0.4.0)
faraday (~> 0.8)
faraday_middleware (0.9.0)
faraday (>= 0.7.4, < 0.9)
font-awesome-rails (4.0.3.1)
railties (>= 3.2, < 5.0)
foreman (0.63.0)
Expand Down Expand Up @@ -285,6 +291,7 @@ PLATFORMS

DEPENDENCIES
active_model_serializers
analytics-ruby
angularjs-rails
bourbon
byebug
Expand Down
1 change: 1 addition & 0 deletions app/controllers/activations_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ class CannotActivatePrivateRepo < StandardError; end

def create
if activator.activate(repo, session[:github_token])
analytics.track_activated(repo)
render json: repo, status: :created
else
report_exception(
Expand Down
13 changes: 13 additions & 0 deletions app/controllers/application_controller.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
class ApplicationController < ActionController::Base
protect_from_forgery
before_filter :force_https
before_filter :capture_campaign_params
before_filter :authenticate
after_filter :set_csrf_cookie_for_ng
helper_method :current_user, :signed_in?
Expand All @@ -19,6 +20,14 @@ def force_https?
true
end

def capture_campaign_params
session[:campaign_params] ||= {
utm_campaign: params[:utm_campaign],
utm_medium: params[:utm_medium],
utm_source: params[:utm_source],
}
end

def authenticate
unless signed_in?
redirect_to sign_in_path
Expand All @@ -33,6 +42,10 @@ def current_user
@current_user ||= User.where(remember_token: session[:remember_token]).first
end

def analytics
@analytics ||= Analytics.new(current_user, session[:campaign_params])
end

def set_csrf_cookie_for_ng
if protect_against_forgery?
cookies['XSRF-TOKEN'] = form_authenticity_token
Expand Down
1 change: 1 addition & 0 deletions app/controllers/deactivations_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ def create
repo = current_user.repos.find(params[:repo_id])

if activator.deactivate(repo, session[:github_token])
analytics.track_deactivated(repo)
render json: repo, status: :created
else
report_exception(
Expand Down
22 changes: 18 additions & 4 deletions app/controllers/sessions_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ def new
end

def create
create_session
user = find_user || create_user
create_session_for(user)
redirect_to root_path
end

Expand All @@ -16,10 +17,23 @@ def destroy

private

def create_session
user = User.where(github_username: github_username).first_or_create
session[:github_token] = github_token
def find_user
if user = User.where(github_username: github_username).first
Analytics.new(user).track_signed_in
end

user
end

def create_user
user = User.create(github_username: github_username)
Analytics.new(user, session[:campaign_params]).track_signed_up
user
end

def create_session_for(user)
session[:remember_token] = user.remember_token
session[:github_token] = github_token
end

def destroy_session
Expand Down
2 changes: 2 additions & 0 deletions app/controllers/subscriptions_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ class FailedToActivate < StandardError; end

def create
if activator.activate(repo, github_token) && create_subscription
analytics.track_subscribed(repo)
render json: repo, status: :created
else
activator.deactivate(repo, github_token)
Expand All @@ -19,6 +20,7 @@ def destroy
repo = current_user.repos.find(params[:repo_id])

if activator.deactivate(repo, session[:github_token]) && delete_subscription
analytics.track_unsubscribed(repo)
render json: repo, status: :created
else
report_activation_error("Failed to unsubscribe and deactivate repo")
Expand Down
60 changes: 60 additions & 0 deletions app/models/analytics.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
class Analytics
class_attribute :backend
self.backend = AnalyticsRuby

def initialize(user, params = {})
@user = user
@params = params
end

def track_signed_up
track(event: "Signed Up", context: { campaign: campaign_params })
end

def track_signed_in
track(event: "Signed In")
end

def track_activated(repo)
track(event: "Activated Public Repo", properties: repo_properties(repo))
end

def track_deactivated(repo)
track(event: "Deactivated Public Repo", properties: repo_properties(repo))
end

def track_reviewed(repo)
track(event: "Reviewed Repo", properties: repo_properties(repo))
end

def track_subscribed(repo)
track(event: "Subscribed Private Repo", properties: repo_properties(repo))
end

def track_unsubscribed(repo)
track(event: "Unsubscribed Private Repo", properties: repo_properties(repo))
end

private

def track(options)
backend.track({
active_repos_count: user.repos.active.count,
user_id: user.id,
}.merge(options))
end

def campaign_params
{
medium: params[:utm_medium],
name: params[:utm_campaign],
source: params[:utm_source],
}.reject { |_, value| value.blank? }
end

def repo_properties(repo)
{ name: repo.full_github_name, revenue: repo.price }
end

attr_reader :params, :user
end
8 changes: 8 additions & 0 deletions app/services/build_runner.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ def run
if repo && relevant_pull_request?
repo.builds.create!(violations: violations)
commenter.comment_on_violations(violations, pull_request)
track_reviewed_repo_for_each_user
end
end

Expand Down Expand Up @@ -37,4 +38,11 @@ def pull_request
def repo
@repo ||= Repo.active.where(github_id: payload.github_repo_id).first
end

def track_reviewed_repo_for_each_user
repo.users.each do |user|
analytics = Analytics.new(user)
analytics.track_reviewed(repo)
end
end
end
51 changes: 51 additions & 0 deletions lib/fake_analytics_ruby.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
class FakeAnalyticsRuby
def initialize
@tracked_events = EventsList.new([])
end

def track(options)
@tracked_events << options
end

delegate :tracked_events_for, to: :tracked_events

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_keys?(options)
events.any? do |event|
(options.to_a - event.to_a).empty?
end
end

private

attr_reader :events
end
end
3 changes: 3 additions & 0 deletions spec/controllers/activations_controller_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@
expect(response.body).to eq RepoSerializer.new(repo).to_json
expect(activator).to have_received(:activate).
with(repo, AuthenticationHelper::GITHUB_TOKEN)
expect(analytics).to have_tracked("Activated Public Repo").
for_user(membership.user).
with(properties: { name: repo.full_github_name, revenue: repo.price })
end
end

Expand Down
3 changes: 3 additions & 0 deletions spec/controllers/deactivations_controller_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@
repo,
AuthenticationHelper::GITHUB_TOKEN
)
expect(analytics).to have_tracked("Deactivated Public Repo").
for_user(membership.user).
with(properties: { name: repo.full_github_name, revenue: repo.price })
end
end

Expand Down
29 changes: 29 additions & 0 deletions spec/controllers/subscriptions_controller_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@
with(repo, AuthenticationHelper::GITHUB_TOKEN)
expect(RepoSubscriber).to have_received(:subscribe).
with(repo, membership.user, "cardtoken")
expect(analytics).to have_tracked("Subscribed Private Repo").
for_user(membership.user).
with(properties: { name: repo.full_github_name, revenue: repo.price })
end

it "updates the current user's email address" do
Expand Down Expand Up @@ -59,3 +62,29 @@
end
end
end

describe SubscriptionsController, "#destroy" do
it "unsubscribes the user to the repo" do
membership = create(:membership)
repo = membership.repo
activator = double(:repo_activator, deactivate: true)
RepoActivator.stub(new: activator)
RepoSubscriber.stub(unsubscribe: true)
stub_sign_in(membership.user)

delete(
:destroy,
repo_id: repo.id,
card_token: "cardtoken",
format: :json
)

expect(activator).to have_received(:deactivate).
with(repo, AuthenticationHelper::GITHUB_TOKEN)
expect(RepoSubscriber).to have_received(:unsubscribe).
with(repo, membership.user)
expect(analytics).to have_tracked("Unsubscribed Private Repo").
for_user(membership.user).
with(properties: { name: repo.full_github_name, revenue: repo.price })
end
end
Loading

0 comments on commit 7b3d921

Please sign in to comment.