rack-tracker 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
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