Skip to content

Commit

Permalink
basic features for analytics js
Browse files Browse the repository at this point in the history
  • Loading branch information
Lars Brillert committed Aug 25, 2014
1 parent a36c214 commit a0c7849
Show file tree
Hide file tree
Showing 16 changed files with 487 additions and 8 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,4 @@ spec/reports
test/tmp
test/version_tmp
tmp
.DS_Store
74 changes: 72 additions & 2 deletions lib/rack/tracker.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,77 @@
require "rack"
require "tilt"
require "active_support/core_ext/hash"
require "active_support/json"

require "rack/tracker/version"
require "rack/tracker/handler"
require "rack/tracker/google_analytics/google_analytics"
require "rack/tracker/facebook/facebook"

module Rack
module Tracker
# Your code goes here...
class Tracker
EVENT_TRACKING_KEY = 'tracker'

def initialize(app, &block)
@app = app
@handlers = Rack::Tracker::HandlerSet.new(&block)
end

def call(env)
@status, @headers, @body = @app.call(env)
return [@status, @headers, @body] unless html?
response = Rack::Response.new([], @status, @headers)

env[EVENT_TRACKING_KEY] = {} unless env[EVENT_TRACKING_KEY]

This comment has been minimized.

Copy link
@jhilden

jhilden Aug 27, 2014

Contributor

what about simply env[EVENT_TRACKING_KEY] ||= {} ?


session = env["rack.session"]
if response.ok?
# Write out the events now

# Get any stored events from a redirection
stored_events = session.delete(EVENT_TRACKING_KEY) if session

env[EVENT_TRACKING_KEY].merge!(stored_events) unless stored_events.nil?
elsif response.redirection? && session
# Store the events until next time
env["rack.session"][EVENT_TRACKING_KEY] = env[EVENT_TRACKING_KEY]
end

@body.each { |fragment| response.write inject(env, fragment) }
@body.close if @body.respond_to?(:close)

response.finish
end

private

def html?; @headers['Content-Type'] =~ /html/; end

def inject(env, response)
@handler = @handlers.first.name.new(env, @handlers.first.options)
response.gsub(%r{</head>}, @handler.render + "</head>")
end

class HandlerSet
Handler = Struct.new(:name, :options)

def initialize(&block)
@handlers = []
self.instance_exec(&block) if block_given?
end

def handler(name, opts = {}, &block)
@handlers << Handler.new(name, opts)
end

def first
@handlers.first
end

def to_a
@handlers
end

end
end
end
25 changes: 25 additions & 0 deletions lib/rack/tracker/facebook/facebook.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
class Rack::Tracker::Facebook < Rack::Tracker::Handler

# options do
# locale 'de_DE'
# app_id
# custom_audience_id
# end

# event do
# id
# value
# currency
# end

# position :body

def event
env[:rack_tracker][:facebook][:event] rescue {}
end

def render
Tilt.new( File.join( File.dirname(__FILE__), 'template/facebook.erb') ).render(self)
end

end
36 changes: 36 additions & 0 deletions lib/rack/tracker/facebook/template/facebook.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<script type="text/javascript">
(function() {
var _fbq = window._fbq || (window._fbq = []);
if (!_fbq.loaded) {
var fbds = document.createElement('script');
fbds.async = true;
fbds.src = '//connect.facebook.net/en_US/fbds.js';
var s = document.getElementsByTagName('script')[0];
s.parentNode.insertBefore(fbds, s);
_fbq.loaded = true;
}
})();
window._fbq = window._fbq || [];

</script>

<% if options[:custom_audience_id] %>
<script type="text/javascript">
window._fbq.push(['addPixelId', '<%= options[:custom_audience_id] %>']);
window._fbq.push(["track", "PixelInitialized", {}]);
</script>

<noscript>
<img height="1" width="1" alt="" style="display:none" src="https://www.facebook.com/tr?id=<%= options[:custom_audience_id] %>&amp;ev=PixelInitialized">
</noscript>
<% end %>

<% if event.any? %>
<script type="text/javascript">
window._fbq.push(['track', '<%= event[:pixel_id] %>', {'value': '<%= event[:value] %>', 'currency': '<%= event[:currency] %>'}]);
</script>

<noscript>
<img height="1" width="1" alt="" style="display:none" src="https://www.facebook.com/offsite_event.php?id=<%= event[:pixel_id] %>&amp;value=<%= event[:value] %>&amp;currency=<%= event[:currency] %>">
</noscript>
<% end %>
20 changes: 20 additions & 0 deletions lib/rack/tracker/google_analytics/google_analytics.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
class Rack::Tracker::GoogleAnalytics < Rack::Tracker::Handler
class Event < Struct.new(:category, :action, :label, :value)
def write
{ hitType: 'event', eventCategory: self.category, eventAction: self.action, eventLabel: self.label, eventValue: self.value }.select{|k,v| v }.to_json
end
end

def events
env['tracker.google_analytics.events'] || []
end

def tracker
options[:tracker].try(:call, env) || options[:tracker]
end

def render
Tilt.new( File.join( File.dirname(__FILE__), 'template/google_analytics.erb') ).render(self)
end

end
43 changes: 43 additions & 0 deletions lib/rack/tracker/google_analytics/template/google_analytics.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<script type="text/javascript">

<% if tracker %>
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
})(window,document,'script','//www.google-analytics.com/analytics.js','ga');

ga('create', '<%= tracker %>', <%= options.slice(:cookieDomain).to_json %>);

<% if options[:enhanced_link_attribution] %>
ga('require', 'linkid', 'linkid.js');
<% end %>

<% if options[:advertising] %>
ga('require', 'displayfeatures');
<% end %>

<% if options[:ecommerce] %>
ga('require', 'ecommerce', 'ecommerce.js');
<% end %>

<% if options[:anonymize_ip] %>
ga('set', 'anonymizeIp', true);
<% end %>

<% if options[:adjusted_bounce_rate_timeouts] %>
<% options[:adjusted_bounce_rate_timeouts].each do |timeout| %>
setTimeout(ga('send', 'event', '<%= "#{timeout.to_s}_seconds" %>', 'read'),<%= timeout*1000 %>);
<% end %>
<% end %>

<% end %>

<% events.each do |var| %>
ga('send', <%= var.write() %>);
<% end %>

<% if tracker %>
ga('send', 'pageview');
<% end %>

</script>
17 changes: 17 additions & 0 deletions lib/rack/tracker/handler.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
class Rack::Tracker::Handler
# options do
# cookie_domain "foo"
# end

attr_accessor :options
attr_accessor :env

def initialize(env, options = {})
self.env = env
self.options = options
end

def render
raise ArgumentError.new('needs implementation')
end
end
Empty file added lib/rack/tracker/railtie.rb
Empty file.
2 changes: 1 addition & 1 deletion lib/rack/tracker/version.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
module Rack
module Tracker
class Tracker
VERSION = "0.0.1"
end
end
6 changes: 6 additions & 0 deletions rack-tracker.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,13 @@ Gem::Specification.new do |spec|
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
spec.require_paths = ["lib"]

spec.add_dependency "rack", "~> 1.5.2"
spec.add_dependency "tilt", "~> 2.0.1"
spec.add_dependency "activesupport", "~> 4.1.5"

spec.add_development_dependency "bundler", "~> 1.5"
spec.add_development_dependency "rake"
spec.add_development_dependency "rspec", "~> 3.0.0"
spec.add_development_dependency "capybara", "~> 2.4.1"
spec.add_development_dependency "pry-debugger"
end
9 changes: 9 additions & 0 deletions spec/fixtures/dummy.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<script type="text/javascript">
<% if dummy_alert %>
alert('<%= dummy_alert %>');
<% else %>
alert('this is a dummy class');
<% end %>

console.log('<%= options[:foo] %>');
</script>
43 changes: 43 additions & 0 deletions spec/handler/facebook_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
RSpec.describe Rack::Tracker::Facebook do
def env
{}
end

describe 'with custom audience id' do
subject { described_class.new(env, custom_audience_id: 'custom_audience_id').render }

it 'will push the tracking events to the queue' do
expect(subject).to match(%r{window._fbq.push\(\['addPixelId', 'custom_audience_id'\]\)})
expect(subject).to match(%r{window._fbq.push\(\["track", "PixelInitialized", \{\}\]\)})
end

it 'will add the noscript fallback' do
expect(subject).to match(%r{https://www.facebook.com/tr\?id=custom_audience_id&amp;ev=PixelInitialized})
end
end

describe 'with events' do
def env
{
rack_tracker: {
facebook: {
event: {
pixel_id: '123456789',
value: '23',
currency: 'EUR',
}
}
}
}
end
subject { described_class.new(env).render }

it 'will push the tracking events to the queue' do
expect(subject).to match(%r{\['track', '123456789', \{'value': '23', 'currency': 'EUR'\}\]})
end

it 'will add the noscript fallback' do
expect(subject).to match(%r{https://www.facebook.com/offsite_event.php\?id=123456789&amp;value=23&amp;currency=EUR})
end
end
end
93 changes: 93 additions & 0 deletions spec/handler/google_analytics_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
RSpec.describe Rack::Tracker::GoogleAnalytics do

def env
{misc: 'foobar'}
end

describe "with events" do
describe "default" do
def env
{"tracker.google_analytics.events" => [
Rack::Tracker::GoogleAnalytics::Event.new("Users", "Login", "Standard")
]}
end

subject { described_class.new(env, tracker: 'somebody', cookieDomain: "railslabs.com").render }
it "will show events" do
expect(subject).to match(%r{ga\('send', {\"hitType\":\"event\",\"eventCategory\":\"Users\",\"eventAction\":\"Login\",\"eventLabel\":\"Standard\"}\)})
end
end

describe "with a event value" do
def env
{"tracker.google_analytics.events" => [
Rack::Tracker::GoogleAnalytics::Event.new("Users", "Login", "Standard", 5)
]}
end

subject { described_class.new(env, tracker: 'somebody', cookieDomain: "railslabs.com").render }
it "will show events with values" do
expect(subject).to match(%r{ga\('send', {\"hitType\":\"event\",\"eventCategory\":\"Users\",\"eventAction\":\"Login\",\"eventLabel\":\"Standard\",\"eventValue\":5}\)},)
end
end
end

describe "with custom domain" do
subject { described_class.new(env, tracker: 'somebody', cookieDomain: "railslabs.com").render }

it "will show asyncronous tracker with cookieDomain" do
expect(subject).to match(%r{ga\('create', 'somebody', {\"cookieDomain\":\"railslabs.com\"}\)})
expect(subject).to match(%r{ga\('send', 'pageview'\)})
end
end

describe "with enhanced_link_attribution" do
subject { described_class.new(env, tracker: 'happy', enhanced_link_attribution: true).render }

it "will embedded the linkid plugin script" do
expect(subject).to match(%r{linkid.js})
end
end

describe "with advertising" do
subject { described_class.new(env, tracker: 'happy', advertising: true).render }

it "will require displayfeatures" do
expect(subject).to match(%r{ga\('require', 'displayfeatures'\)})
end
end

describe "with e-commerce" do
subject { described_class.new(env, tracker: 'happy', ecommerce: true).render }

it "will require the ecommerce plugin" do
expect(subject).to match(%r{ga\('require', 'ecommerce', 'ecommerce\.js'\)})
end
end

describe "with anonymizeIp" do
subject { described_class.new(env, tracker: 'happy', anonymize_ip: true).render }

it "will set anonymizeIp to true" do
expect(subject).to match(%r{ga\('set', 'anonymizeIp', true\)})
end
end

describe "with dynamic tracker" do
subject { described_class.new(env, { tracker: lambda { |env| return env[:misc] }}).render }

it 'will call tracker lambdas to obtain tracking codes' do
expect(subject).to match(%r{ga\('create', 'foobar', {}\)})
end
end

describe 'adjusted bounce rate' do
subject { described_class.new(env, tracker: 'afake', adjusted_bounce_rate_timeouts: [15, 30]).render }

it "will add timeouts to push read events" do
expect(subject).to match(%r{ga\('send', 'event', '15_seconds', 'read'\)})
expect(subject).to match(%r{ga\('send', 'event', '30_seconds', 'read'\)})
end
end

end
Loading

0 comments on commit a0c7849

Please sign in to comment.