rack-tracker 0.0.1 → 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: f5d422a3b9c89bb3594c892b498efbd590008469
4
- data.tar.gz: 48405d43a9ddab1569886c927930fda3958767d0
3
+ metadata.gz: 42c882f19b618a8ce401d37e6dd5baba8dcc2335
4
+ data.tar.gz: b841bbdbac1fb8752bbddf6be2c526286a337c04
5
5
  SHA512:
6
- metadata.gz: 8f65ac1d2b4ed1bb0500ffda34bea41f9205cc48e16ca90496402ac6bbcc2b52c8262db72df9df87f3ab3563671626b627c9ee50136dca0703052a67a5567541
7
- data.tar.gz: 44c838bd7e679cc923c593d670b0f21b7d56dfceabf285a0dba0d21bfe0364596646c02eafb282ff4d480ede6eb99734c2824f3b9b711a4d7b4f0d28a27797cc
6
+ metadata.gz: ad51b4b1fd58c9dcc2f0985d8066fe13b58504cdf4cb6d1397285bb67ae729ea3fdffb119bcd41c2abec7924da3c8887a1668f03468b70b4f941bc8f7cb90e16
7
+ data.tar.gz: bb7a62cde667581a270cc4036b1413ce15be8c1ad490569210060763c572713611d5bc9ecf1ca3b35640402e0c2eee39c1a6fba29d6d027702eefda7b0d5fdb6
data/.gitignore CHANGED
@@ -15,3 +15,4 @@ spec/reports
15
15
  test/tmp
16
16
  test/version_tmp
17
17
  tmp
18
+ .DS_Store
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --require spec_helper
2
+ --color
@@ -1,4 +1,6 @@
1
1
  language: ruby
2
2
  rvm:
3
3
  - 1.9.3
4
+ - 2.0.0
4
5
  - 2.1.2
6
+ - jruby
data/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  [![Code Climate](https://codeclimate.com/github/railslove/rack-tracker/badges/gpa.svg)](https://codeclimate.com/github/railslove/rack-tracker)
4
4
 
5
- TODO: Write a gem description
5
+ [![Build Status](https://travis-ci.org/railslove/rack-tracker.svg?branch=master)](https://travis-ci.org/railslove/rack-tracker)
6
6
 
7
7
  ## Installation
8
8
 
@@ -20,11 +20,47 @@ Or install it yourself as:
20
20
 
21
21
  ## Usage
22
22
 
23
- TODO: Write usage instructions here
23
+ Add it to your middleware stack
24
+
25
+ config.middleware.use(Rack::Tracker) do
26
+ handler :google_analytics, { tracker: 'U-XXXXX-Y' }
27
+ end
28
+
29
+ This will add Google Analytics as a tracking handler. `Rack::Tracker` some with
30
+ support for Google Analytics and Facebook. Others might be added in the future
31
+ but you can easily write your own handlers.
32
+
33
+ ### Google Analytics
34
+
35
+ * `:anonymize_ip` - sets the tracker to remove the last octet from all IP addresses, see https://developers.google.com/analytics/devguides/collection/gajs/methods/gaJSApi_gat?hl=de#_gat._anonymizeIp for details.
36
+ * `:cookie_domain` - sets the domain name for the GATC cookies. Defaults to `auto`.
37
+ * `:site_speed_sample_rate` - Defines a new sample set size for Site Speed data collection, see https://developers.google.com/analytics/devguides/collection/gajs/methods/gaJSApiBasicConfiguration?hl=de#_gat.GA_Tracker_._setSiteSpeedSampleRate
38
+ * `:adjusted_bounce_rate_timeouts` - An array of times in seconds that the tracker will use to set timeouts for adjusted bounce rate tracking. See http://analytics.blogspot.ca/2012/07/tracking-adjusted-bounce-rate-in-google.html for details.
39
+ * `:enhanced_link_attribution` - Enables [Enhanced Link Attribution](https://developers.google.com/analytics/devguides/collection/analyticsjs/advanced#enhancedlink).
40
+ * `:advertising` - Enables [Display Features](https://developers.google.com/analytics/devguides/collection/analyticsjs/display-features).
41
+ * `:ecommerce` - Enables [Ecommerce Tracking](https://developers.google.com/analytics/devguides/collection/analyticsjs/ecommerce).
42
+
43
+ ### Facebook
44
+
45
+ * `custom_audience_id`
46
+
47
+
48
+ ## Server side events
49
+
50
+ When using `rack-tracker` within a Rails application you can track events from
51
+ your controller and the tracking snippit will then populated with this data.
52
+
53
+ ```ruby
54
+ def show
55
+ tracker do
56
+ google_analytics category: 'foo'
57
+ end
58
+ end
59
+ ```
24
60
 
25
61
  ## Contributing
26
62
 
27
- 1. Fork it ( http://github.com/<my-github-username>/rack-tracker/fork )
63
+ 1. Fork it ( http://github.com/railslove/rack-tracker/fork )
28
64
  2. Create your feature branch (`git checkout -b my-new-feature`)
29
65
  3. Commit your changes (`git commit -am 'Add some feature'`)
30
66
  4. Push to the branch (`git push origin my-new-feature`)
data/Rakefile CHANGED
@@ -1 +1,6 @@
1
1
  require "bundler/gem_tasks"
2
+ require 'rspec/core/rake_task'
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task default: :spec
@@ -1,7 +1,88 @@
1
+ require "rack"
2
+ require "tilt"
3
+ require "active_support/core_ext/class/attribute"
4
+ require "active_support/core_ext/hash"
5
+ require "active_support/json"
6
+
1
7
  require "rack/tracker/version"
8
+ require "rack/tracker/extensions"
9
+ require 'rack/tracker/railtie' if defined?(Rails)
10
+ require "rack/tracker/handler"
11
+ require "rack/tracker/handler_delegator"
12
+ require "rack/tracker/controller"
13
+ require "rack/tracker/google_analytics/google_analytics"
14
+ require "rack/tracker/facebook/facebook"
2
15
 
3
16
  module Rack
4
- module Tracker
5
- # Your code goes here...
17
+ class Tracker
18
+ EVENT_TRACKING_KEY = 'tracker'
19
+
20
+ def initialize(app, &block)
21
+ @app = app
22
+ @handlers = Rack::Tracker::HandlerSet.new(&block)
23
+ end
24
+
25
+ def call(env)
26
+ @status, @headers, @body = @app.call(env)
27
+ return [@status, @headers, @body] unless html?
28
+ response = Rack::Response.new([], @status, @headers)
29
+
30
+ env[EVENT_TRACKING_KEY] = {} unless env[EVENT_TRACKING_KEY]
31
+
32
+ session = env["rack.session"]
33
+ if response.ok?
34
+ # Write out the events now
35
+
36
+ # Get any stored events from a redirection
37
+ stored_events = session.delete(EVENT_TRACKING_KEY) if session
38
+
39
+ env[EVENT_TRACKING_KEY].deep_merge!(stored_events) { |key, old, new| Array.wrap(old) + Array.wrap(new) } unless stored_events.nil?
40
+ elsif response.redirection? && session
41
+ # Store the events until next time
42
+ env["rack.session"][EVENT_TRACKING_KEY] = env[EVENT_TRACKING_KEY]
43
+ end
44
+
45
+ @body.each { |fragment| response.write inject(env, fragment) }
46
+ @body.close if @body.respond_to?(:close)
47
+
48
+ response.finish
49
+ end
50
+
51
+ private
52
+
53
+ def html?; @headers['Content-Type'] =~ /html/; end
54
+
55
+ def inject(env, response)
56
+ @handlers.each(env) do |handler|
57
+ response.gsub!(%r{</#{handler.position}>}, handler.render + "</#{handler.position}>")
58
+ end
59
+ response
60
+ end
61
+
62
+ class HandlerSet
63
+ class Handler
64
+ def initialize(name, options)
65
+ @name = name
66
+ @options = options
67
+ end
68
+
69
+ def init(env)
70
+ @name.new(env, @options)
71
+ end
72
+ end
73
+
74
+ def initialize(&block)
75
+ @handlers = []
76
+ self.instance_exec(&block) if block_given?
77
+ end
78
+
79
+ def handler(name, opts = {}, &block)
80
+ @handlers << Handler.new(Rack::Tracker::HandlerDelegator.handler(name), opts)
81
+ end
82
+
83
+ def each(env = {}, &block)
84
+ @handlers.map{|h| h.init(env)}.each(&block)
85
+ end
86
+ end
6
87
  end
7
88
  end
@@ -0,0 +1,11 @@
1
+ module Rack
2
+ class Tracker
3
+ module Controller
4
+ def tracker(&block)
5
+ if block_given?
6
+ Rack::Tracker::HandlerDelegator.new(env).instance_exec(&block)
7
+ end
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,8 @@
1
+ require 'ostruct'
2
+
3
+ # Backport of 2.0.0 stdlib ostruct#to_h
4
+ class OpenStruct
5
+ def to_h
6
+ @table.dup
7
+ end unless method_defined? :to_h
8
+ end
@@ -0,0 +1,22 @@
1
+ class Rack::Tracker::Facebook < Rack::Tracker::Handler
2
+ self.position = :body
3
+
4
+ # options do
5
+ # locale 'de_DE'
6
+ # app_id
7
+ # custom_audience_id
8
+ # end
9
+
10
+ # event do
11
+ # id
12
+ # value
13
+ # currency
14
+ # end
15
+
16
+ # position :body
17
+
18
+ def render
19
+ Tilt.new( File.join( File.dirname(__FILE__), 'template/facebook.erb') ).render(self)
20
+ end
21
+
22
+ end
@@ -0,0 +1,40 @@
1
+ <script type="text/javascript">
2
+ (function() {
3
+ var _fbq = window._fbq || (window._fbq = []);
4
+ if (!_fbq.loaded) {
5
+ var fbds = document.createElement('script');
6
+ fbds.async = true;
7
+ fbds.src = '//connect.facebook.net/en_US/fbds.js';
8
+ var s = document.getElementsByTagName('script')[0];
9
+ s.parentNode.insertBefore(fbds, s);
10
+ _fbq.loaded = true;
11
+ }
12
+ })();
13
+ window._fbq = window._fbq || [];
14
+
15
+ </script>
16
+
17
+ <% if options[:custom_audience_id] %>
18
+ <script type="text/javascript">
19
+ window._fbq.push(['addPixelId', '<%= options[:custom_audience_id] %>']);
20
+ window._fbq.push(["track", "PixelInitialized", {}]);
21
+ </script>
22
+
23
+ <noscript>
24
+ <img height="1" width="1" alt="" style="display:none" src="https://www.facebook.com/tr?id=<%= options[:custom_audience_id] %>&amp;ev=PixelInitialized">
25
+ </noscript>
26
+ <% end %>
27
+
28
+ <% if events.any? %>
29
+ <script type="text/javascript">
30
+ <% events.each do |event| %>
31
+ window._fbq.push(['track', '<%= event["pixel_id"] %>', {'value': '<%= event["value"] %>', 'currency': '<%= event["currency"] %>'}]);
32
+ <% end %>
33
+ </script>
34
+
35
+ <noscript>
36
+ <% events.each do |event| %>
37
+ <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"] %>">
38
+ <% end %>
39
+ </noscript>
40
+ <% end %>
@@ -0,0 +1,33 @@
1
+ class Rack::Tracker::GoogleAnalytics < Rack::Tracker::Handler
2
+ class Event < OpenStruct
3
+ def write
4
+ ['send', event].to_json.gsub(/\[|\]/, '')
5
+ end
6
+
7
+ def event
8
+ { hitType: 'event' }.merge(attributes).compact
9
+ end
10
+
11
+ def attributes
12
+ Hash[to_h.map { |k,v| ['event' + k.to_s.capitalize, v] }]
13
+ end
14
+ end
15
+
16
+ class Ecommerce < Struct.new(:action, :payload)
17
+ def write
18
+ [self.action, self.payload.compact].to_json.gsub(/\[|\]/, '')
19
+ end
20
+ end
21
+
22
+ def tracker
23
+ options[:tracker].try(:call, env) || options[:tracker]
24
+ end
25
+
26
+ def render
27
+ Tilt.new( File.join( File.dirname(__FILE__), 'template', 'google_analytics.erb') ).render(self)
28
+ end
29
+
30
+ def self.track(name, event)
31
+ { name.to_s => [Event.new(event)] }
32
+ end
33
+ end
@@ -0,0 +1,43 @@
1
+ <script type="text/javascript">
2
+
3
+ <% if tracker %>
4
+ (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
5
+ (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
6
+ m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
7
+ })(window,document,'script','//www.google-analytics.com/analytics.js','ga');
8
+
9
+ ga('create', '<%= tracker %>', <%= options.slice(:cookieDomain).to_json %>);
10
+
11
+ <% if options[:enhanced_link_attribution] %>
12
+ ga('require', 'linkid', 'linkid.js');
13
+ <% end %>
14
+
15
+ <% if options[:advertising] %>
16
+ ga('require', 'displayfeatures');
17
+ <% end %>
18
+
19
+ <% if options[:ecommerce] %>
20
+ ga('require', 'ecommerce', 'ecommerce.js');
21
+ <% end %>
22
+
23
+ <% if options[:anonymize_ip] %>
24
+ ga('set', 'anonymizeIp', true);
25
+ <% end %>
26
+
27
+ <% if options[:adjusted_bounce_rate_timeouts] %>
28
+ <% options[:adjusted_bounce_rate_timeouts].each do |timeout| %>
29
+ setTimeout(ga('send', 'event', '<%= "#{timeout.to_s}_seconds" %>', 'read'),<%= timeout*1000 %>);
30
+ <% end %>
31
+ <% end %>
32
+
33
+ <% end %>
34
+
35
+ <% events.each do |var| %>
36
+ ga(<%= var.write() %>);
37
+ <% end %>
38
+
39
+ <% if tracker %>
40
+ ga('send', 'pageview');
41
+ <% end %>
42
+
43
+ </script>
@@ -0,0 +1,24 @@
1
+ class Rack::Tracker::Handler
2
+ class_attribute :position
3
+ self.position = :head
4
+
5
+ attr_accessor :options
6
+ attr_accessor :env
7
+
8
+ def initialize(env, options = {})
9
+ self.env = env
10
+ self.options = options
11
+ end
12
+
13
+ def events
14
+ env.fetch('tracker', {})[self.class.to_s.demodulize.underscore] || []
15
+ end
16
+
17
+ def render
18
+ raise NotImplementedError.new('needs implementation')
19
+ end
20
+
21
+ def self.track(name, event)
22
+ raise NotImplementedError.new("class method `#{__callee__}` is not implemented.")
23
+ end
24
+ end
@@ -0,0 +1,48 @@
1
+ class Rack::Tracker::HandlerDelegator
2
+ class << self
3
+ def handler(method_name)
4
+ new.handler(method_name)
5
+ end
6
+ end
7
+
8
+ attr_accessor :env
9
+
10
+ def initialize(env={})
11
+ @env = env
12
+ end
13
+
14
+ def method_missing(method_name, *args, &block)
15
+ if respond_to?(method_name)
16
+ write_event(handler(method_name).track(method_name, *args, &block))
17
+ else
18
+ super
19
+ end
20
+ end
21
+
22
+ def write_event(event)
23
+ if env.key?('tracker')
24
+ self.env = env['tracker'].deep_merge!(event)
25
+ else
26
+ self.env['tracker'] = event
27
+ end
28
+ end
29
+
30
+ def respond_to?(method_name, include_private=false)
31
+ handler(method_name).respond_to?(:track, include_private)
32
+ end
33
+
34
+ def handler(method_name)
35
+ return method_name if method_name.kind_of?(Class)
36
+
37
+ _handler = method_name.to_s.camelize
38
+ ["Rack::Tracker::#{_handler}", _handler].detect do |const|
39
+ begin
40
+ return const.constantize
41
+ rescue NameError
42
+ false
43
+ end
44
+ end
45
+
46
+ raise ArgumentError, "No such Handler: #{_handler}"
47
+ end
48
+ end
@@ -0,0 +1,11 @@
1
+ module Rack
2
+ class Tracker
3
+ class Railtie < ::Rails::Railtie
4
+ initializer "rack-tracker.configure_controller" do |app|
5
+ ActiveSupport.on_load :action_controller do
6
+ include Rack::Tracker::Controller
7
+ end
8
+ end
9
+ end
10
+ end
11
+ end
@@ -1,5 +1,5 @@
1
1
  module Rack
2
- module Tracker
3
- VERSION = "0.0.1"
2
+ class Tracker
3
+ VERSION = "0.0.2"
4
4
  end
5
5
  end
@@ -9,7 +9,7 @@ Gem::Specification.new do |spec|
9
9
  spec.authors = ["Lars Brillert", "Marco Schaden"]
10
10
  spec.email = ["lars@railslove.com", "marco@railslove.com"]
11
11
  spec.summary = %q{Tracking made easy}
12
- spec.description = %q{Don’t fool around with adding tracking partials to your app concentrate on the things that matter.}
12
+ spec.description = %q{Don’t fool around with adding tracking partials to your app and concentrate on the things that matter.}
13
13
  spec.homepage = "https://github.com/railslove/rack-tracker"
14
14
  spec.license = "MIT"
15
15
 
@@ -18,6 +18,14 @@ Gem::Specification.new do |spec|
18
18
  spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
19
  spec.require_paths = ["lib"]
20
20
 
21
+ spec.add_dependency "rack", "~> 1.5.2"
22
+ spec.add_dependency "tilt", "~> 1.4.1"
23
+ spec.add_dependency "activesupport", "~> 4.1.0"
24
+
21
25
  spec.add_development_dependency "bundler", "~> 1.5"
22
26
  spec.add_development_dependency "rake"
27
+ spec.add_development_dependency "rspec", "~> 3.0.0"
28
+ spec.add_development_dependency "capybara", "~> 2.4.1"
29
+ spec.add_development_dependency "actionpack", "~> 4.1.5"
30
+ spec.add_development_dependency "pry"
23
31
  end
@@ -0,0 +1,3 @@
1
+ <script type="text/javascript">
2
+ anotherFunction("tracks-event-from-down-under", "<%= options[:custom_key] %>");
3
+ </script>
@@ -0,0 +1,9 @@
1
+ <script type="text/javascript">
2
+ <% if dummy_alert %>
3
+ alert('<%= dummy_alert %>');
4
+ <% else %>
5
+ alert('this is a dummy class');
6
+ <% end %>
7
+
8
+ console.log('<%= options[:foo] %>');
9
+ </script>
@@ -0,0 +1,3 @@
1
+ <script type="text/javascript">
2
+ myAwesomeFunction("tracks", "like", "<%= track_me[:like] %>", "<%= options[:custom_key] %>");
3
+ </script>
@@ -0,0 +1,8 @@
1
+ <html>
2
+ <head>
3
+ <title>Metal Layout</title>
4
+ </head>
5
+ <body>
6
+ <%= yield %>
7
+ </body>
8
+ </html>
@@ -0,0 +1 @@
1
+ <h1>welcome to metal#index</h1>
@@ -0,0 +1,47 @@
1
+ RSpec.describe Rack::Tracker::Facebook do
2
+ def env
3
+ {}
4
+ end
5
+
6
+ it 'will be placed in the body' do
7
+ expect(described_class.position).to eq(:body)
8
+ expect(described_class.new(env).position).to eq(:body)
9
+ end
10
+
11
+ describe 'with custom audience id' do
12
+ subject { described_class.new(env, custom_audience_id: 'custom_audience_id').render }
13
+
14
+ it 'will push the tracking events to the queue' do
15
+ expect(subject).to match(%r{window._fbq.push\(\['addPixelId', 'custom_audience_id'\]\)})
16
+ expect(subject).to match(%r{window._fbq.push\(\["track", "PixelInitialized", \{\}\]\)})
17
+ end
18
+
19
+ it 'will add the noscript fallback' do
20
+ expect(subject).to match(%r{https://www.facebook.com/tr\?id=custom_audience_id&amp;ev=PixelInitialized})
21
+ end
22
+ end
23
+
24
+ describe 'with events' do
25
+ def env
26
+ {
27
+ 'tracker' => {
28
+ 'facebook' =>
29
+ [
30
+ {'pixel_id' => '123456789',
31
+ 'value' => '23',
32
+ 'currency' => 'EUR'}
33
+ ]
34
+ }
35
+ }
36
+ end
37
+ subject { described_class.new(env).render }
38
+
39
+ it 'will push the tracking events to the queue' do
40
+ expect(subject).to match(%r{\['track', '123456789', \{'value': '23', 'currency': 'EUR'\}\]})
41
+ end
42
+
43
+ it 'will add the noscript fallback' do
44
+ expect(subject).to match(%r{https://www.facebook.com/offsite_event.php\?id=123456789&amp;value=23&amp;currency=EUR})
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,117 @@
1
+ RSpec.describe Rack::Tracker::GoogleAnalytics do
2
+
3
+ def env
4
+ {misc: 'foobar'}
5
+ end
6
+
7
+ it 'will be placed in the head' do
8
+ expect(described_class.position).to eq(:head)
9
+ expect(described_class.new(env).position).to eq(:head)
10
+ end
11
+
12
+ describe "with events" do
13
+ describe "default" do
14
+ def env
15
+ {'tracker' => {
16
+ 'google_analytics' => [
17
+ Rack::Tracker::GoogleAnalytics::Event.new(category: "Users", action: "Login", label: "Standard")
18
+ ]
19
+ }}
20
+ end
21
+
22
+ subject { described_class.new(env, tracker: 'somebody', cookieDomain: "railslabs.com").render }
23
+ it "will show events" do
24
+ expect(subject).to match(%r{ga\(\"send\",{\"hitType\":\"event\",\"eventCategory\":\"Users\",\"eventAction\":\"Login\",\"eventLabel\":\"Standard\"}\)})
25
+ end
26
+ end
27
+
28
+ describe "with a event value" do
29
+ def env
30
+ {'tracker' => { 'google_analytics' => [
31
+ Rack::Tracker::GoogleAnalytics::Event.new(category: "Users", action: "Login", label: "Standard", value: 5)
32
+ ]}}
33
+ end
34
+
35
+ subject { described_class.new(env, tracker: 'somebody', cookieDomain: "railslabs.com").render }
36
+ it "will show events with values" do
37
+ expect(subject).to match(%r{ga\(\"send\",{\"hitType\":\"event\",\"eventCategory\":\"Users\",\"eventAction\":\"Login\",\"eventLabel\":\"Standard\",\"eventValue\":5}\)},)
38
+ end
39
+ end
40
+ end
41
+
42
+ describe 'with e-commerce events' do
43
+ describe "default" do
44
+ def env
45
+ {'tracker' => {
46
+ 'google_analytics' => [
47
+ Rack::Tracker::GoogleAnalytics::Ecommerce.new('ecommerce:addItem', {id: '1234', affiliation: 'Acme Clothing', revenue: 11.99, shipping: '5', tax: '1.29', currency: 'EUR'})
48
+ ]
49
+ }}
50
+ end
51
+
52
+ subject { described_class.new(env, tracker: 'somebody', cookieDomain: "railslabs.com").render }
53
+ it "will show events" do
54
+ expect(subject).to match(%r{ga\(\"ecommerce:addItem\",#{{id: '1234', affiliation: 'Acme Clothing', revenue: 11.99, shipping: '5', tax: '1.29', currency: 'EUR'}.to_json}})
55
+ end
56
+ end
57
+ end
58
+
59
+ describe "with custom domain" do
60
+ subject { described_class.new(env, tracker: 'somebody', cookieDomain: "railslabs.com").render }
61
+
62
+ it "will show asyncronous tracker with cookieDomain" do
63
+ expect(subject).to match(%r{ga\('create', 'somebody', {\"cookieDomain\":\"railslabs.com\"}\)})
64
+ expect(subject).to match(%r{ga\('send', 'pageview'\)})
65
+ end
66
+ end
67
+
68
+ describe "with enhanced_link_attribution" do
69
+ subject { described_class.new(env, tracker: 'happy', enhanced_link_attribution: true).render }
70
+
71
+ it "will embedded the linkid plugin script" do
72
+ expect(subject).to match(%r{linkid.js})
73
+ end
74
+ end
75
+
76
+ describe "with advertising" do
77
+ subject { described_class.new(env, tracker: 'happy', advertising: true).render }
78
+
79
+ it "will require displayfeatures" do
80
+ expect(subject).to match(%r{ga\('require', 'displayfeatures'\)})
81
+ end
82
+ end
83
+
84
+ describe "with e-commerce" do
85
+ subject { described_class.new(env, tracker: 'happy', ecommerce: true).render }
86
+
87
+ it "will require the ecommerce plugin" do
88
+ expect(subject).to match(%r{ga\('require', 'ecommerce', 'ecommerce\.js'\)})
89
+ end
90
+ end
91
+
92
+ describe "with anonymizeIp" do
93
+ subject { described_class.new(env, tracker: 'happy', anonymize_ip: true).render }
94
+
95
+ it "will set anonymizeIp to true" do
96
+ expect(subject).to match(%r{ga\('set', 'anonymizeIp', true\)})
97
+ end
98
+ end
99
+
100
+ describe "with dynamic tracker" do
101
+ subject { described_class.new(env, { tracker: lambda { |env| return env[:misc] }}).render }
102
+
103
+ it 'will call tracker lambdas to obtain tracking codes' do
104
+ expect(subject).to match(%r{ga\('create', 'foobar', {}\)})
105
+ end
106
+ end
107
+
108
+ describe 'adjusted bounce rate' do
109
+ subject { described_class.new(env, tracker: 'afake', adjusted_bounce_rate_timeouts: [15, 30]).render }
110
+
111
+ it "will add timeouts to push read events" do
112
+ expect(subject).to match(%r{ga\('send', 'event', '15_seconds', 'read'\)})
113
+ expect(subject).to match(%r{ga\('send', 'event', '30_seconds', 'read'\)})
114
+ end
115
+ end
116
+
117
+ end
@@ -0,0 +1,39 @@
1
+ require 'support/metal_controller'
2
+ require 'support/fake_handler'
3
+
4
+ Capybara.app = Rack::Builder.new do
5
+ use Rack::Tracker do
6
+ handler :track_all_the_things, { custom_key: 'SomeKey123' }
7
+ handler :another_handler, { custom_key: 'AnotherKey42' }
8
+ end
9
+ run MetalController.action(:index)
10
+ end
11
+
12
+
13
+ RSpec.describe "Rails Integration" do
14
+ before { visit '/' }
15
+ subject { page.html.gsub(/^\s*/, '') }
16
+
17
+ let(:expected_html) do
18
+ <<-HTML.gsub(/^\s*/, '')
19
+ <html>
20
+ <head>
21
+ <title>Metal Layout</title>
22
+ <script type="text/javascript">
23
+ myAwesomeFunction("tracks", "like", "no-one-else", "SomeKey123");
24
+ </script>
25
+ </head>
26
+ <body>
27
+ <h1>welcome to metal#index</h1>
28
+ <script type="text/javascript">
29
+ anotherFunction("tracks-event-from-down-under", "AnotherKey42");
30
+ </script>
31
+ </body>
32
+ </html>
33
+ HTML
34
+ end
35
+
36
+ it "embeds the script tag with tracking event from the controller action" do
37
+ expect(subject).to eql(expected_html)
38
+ end
39
+ end
@@ -0,0 +1,70 @@
1
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
2
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
3
+
4
+ require 'rspec'
5
+ require 'rack/test'
6
+ require 'capybara/rspec'
7
+ require 'rack/tracker'
8
+
9
+ # Requires supporting files with custom matchers and macros, etc,
10
+ # in ./support/ and its subdirectories.
11
+ Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each {|f| require f}
12
+
13
+ Capybara.ignore_hidden_elements = false
14
+
15
+ RSpec.configure do |config|
16
+ config.include Rack::Test::Methods
17
+ config.include Capybara::DSL
18
+
19
+ # These two settings work together to allow you to limit a spec run
20
+ # to individual examples or groups you care about by tagging them with
21
+ # `:focus` metadata. When nothing is tagged with `:focus`, all examples
22
+ # get run.
23
+ config.filter_run :focus
24
+ config.run_all_when_everything_filtered = true
25
+
26
+ # Many RSpec users commonly either run the entire suite or an individual
27
+ # file, and it's useful to allow more verbose output when running an
28
+ # individual spec file.
29
+ if config.files_to_run.one?
30
+ # Use the documentation formatter for detailed output,
31
+ # unless a formatter has already been configured
32
+ # (e.g. via a command-line flag).
33
+ config.default_formatter = 'doc'
34
+ end
35
+
36
+ # Run specs in random order to surface order dependencies. If you find an
37
+ # order dependency and want to debug it, you can fix the order by providing
38
+ # the seed, which is printed after each run.
39
+ # --seed 1234
40
+ config.order = :random
41
+
42
+ # Seed global randomization in this process using the `--seed` CLI option.
43
+ # Setting this allows you to use `--seed` to deterministically reproduce
44
+ # test failures related to randomization by passing the same `--seed` value
45
+ # as the one that triggered the failure.
46
+ Kernel.srand config.seed
47
+
48
+ # rspec-expectations config goes here. You can use an alternate
49
+ # assertion/expectation library such as wrong or the stdlib/minitest
50
+ # assertions if you prefer.
51
+ config.expect_with :rspec do |expectations|
52
+ # Enable only the newer, non-monkey-patching expect syntax.
53
+ # For more details, see:
54
+ # - http://myronmars.to/n/dev-blog/2012/06/rspecs-new-expectation-syntax
55
+ expectations.syntax = :expect
56
+ end
57
+
58
+ # rspec-mocks config goes here. You can use an alternate test double
59
+ # library (such as bogus or mocha) by changing the `mock_with` option here.
60
+ config.mock_with :rspec do |mocks|
61
+ # Enable only the newer, non-monkey-patching expect syntax.
62
+ # For more details, see:
63
+ # - http://teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/
64
+ mocks.syntax = :expect
65
+
66
+ # Prevents you from mocking or stubbing a method that does not exist on
67
+ # a real object. This is generally recommended.
68
+ mocks.verify_partial_doubles = true
69
+ end
70
+ end
@@ -0,0 +1,30 @@
1
+ class TrackAllTheThings < Rack::Tracker::Handler
2
+ def render
3
+ Tilt.new( File.join( File.dirname(__FILE__), '../fixtures/track_all_the_things.erb') ).render(self)
4
+ end
5
+
6
+ def self.track(name, event)
7
+ { name.to_s => event }
8
+ end
9
+
10
+ def track_me
11
+ env['tracker']['track_all_the_things']
12
+ end
13
+ end
14
+
15
+
16
+ class AnotherHandler < Rack::Tracker::Handler
17
+ self.position = :body
18
+
19
+ def render
20
+ Tilt.new( File.join( File.dirname(__FILE__), '../fixtures/another_handler.erb') ).render(self)
21
+ end
22
+
23
+ def self.track(name, event)
24
+ { name.to_s => event }
25
+ end
26
+
27
+ def track_me
28
+ env['tracker']['another_handler']
29
+ end
30
+ end
@@ -0,0 +1,17 @@
1
+ require 'action_controller'
2
+
3
+ class MetalController < ActionController::Metal
4
+ include Rack::Tracker::Controller
5
+ include AbstractController::Rendering
6
+ include ActionView::Layouts
7
+ append_view_path File.join(File.dirname(__FILE__), '../fixtures/views')
8
+ layout 'application'
9
+
10
+ def index
11
+ tracker do
12
+ track_all_the_things like: 'no-one-else'
13
+ another_handler likes: 'you'
14
+ end
15
+ render "metal/index"
16
+ end
17
+ end
@@ -0,0 +1,31 @@
1
+ class SomeController
2
+ include Rack::Tracker::Controller
3
+
4
+ attr_accessor :env
5
+
6
+ def initialize
7
+ @env = {}
8
+ end
9
+
10
+ def index
11
+ tracker do
12
+ google_analytics category: 'foo'
13
+ end
14
+ end
15
+ end
16
+
17
+
18
+ RSpec.describe Rack::Tracker::Controller do
19
+ context 'controller' do
20
+ let(:event) { Rack::Tracker::GoogleAnalytics::Event.new(category: 'foo') }
21
+
22
+ it 'writes the event into env' do
23
+ controller = SomeController.new
24
+ expect {
25
+ controller.index
26
+ }.to change {
27
+ controller.env
28
+ }.from({}).to('tracker' => {'google_analytics' => [event]})
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,26 @@
1
+ Foo = Class.new
2
+ Bar = Class.new
3
+
4
+ RSpec.describe Rack::Tracker::HandlerDelegator do
5
+
6
+ describe '#handler' do
7
+
8
+ it 'will find handler in the Rack::Tracker namespace' do
9
+ expect(described_class.handler(:google_analytics)).to eq(Rack::Tracker::GoogleAnalytics)
10
+ end
11
+
12
+ it 'will find handler outside the Rack::Tracker namespace' do
13
+ expect(described_class.handler(:foo)).to eq(Foo)
14
+ end
15
+
16
+ it 'will just return a class' do
17
+ expect(described_class.handler(Bar)).to eq(Bar)
18
+ end
19
+
20
+ it 'will raise when no handler is found' do
21
+ expect { described_class.handler(:baz)}.to raise_error(ArgumentError, "No such Handler: Baz")
22
+ end
23
+
24
+ end
25
+
26
+ end
@@ -0,0 +1,32 @@
1
+ class Dummy < Rack::Tracker::Handler
2
+ end
3
+
4
+ RSpec.describe Rack::Tracker::HandlerSet do
5
+
6
+ let(:set) do
7
+ Rack::Tracker::HandlerSet.new do
8
+ handler 'dummy', {foo: "bar"}
9
+ end
10
+ end
11
+
12
+ describe '#each' do
13
+ subject { set.each }
14
+ specify { expect(subject.to_a.size).to eq(1) }
15
+ specify { expect(subject).to match_array(Dummy) }
16
+ end
17
+
18
+ describe Rack::Tracker::HandlerSet::Handler do
19
+ subject { described_class.new(Dummy, {foo: 'bar'}) }
20
+
21
+ describe '#init' do
22
+ it 'will initialize the handler with the given class' do
23
+ expect(subject.init({})).to be_kind_of(Dummy)
24
+ end
25
+
26
+ it 'will initialize the handler with the given options' do
27
+ expect(subject.init({}).options).to eq({foo: 'bar'})
28
+ end
29
+ end
30
+ end
31
+
32
+ end
@@ -0,0 +1,100 @@
1
+ class DummyHandler < Rack::Tracker::Handler
2
+ def render
3
+ Tilt.new( File.join( File.dirname(__FILE__), '../fixtures/dummy.erb') ).render(self)
4
+ end
5
+
6
+ def dummy_alert
7
+ env['tracker']['dummy']
8
+ end
9
+ end
10
+
11
+ class BodyHandler < DummyHandler
12
+ self.position = :body
13
+ end
14
+
15
+ RSpec.describe Rack::Tracker do
16
+ def app
17
+ Rack::Builder.new do
18
+ use Rack::Session::Cookie, secret: "FOO"
19
+
20
+ use Rack::Tracker do
21
+ handler DummyHandler, { foo: 'head' }
22
+ handler BodyHandler, { foo: 'body' }
23
+ end
24
+
25
+ run lambda {|env|
26
+ request = Rack::Request.new(env)
27
+ case request.path
28
+ when '/' then
29
+ [200, {'Content-Type' => 'application/html'}, ['<head>Hello world</head>']]
30
+ when '/body' then
31
+ [200, {'Content-Type' => 'application/html'}, ['<body>bob here</body>']]
32
+ when '/body-head' then
33
+ [200, {'Content-Type' => 'application/html'}, ['<head></head><body></body>']]
34
+ when '/test.xml' then
35
+ [200, {'Content-Type' => 'application/xml'}, ['Xml here']]
36
+ when '/redirect' then
37
+ [302, {'Content-Type' => 'application/html', 'Location' => '/'}, ['<body>redirection</body>']]
38
+ else
39
+ [404, 'Nothing here']
40
+ end
41
+ }
42
+ end
43
+ end
44
+ subject { app }
45
+
46
+ describe 'when head is present' do
47
+ it 'injects the handler code' do
48
+ get '/'
49
+ expect(last_response.body).to include("alert('this is a dummy class');")
50
+ end
51
+
52
+ it 'will pass options to the Handler' do
53
+ get '/'
54
+ expect(last_response.body).to include("console.log('head');")
55
+ expect(last_response.body).to_not include("console.log('body');")
56
+ end
57
+
58
+ it 'injects custom variables that was directly assigned' do
59
+ get '/', {}, {'tracker' => { 'dummy' => 'foo bar'}}
60
+ expect(last_response.body).to include("alert('foo bar');")
61
+ end
62
+
63
+ it 'injects custom variables that lives in the session' do
64
+ get '/', {}, {'rack.session' => {'tracker' => { 'dummy' => 'bar foo'}}}
65
+ expect(last_response.body).to include("alert('bar foo');")
66
+ end
67
+ end
68
+
69
+ describe 'when body is present' do
70
+ it 'will not inject the body handler code' do
71
+ get '/body'
72
+ expect(last_response.body).to include("console.log('body');")
73
+ expect(last_response.body).to_not include("console.log('head');")
74
+ end
75
+ end
76
+
77
+ describe 'when head and body is present' do
78
+ it 'will pass options to the Handler' do
79
+ get '/body-head'
80
+ expect(last_response.body).to include("console.log('head');")
81
+ expect(last_response.body).to include("console.log('body');")
82
+ end
83
+ end
84
+
85
+ describe 'when a redirect' do
86
+ it 'will keep the tracker attributes and show them on the new location' do
87
+ get '/redirect', {}, { 'tracker' => { 'dummy' => 'Keep this!' } }
88
+ follow_redirect!
89
+ expect(last_response.body).to include("alert('Keep this!');")
90
+ end
91
+ end
92
+
93
+ describe 'when not html' do
94
+ it 'will not inject the handler code' do
95
+ get '/test.xml'
96
+
97
+ expect(last_response.body).to_not include("alert('this is a dummy class');")
98
+ end
99
+ end
100
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rack-tracker
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.0.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Lars Brillert
@@ -9,37 +9,135 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2014-08-19 00:00:00.000000000 Z
12
+ date: 2014-08-27 00:00:00.000000000 Z
13
13
  dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: rack
16
+ requirement: !ruby/object:Gem::Requirement
17
+ requirements:
18
+ - - "~>"
19
+ - !ruby/object:Gem::Version
20
+ version: 1.5.2
21
+ type: :runtime
22
+ prerelease: false
23
+ version_requirements: !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - "~>"
26
+ - !ruby/object:Gem::Version
27
+ version: 1.5.2
28
+ - !ruby/object:Gem::Dependency
29
+ name: tilt
30
+ requirement: !ruby/object:Gem::Requirement
31
+ requirements:
32
+ - - "~>"
33
+ - !ruby/object:Gem::Version
34
+ version: 1.4.1
35
+ type: :runtime
36
+ prerelease: false
37
+ version_requirements: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - "~>"
40
+ - !ruby/object:Gem::Version
41
+ version: 1.4.1
42
+ - !ruby/object:Gem::Dependency
43
+ name: activesupport
44
+ requirement: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - "~>"
47
+ - !ruby/object:Gem::Version
48
+ version: 4.1.0
49
+ type: :runtime
50
+ prerelease: false
51
+ version_requirements: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - "~>"
54
+ - !ruby/object:Gem::Version
55
+ version: 4.1.0
14
56
  - !ruby/object:Gem::Dependency
15
57
  name: bundler
16
58
  requirement: !ruby/object:Gem::Requirement
17
59
  requirements:
18
- - - ~>
60
+ - - "~>"
19
61
  - !ruby/object:Gem::Version
20
62
  version: '1.5'
21
63
  type: :development
22
64
  prerelease: false
23
65
  version_requirements: !ruby/object:Gem::Requirement
24
66
  requirements:
25
- - - ~>
67
+ - - "~>"
26
68
  - !ruby/object:Gem::Version
27
69
  version: '1.5'
28
70
  - !ruby/object:Gem::Dependency
29
71
  name: rake
30
72
  requirement: !ruby/object:Gem::Requirement
31
73
  requirements:
32
- - - '>='
74
+ - - ">="
75
+ - !ruby/object:Gem::Version
76
+ version: '0'
77
+ type: :development
78
+ prerelease: false
79
+ version_requirements: !ruby/object:Gem::Requirement
80
+ requirements:
81
+ - - ">="
82
+ - !ruby/object:Gem::Version
83
+ version: '0'
84
+ - !ruby/object:Gem::Dependency
85
+ name: rspec
86
+ requirement: !ruby/object:Gem::Requirement
87
+ requirements:
88
+ - - "~>"
89
+ - !ruby/object:Gem::Version
90
+ version: 3.0.0
91
+ type: :development
92
+ prerelease: false
93
+ version_requirements: !ruby/object:Gem::Requirement
94
+ requirements:
95
+ - - "~>"
96
+ - !ruby/object:Gem::Version
97
+ version: 3.0.0
98
+ - !ruby/object:Gem::Dependency
99
+ name: capybara
100
+ requirement: !ruby/object:Gem::Requirement
101
+ requirements:
102
+ - - "~>"
103
+ - !ruby/object:Gem::Version
104
+ version: 2.4.1
105
+ type: :development
106
+ prerelease: false
107
+ version_requirements: !ruby/object:Gem::Requirement
108
+ requirements:
109
+ - - "~>"
110
+ - !ruby/object:Gem::Version
111
+ version: 2.4.1
112
+ - !ruby/object:Gem::Dependency
113
+ name: actionpack
114
+ requirement: !ruby/object:Gem::Requirement
115
+ requirements:
116
+ - - "~>"
117
+ - !ruby/object:Gem::Version
118
+ version: 4.1.5
119
+ type: :development
120
+ prerelease: false
121
+ version_requirements: !ruby/object:Gem::Requirement
122
+ requirements:
123
+ - - "~>"
124
+ - !ruby/object:Gem::Version
125
+ version: 4.1.5
126
+ - !ruby/object:Gem::Dependency
127
+ name: pry
128
+ requirement: !ruby/object:Gem::Requirement
129
+ requirements:
130
+ - - ">="
33
131
  - !ruby/object:Gem::Version
34
132
  version: '0'
35
133
  type: :development
36
134
  prerelease: false
37
135
  version_requirements: !ruby/object:Gem::Requirement
38
136
  requirements:
39
- - - '>='
137
+ - - ">="
40
138
  - !ruby/object:Gem::Version
41
139
  version: '0'
42
- description: Don’t fool around with adding tracking partials to your app concentrate
140
+ description: Don’t fool around with adding tracking partials to your app and concentrate
43
141
  on the things that matter.
44
142
  email:
45
143
  - lars@railslove.com
@@ -48,15 +146,40 @@ executables: []
48
146
  extensions: []
49
147
  extra_rdoc_files: []
50
148
  files:
51
- - .gitignore
52
- - .travis.yml
149
+ - ".gitignore"
150
+ - ".rspec"
151
+ - ".travis.yml"
53
152
  - Gemfile
54
153
  - LICENSE.txt
55
154
  - README.md
56
155
  - Rakefile
57
156
  - lib/rack/tracker.rb
157
+ - lib/rack/tracker/controller.rb
158
+ - lib/rack/tracker/extensions.rb
159
+ - lib/rack/tracker/facebook/facebook.rb
160
+ - lib/rack/tracker/facebook/template/facebook.erb
161
+ - lib/rack/tracker/google_analytics/google_analytics.rb
162
+ - lib/rack/tracker/google_analytics/template/google_analytics.erb
163
+ - lib/rack/tracker/handler.rb
164
+ - lib/rack/tracker/handler_delegator.rb
165
+ - lib/rack/tracker/railtie.rb
58
166
  - lib/rack/tracker/version.rb
59
167
  - rack-tracker.gemspec
168
+ - spec/fixtures/another_handler.erb
169
+ - spec/fixtures/dummy.erb
170
+ - spec/fixtures/track_all_the_things.erb
171
+ - spec/fixtures/views/layouts/application.html.erb
172
+ - spec/fixtures/views/metal/index.html.erb
173
+ - spec/handler/facebook_spec.rb
174
+ - spec/handler/google_analytics_spec.rb
175
+ - spec/integration/rails_integration_spec.rb
176
+ - spec/spec_helper.rb
177
+ - spec/support/fake_handler.rb
178
+ - spec/support/metal_controller.rb
179
+ - spec/tracker/controller_spec.rb
180
+ - spec/tracker/handler_delegator_spec.rb
181
+ - spec/tracker/handler_set_spec.rb
182
+ - spec/tracker/tracker_spec.rb
60
183
  homepage: https://github.com/railslove/rack-tracker
61
184
  licenses:
62
185
  - MIT
@@ -67,12 +190,12 @@ require_paths:
67
190
  - lib
68
191
  required_ruby_version: !ruby/object:Gem::Requirement
69
192
  requirements:
70
- - - '>='
193
+ - - ">="
71
194
  - !ruby/object:Gem::Version
72
195
  version: '0'
73
196
  required_rubygems_version: !ruby/object:Gem::Requirement
74
197
  requirements:
75
- - - '>='
198
+ - - ">="
76
199
  - !ruby/object:Gem::Version
77
200
  version: '0'
78
201
  requirements: []
@@ -81,4 +204,19 @@ rubygems_version: 2.2.2
81
204
  signing_key:
82
205
  specification_version: 4
83
206
  summary: Tracking made easy
84
- test_files: []
207
+ test_files:
208
+ - spec/fixtures/another_handler.erb
209
+ - spec/fixtures/dummy.erb
210
+ - spec/fixtures/track_all_the_things.erb
211
+ - spec/fixtures/views/layouts/application.html.erb
212
+ - spec/fixtures/views/metal/index.html.erb
213
+ - spec/handler/facebook_spec.rb
214
+ - spec/handler/google_analytics_spec.rb
215
+ - spec/integration/rails_integration_spec.rb
216
+ - spec/spec_helper.rb
217
+ - spec/support/fake_handler.rb
218
+ - spec/support/metal_controller.rb
219
+ - spec/tracker/controller_spec.rb
220
+ - spec/tracker/handler_delegator_spec.rb
221
+ - spec/tracker/handler_set_spec.rb
222
+ - spec/tracker/tracker_spec.rb