diff --git a/.travis.yml b/.travis.yml
index 8a42c15..3c8e02f 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,17 +1,23 @@
language: ruby
sudo: false
rvm:
- - 2.1.10
- - 2.2.7
- - 2.3.4
- - 2.4.1
+ - 2.3.8
+ - 2.4.5
+ - 2.5.3
+ - 2.6.1
- jruby-9.1.10.0
+ - jruby-9.2.7.0
gemfile:
- Gemfile
- Gemfile.rails-3.2
- Gemfile.rails-4.2
- - Gemfile.rails-5.1
+ - Gemfile.rails-5.2
+ - Gemfile.rails-6.0
matrix:
exclude:
- - rvm: 2.1.10
- gemfile: Gemfile.rails-5.1 # rails 5 needs ruby >= 2.2.2
+ - rvm: 2.3.8
+ gemfile: Gemfile.rails-6.0
+ - rvm: 2.4.5
+ gemfile: Gemfile.rails-6.0
+ - rvm: jruby-9.1.10.0
+ gemfile: Gemfile.rails-6.0
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d37e56b..4398e32 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,55 @@
+# 1.13.0
+
+* [ENHANCEMENT] hotwired/turbo support #160 (thx @wrozka)
+* [BUGFIX] Use leftmost match for gtm tag injection #156 (thx @yutoji)
+
+# 1.12.1
+
+* [ENHANCEMENT] Use local variables to prevent instance state #151 (thx @bumi)
+* [ENHANCEMENT] Make middleware thread safe #150 (thx @kspe)
+
+# 1.12.0
+
+* [ENHANCEMENT] Add support for Heap #147 (thx @mohanzhang)
+
+# 1.11.2
+
+ * [ENHANCEMENT] Allows disabling the Google Analytics pageview send. Defaults to true #131 (thx @ChrisCoffey)
+
+# 1.11.1
+
+ * [BUGFIX] Uncaught ReferenceError Fix: wrap Drift account ID in quotes #140 (thx @sassela)
+
+# 1.11.0
+
+ * [ENHANCEMENT] Add support for Drift #139 (thx @sassela)
+
+# 1.10.0
+
+ * [ENHANCEMENT] Hubspot integration #136 (thx @ChrisCoffey)
+
+# 1.9.0
+
+ * [ENHANCEMENT] Integration for Bing tracking #131 (thx @pcraston)
+ * [ENHANCEMENT] Possibility to integrate Google Optimize ID into the allowed tracker options #127 (thx @nachoabad)
+ * [ENHANCEMENT] Support for google global events #126 (thx @atd)
+
+# 1.8.0
+
+ * [ENHANCEMENT] Google Global Site Tag: basic integration with support for pageviews to Google global tag #123 (thx @atd)
+
+# 1.7.0
+
+ * [BUGFIX] dup response string in Rack::Tracker#inject to avoid RuntimeError #114 (thx @zpfled)
+ * [ENHANCEMENT] Allow to use custom pageview url script for GoogleAnalytics tracker. #119 (thx @Haerezis)
+
+# 1.6.0
+
+ * [BUGFIX] set wildcard to non-greedy for GTM body insertion #107
+ * [ENHANCEMENT] Test against Ruby 2.5 #104
+ * [ENHANCEMENT] Google Optimize container ID #103
+ * [ENHANCEMENT] Allow for dynamic Facebook Pixel options #101
+
# 1.5.0
* [ENHANCEMENT] facebook pixel now supports non-standard (custom) event names #93
diff --git a/Gemfile.rails-5.1 b/Gemfile.rails-5.1
deleted file mode 100644
index f54ce1a..0000000
--- a/Gemfile.rails-5.1
+++ /dev/null
@@ -1,6 +0,0 @@
-source "https://rubygems.org"
-
-gemspec
-
-gem 'activesupport', '~> 5.1.0'
-gem 'actionpack', '~> 5.1.0'
diff --git a/Gemfile.rails-5.2 b/Gemfile.rails-5.2
new file mode 100644
index 0000000..47e7c4c
--- /dev/null
+++ b/Gemfile.rails-5.2
@@ -0,0 +1,6 @@
+source "https://rubygems.org"
+
+gemspec
+
+gem 'activesupport', '~> 5.2.0'
+gem 'actionpack', '~> 5.2.0'
diff --git a/Gemfile.rails-6.0 b/Gemfile.rails-6.0
new file mode 100644
index 0000000..ee3fb05
--- /dev/null
+++ b/Gemfile.rails-6.0
@@ -0,0 +1,6 @@
+source "https://rubygems.org"
+
+gemspec
+
+gem 'activesupport', '~> 6.0.0'
+gem 'actionpack', '~> 6.0.0'
diff --git a/README.md b/README.md
index 1550a64..e1b7927 100644
--- a/README.md
+++ b/README.md
@@ -18,17 +18,35 @@ rack middleware that can be hooked up to multiple services and exposing them in
fashion. It comes in two parts, the first one is the actual middleware that you can add
to the middleware stack the second part are the service-handlers that you're going to use
in your application. It's easy to add your own [custom handlers](#custom-handlers),
-but to get you started we're shipping support for the following services out of the box:
+but to get you started we're shipping support for the services [mentioned below](#services)
+out of the box:
-* [Google Analytics](#google-analytics)
-* [Google Adwords Conversion](#google-adwords-conversion)
-* [Google Tag Manager](#google-tag-manager)
-* [Facebook](#facebook)
-* [Visual Website Optimizer (VWO)](#visual-website-optimizer-vwo)
-* [GoSquared](#gosquared)
-* [Criteo](#criteo)
-* [Zanox](#zanox)
-* [Hotjar](#hotjar)
+
+## Respecting the Do Not Track (DNT) HTTP header
+
+The Do Not Track (DNT) HTTP header is a HTTP header that requests the server to disable its tracking of the individual user.
+This is an opt-out option supported by most browsers. This option is disabled by default and has to be explicitly enabled to indicate the user's request to opt-out.
+We believe evey application should respect the user's choice to opt-out and respect this HTTP header.
+
+Since version 2.0.0 rack-tracker respects that request header by default. That means NO tracker is injected IF the DNT header is set to "1".
+
+This option can be overwriten using the `DO_NOT_RESPECT_DNT_HEADER => true` option which must be set on any handler that should ignore the DNT header. (but please think twice before doing that)
+
+### Example on how to not respect the DNT header
+
+```ruby
+use Rack::Tracker do
+ # this tracker will be injected EVEN IF the DNT header is set to 1
+ handler :maybe_a_friendly_tracker, { tracker: 'U-XXXXX-Y', DO_NOT_RESPECT_DNT_HEADER: true }
+ # this tracker will NOT be injected if the DNT header is set to 1
+ handler :google_analytics, { tracker: 'U-XXXXX-Y' }
+end
+```
+
+Further reading on the DNT header:
+
+* [Wikipedia Do Not Track](https://en.wikipedia.org/wiki/Do_Not_Track)
+* [EFF: Do Not Track](https://www.eff.org/issues/do-not-track)
## Installation
@@ -85,6 +103,30 @@ request.env['tracker'] = {
}
```
+## Services
+
+### Google Global Site Tag (gtag.js)
+
+* `:anonymize_ip` - sets the tracker to remove the last octet from all IP addresses, see https://developers.google.com/analytics/devguides/collection/gtagjs/ip-anonymization for details.
+* `:cookie_domain` - sets the domain name for the [GATC cookies](https://developers.google.com/analytics/devguides/collection/gtagjs/cookies-user-id). If not set its the website domain, with the www. prefix removed.
+* `:user_id` - defines a proc to set the [userId](https://developers.google.com/analytics/devguides/collection/gtagjs/cookies-user-id). Ex: `user_id: lambda { |env| env['rack.session']['user_id'] }` would return the user_id from the session.
+* `:link_attribution` - Enables [Enhanced Link Attribution](https://developers.google.com/analytics/devguides/collection/gtagjs/enhanced-link-attribution).
+* `:allow_display_features` - Can be used to disable [Display Features](https://developers.google.com/analytics/devguides/collection/gtagjs/display-features).
+* `:custom_map` - Used to [Configure and send custom dimensions](https://developers.google.com/analytics/devguides/collection/gtagjs/custom-dims-mets)
+* `:optimize_id` - Used to [Deploy Optimize using gtag](https://support.google.com/optimize/answer/7513085)
+* `:set` - Used in the [set command to configure multiple properties](https://developers.google.com/analytics/devguides/collection/gtagjs/setting-values)
+
+#### Trackers
+
+Google Global Site tag allows configuring multiple trackers. Use the tracker option to configure the ids:
+
+
+```ruby
+config.middleware.use(Rack::Tracker) do
+ handler :google_global, { trackers: [ { id: 'U-XXXXX-Y' }, { id: 'U-WWWWWW-Z'} ] }
+end
+```
+
### Google Analytics
* `: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.
@@ -96,6 +138,9 @@ request.env['tracker'] = {
* `:advertising` - Enables [Display Features](https://developers.google.com/analytics/devguides/collection/analyticsjs/display-features).
* `:ecommerce` - Enables [Ecommerce Tracking](https://developers.google.com/analytics/devguides/collection/analyticsjs/ecommerce).
* `:enhanced_ecommerce` - Enables [Enhanced Ecommerce Tracking](https://developers.google.com/analytics/devguides/collection/analyticsjs/enhanced-ecommerce)
+* `:optimize` - pass [Google Optimize container ID](https://support.google.com/360suite/optimize/answer/6262084#example-combined-snippet) as value (e.g. `optimize: 'GTM-1234'`).
+* `:pageview_url_script` - a String containing a custom js script evaluating to the url that shoudl be given to the pageview event. Default to `window.location.pathname + window.location.search`.
+* `:explicit_pageview` - A boolean that controls whether to send the `pageview` event on pageload. This defaults to true.
#### Events
@@ -275,6 +320,26 @@ First, add the following to your config:
end
```
+#### Dynamic Pixel Configuration
+
+If you need to have different pixel ids e.g. based on the request or serving pages for different accounts, you have the possibility to achieve this by passing a lambda:
+
+```ruby
+ config.middleware.use(Rack::Tracker) do
+ handler :facebook_pixel, { id: lambda { |env| env['PIXEL_ID'] } }
+ end
+```
+
+and set the pixel id within the request `env` variable. Here an example on how it can be done in a rails action:
+
+```ruby
+ class MyController < ApplicationController
+ def show
+ request.env['PIXEL_ID'] = 'DYNAMIC_PIXEL_ID'
+ end
+ end
+```
+
#### Standard Events
To track Standard Events from the server side just call the `tracker` method in your controller.
@@ -505,6 +570,62 @@ config.middleware.use(Rack::Tracker) do
end
```
+### Bing
+
+[Bing](https://bingads.microsoft.com/)
+
+To add the tracking snippet:
+
+```
+config.middleware.use(Rack::Tracker) do
+ handler :bing, { tracker: '12345678' }
+end
+```
+
+To send conversion events:
+```
+tracker do |t|
+ t.bing :conversion, {
+ type: 'event',
+ category: 'Users',
+ action: 'Login',
+ label: 'Standard',
+ value: 10
+ }
+end
+```
+
+### Hubspot
+
+[Hubspot](https://www.hubspot.com/)
+
+```
+config.middleware.use(Rack::Tracker) do
+ handler :hubspot, { site_id: '1234' }
+end
+```
+
+### Drift
+
+[Drift](https://www.drift.com/)
+
+```
+config.middleware.use(Rack::Tracker) do
+ handler :drift, account_id: 'DRIFT_ID'
+end
+```
+
+### Heap
+
+[Heap](https://heap.io/). Heap has Projects (e.g. "Main") which have multiple
+Environments (e.g. "Production" or "Development"). `env_id` is therefore the numerical ID
+that represents the Environment. See Settings -> Projects -> Environments in your dashboard.
+
+```
+config.middleware.use(Rack::Tracker) do
+ handler :heap, env_id: 'HEAP_ID'
+end
+```
### Custom Handlers
diff --git a/lib/rack/tracker.rb b/lib/rack/tracker.rb
index d549a53..4e10d87 100644
--- a/lib/rack/tracker.rb
+++ b/lib/rack/tracker.rb
@@ -13,6 +13,7 @@
require "rack/tracker/handler_delegator"
require "rack/tracker/controller"
require "rack/tracker/google_analytics/google_analytics"
+require "rack/tracker/google_global/google_global"
require "rack/tracker/google_tag_manager/google_tag_manager"
require "rack/tracker/google_adwords_conversion/google_adwords_conversion"
require "rack/tracker/facebook/facebook"
@@ -22,6 +23,10 @@
require "rack/tracker/criteo/criteo"
require "rack/tracker/zanox/zanox"
require "rack/tracker/hotjar/hotjar"
+require "rack/tracker/bing/bing"
+require "rack/tracker/hubspot/hubspot"
+require "rack/tracker/drift/drift"
+require "rack/tracker/heap/heap"
module Rack
class Tracker
@@ -33,10 +38,14 @@ def initialize(app, &block)
end
def call(env)
- @status, @headers, @body = @app.call(env)
- return [@status, @headers, @body] unless html?
- response = Rack::Response.new([], @status, @headers)
+ dup._call(env)
+ end
+
+ def _call(env)
+ status, headers, body = @app.call(env)
+ return [status, headers, body] unless headers['Content-Type'] =~ /html/
+ response = Rack::Response.new([], status, headers)
env[EVENT_TRACKING_KEY] ||= {}
if session = env["rack.session"]
@@ -47,21 +56,20 @@ def call(env)
session[EVENT_TRACKING_KEY] = env[EVENT_TRACKING_KEY]
end
- @body.each { |fragment| response.write inject(env, fragment) }
- @body.close if @body.respond_to?(:close)
+ 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)
+ duplicated_response = response.dup
@handlers.each(env) do |handler|
- handler.inject(response)
+ handler.inject(duplicated_response)
end
- response
+ duplicated_response
end
class HandlerSet
diff --git a/lib/rack/tracker/bing/bing.rb b/lib/rack/tracker/bing/bing.rb
new file mode 100644
index 0000000..f7e71c7
--- /dev/null
+++ b/lib/rack/tracker/bing/bing.rb
@@ -0,0 +1,12 @@
+class Rack::Tracker::Bing < Rack::Tracker::Handler
+
+ class Conversion < OpenStruct
+ end
+
+ self.position = :body
+
+ def tracker
+ options[:tracker].respond_to?(:call) ? options[:tracker].call(env) : options[:tracker]
+ end
+
+end
diff --git a/lib/rack/tracker/bing/template/bing.erb b/lib/rack/tracker/bing/template/bing.erb
new file mode 100644
index 0000000..9c0d342
--- /dev/null
+++ b/lib/rack/tracker/bing/template/bing.erb
@@ -0,0 +1,22 @@
+<% if events.any? %>
+
+
+<% end %>
+
+
+
\ No newline at end of file
diff --git a/lib/rack/tracker/drift/drift.rb b/lib/rack/tracker/drift/drift.rb
new file mode 100644
index 0000000..fb7fe34
--- /dev/null
+++ b/lib/rack/tracker/drift/drift.rb
@@ -0,0 +1,2 @@
+class Rack::Tracker::Drift < Rack::Tracker::Handler
+end
diff --git a/lib/rack/tracker/drift/template/drift.erb b/lib/rack/tracker/drift/template/drift.erb
new file mode 100644
index 0000000..e046404
--- /dev/null
+++ b/lib/rack/tracker/drift/template/drift.erb
@@ -0,0 +1,26 @@
+
diff --git a/lib/rack/tracker/facebook_pixel/facebook_pixel.rb b/lib/rack/tracker/facebook_pixel/facebook_pixel.rb
index 1acf5d5..f6015dd 100644
--- a/lib/rack/tracker/facebook_pixel/facebook_pixel.rb
+++ b/lib/rack/tracker/facebook_pixel/facebook_pixel.rb
@@ -1,5 +1,6 @@
class Rack::Tracker::FacebookPixel < Rack::Tracker::Handler
self.position = :body
+ self.allowed_tracker_options = [:id]
class Event < OpenStruct
def write
diff --git a/lib/rack/tracker/facebook_pixel/template/facebook_pixel.erb b/lib/rack/tracker/facebook_pixel/template/facebook_pixel.erb
index 4209e21..2901c52 100644
--- a/lib/rack/tracker/facebook_pixel/template/facebook_pixel.erb
+++ b/lib/rack/tracker/facebook_pixel/template/facebook_pixel.erb
@@ -7,12 +7,12 @@
t.src=v;s=b.getElementsByTagName(e)[0];s.parentNode.insertBefore(t,s)}(window,
document,'script','//connect.facebook.net/en_US/fbevents.js');
- fbq('init', '<%= options[:id] %>');
+ fbq('init', '<%= tracker_options[:id] %>');
fbq('track', "PageView");
}
<% if events.any? %>
diff --git a/lib/rack/tracker/google_analytics/google_analytics.rb b/lib/rack/tracker/google_analytics/google_analytics.rb
index d490d20..f33663b 100644
--- a/lib/rack/tracker/google_analytics/google_analytics.rb
+++ b/lib/rack/tracker/google_analytics/google_analytics.rb
@@ -1,6 +1,11 @@
class Rack::Tracker::GoogleAnalytics < Rack::Tracker::Handler
- ALLOWED_TRACKER_OPTIONS = [:cookie_domain, :user_id]
+ self.allowed_tracker_options = [:cookie_domain, :user_id]
+
+ def initialize(env, options = {})
+ options[:explicit_pageview] = true if !options.has_key?(:explicit_pageview)
+ super(env, options)
+ end
class Send < OpenStruct
def initialize(attrs = {})
@@ -57,16 +62,6 @@ def tracker
options[:tracker].respond_to?(:call) ? options[:tracker].call(env) : options[:tracker]
end
- def tracker_options
- @tracker_options ||= {}.tap do |tracker_options|
- options.slice(*ALLOWED_TRACKER_OPTIONS).each do |key, value|
- if option_value = value.respond_to?(:call) ? value.call(env) : value
- tracker_options[key.to_s.camelize(:lower).to_sym] = option_value.to_s
- end
- end
- end
- end
-
def ecommerce_events
events.select {|e| e.kind_of?(Ecommerce) }
end
@@ -74,4 +69,18 @@ def ecommerce_events
def enhanced_ecommerce_events
events.select {|e| e.kind_of?(EnhancedEcommerce) }
end
+
+ def pageview_url_script
+ options[:pageview_url_script] || 'window.location.pathname + window.location.search'
+ end
+
+ private
+
+ def tracker_option_key(key)
+ key.to_s.camelize(:lower).to_sym
+ end
+
+ def tracker_option_value(value)
+ value.to_s
+ end
end
diff --git a/lib/rack/tracker/google_analytics/template/google_analytics.erb b/lib/rack/tracker/google_analytics/template/google_analytics.erb
index d56f043..d6d9d7b 100644
--- a/lib/rack/tracker/google_analytics/template/google_analytics.erb
+++ b/lib/rack/tracker/google_analytics/template/google_analytics.erb
@@ -1,5 +1,5 @@
-
+<% end %>
diff --git a/lib/rack/tracker/google_global/google_global.rb b/lib/rack/tracker/google_global/google_global.rb
new file mode 100644
index 0000000..5ccb817
--- /dev/null
+++ b/lib/rack/tracker/google_global/google_global.rb
@@ -0,0 +1,80 @@
+class Rack::Tracker::GoogleGlobal < Rack::Tracker::Handler
+ self.allowed_tracker_options = [:cookie_domain, :user_id,
+ :link_attribution, :allow_display_features, :anonymize_ip,
+ :custom_map, :optimize_id]
+
+ class Page < OpenStruct
+ def params
+ Hash[to_h.slice(:title, :location, :path).map { |key, value| ["page_#{key}", value] }]
+ end
+ end
+
+ class Event < OpenStruct
+ PREFIXED_PARAMS = %i[category label]
+ SKIP_PARAMS = %i[action]
+
+ def params
+ Hash[to_h.except(*SKIP_PARAMS).map { |key, value| [param_key(key), value] }]
+ end
+
+ private
+
+ def param_key(key)
+ PREFIXED_PARAMS.include?(key) ? "event_#{key}" : key.to_s
+ end
+ end
+
+ def pages
+ select_handler_events(Page)
+ end
+
+ alias handler_events events
+
+ def events
+ select_handler_events(Event)
+ end
+
+ def trackers
+ @_trackers ||= build_trackers
+ end
+
+ def set_options
+ @_set_options ||= build_set_options
+ end
+
+ private
+
+ def build_trackers
+ options[:trackers].map(&method(:call_tracker)).reject(&method(:invalid_tracker?))
+ end
+
+ def call_tracker(tracker)
+ if tracker[:id].respond_to?(:call)
+ tracker.merge(id: tracker[:id].call(env))
+ else
+ tracker
+ end
+ end
+
+ def invalid_tracker?(tracker)
+ if tracker[:id].to_s.strip == ''
+ $stdout.puts <<~WARN
+ WARNING: One of the trackers specified for Rack::Tracker handler 'google_global' is empty.
+ Trackers: #{options[:trackers]}
+ WARN
+
+ true
+ else
+ false
+ end
+ end
+
+ def build_set_options
+ value = options[:set]
+ value.respond_to?(:call) ? value.call(env) : value
+ end
+
+ def select_handler_events(klass)
+ handler_events.select { |event| event.is_a?(klass) }
+ end
+end
diff --git a/lib/rack/tracker/google_global/template/google_global.erb b/lib/rack/tracker/google_global/template/google_global.erb
new file mode 100644
index 0000000..9103419
--- /dev/null
+++ b/lib/rack/tracker/google_global/template/google_global.erb
@@ -0,0 +1,23 @@
+<% if trackers.any? %>
+
+
+<% end %>
diff --git a/lib/rack/tracker/google_tag_manager/google_tag_manager.rb b/lib/rack/tracker/google_tag_manager/google_tag_manager.rb
index 1fb286c..1a05489 100644
--- a/lib/rack/tracker/google_tag_manager/google_tag_manager.rb
+++ b/lib/rack/tracker/google_tag_manager/google_tag_manager.rb
@@ -10,10 +10,10 @@ def inject(response)
# Sub! is enough, in well formed html there's only one head or body tag.
# Block syntax need to be used, otherwise backslashes in input will mess the output.
# @see http://stackoverflow.com/a/4149087/518204 and https://github.com/railslove/rack-tracker/issues/50
- response.sub! %r{
} do |m|
+ response.sub! %r{} do |m|
m.to_s << self.render_head
end
- response.sub! %r{} do |m|
+ response.sub! %r{} do |m|
m.to_s << self.render_body
end
response
diff --git a/lib/rack/tracker/google_tag_manager/template/google_tag_manager_head.erb b/lib/rack/tracker/google_tag_manager/template/google_tag_manager_head.erb
index 3b059c0..2df1117 100644
--- a/lib/rack/tracker/google_tag_manager/template/google_tag_manager_head.erb
+++ b/lib/rack/tracker/google_tag_manager/template/google_tag_manager_head.erb
@@ -1,23 +1,29 @@
<% if container %>
- <% unless options[:turbolinks] %>
- <% if events.any? %>
-
- <% end %>
- <% end %>
-
+
+ <% if events.any? %>
+
+ <% end %>
+
+
diff --git a/lib/rack/tracker/hubspot/hubspot.rb b/lib/rack/tracker/hubspot/hubspot.rb
new file mode 100644
index 0000000..162b07c
--- /dev/null
+++ b/lib/rack/tracker/hubspot/hubspot.rb
@@ -0,0 +1,2 @@
+class Rack::Tracker::Hubspot < Rack::Tracker::Handler
+end
diff --git a/lib/rack/tracker/hubspot/template/hubspot.erb b/lib/rack/tracker/hubspot/template/hubspot.erb
new file mode 100644
index 0000000..ecf9556
--- /dev/null
+++ b/lib/rack/tracker/hubspot/template/hubspot.erb
@@ -0,0 +1 @@
+
diff --git a/lib/rack/tracker/version.rb b/lib/rack/tracker/version.rb
index 8677a68..3cf2d99 100644
--- a/lib/rack/tracker/version.rb
+++ b/lib/rack/tracker/version.rb
@@ -1,5 +1,5 @@
module Rack
class Tracker
- VERSION = '1.5.0'
+ VERSION = '1.13.0'
end
end
diff --git a/rack-tracker.gemspec b/rack-tracker.gemspec
index 0aa7116..d5fa3d2 100644
--- a/rack-tracker.gemspec
+++ b/rack-tracker.gemspec
@@ -23,7 +23,7 @@ Gem::Specification.new do |spec|
spec.add_dependency 'activesupport', '>= 3.0'
spec.add_development_dependency 'actionpack', '>= 3.0'
- spec.add_development_dependency "bundler", "~> 1.5"
+ spec.add_development_dependency "bundler", ">= 1.16"
spec.add_development_dependency "rake"
spec.add_development_dependency "rspec", "~> 3.2"
spec.add_development_dependency "capybara", "~> 2.4"
diff --git a/spec/handler/bing_spec.rb b/spec/handler/bing_spec.rb
new file mode 100644
index 0000000..843752a
--- /dev/null
+++ b/spec/handler/bing_spec.rb
@@ -0,0 +1,53 @@
+RSpec.describe Rack::Tracker::Bing do
+
+ it 'will be placed in the body' do
+ expect(described_class.position).to eq(:body)
+ end
+
+ describe "with events" do
+ subject { described_class.new(env, tracker: 'somebody').render }
+
+ describe "default" do
+ def env
+ {'tracker' => {
+ 'bing' => [
+ { 'class_name' => 'Conversion', 'category' => 'Users', 'action' => 'Login', 'label' => 'Standard', 'value' => 10 }
+ ]
+ }}
+ end
+
+ it "will show event initialiser" do
+ expect(subject).to include "window.uetq = window.uetq || [];"
+ end
+
+ it "will show events" do
+ expect(subject).to include "window.uetq.push({ 'ec': 'Users', 'ea': 'Login', 'el': 'Standard', 'ev': 10 });"
+ end
+ end
+ end
+
+ describe "with multiple events" do
+ subject { described_class.new(env, tracker: 'somebody').render }
+
+ describe "default" do
+ def env
+ {'tracker' => {
+ 'bing' => [
+ { 'class_name' => 'Conversion', 'category' => 'Users', 'action' => 'Login', 'label' => 'Standard', 'value' => 10 },
+ { 'class_name' => 'Conversion', 'category' => 'Users', 'action' => 'Logout', 'label' => 'Standard', 'value' => 5 }
+ ]
+ }}
+ end
+
+ it "will show event initialiser" do
+ expect(subject).to include "window.uetq = window.uetq || [];"
+ end
+
+ it "will show events" do
+ expect(subject).to include "window.uetq.push({ 'ec': 'Users', 'ea': 'Login', 'el': 'Standard', 'ev': 10 });"
+ expect(subject).to include "window.uetq.push({ 'ec': 'Users', 'ea': 'Logout', 'el': 'Standard', 'ev': 5 });"
+ end
+ end
+ end
+
+end
\ No newline at end of file
diff --git a/spec/handler/drift_spec.rb b/spec/handler/drift_spec.rb
new file mode 100644
index 0000000..5f96374
--- /dev/null
+++ b/spec/handler/drift_spec.rb
@@ -0,0 +1,10 @@
+RSpec.describe Rack::Tracker::Drift do
+ def env
+ { foo: 'bar' }
+ end
+
+ it 'will be placed in the head' do
+ expect(described_class.position).to eq(:head)
+ expect(described_class.new(env).position).to eq(:head)
+ end
+end
diff --git a/spec/handler/facebook_pixel_spec.rb b/spec/handler/facebook_pixel_spec.rb
index 7f70bee..9716529 100644
--- a/spec/handler/facebook_pixel_spec.rb
+++ b/spec/handler/facebook_pixel_spec.rb
@@ -1,6 +1,6 @@
RSpec.describe Rack::Tracker::FacebookPixel do
def env
- {}
+ { 'PIXEL_ID' => 'DYNAMIC_PIXEL_ID' }
end
it 'will be placed in the body' do
@@ -8,7 +8,7 @@ def env
expect(described_class.new(env).position).to eq(:body)
end
- describe 'with id' do
+ describe 'with static id' do
subject { described_class.new(env, id: 'PIXEL_ID').render }
it 'will push the tracking events to the queue' do
@@ -20,6 +20,18 @@ def env
end
end
+ describe 'with dynamic id' do
+ subject { described_class.new(env, id: lambda { |env| env['PIXEL_ID'] }).render }
+
+ it 'will push the tracking events to the queue' do
+ expect(subject).to match(%r{fbq\('init', 'DYNAMIC_PIXEL_ID'\)})
+ end
+
+ it 'will add the noscript fallback' do
+ expect(subject).to match(%r{https://www.facebook.com/tr\?id=DYNAMIC_PIXEL_ID&ev=PageView&noscript=1})
+ end
+ end
+
describe 'with events' do
def env
{
diff --git a/spec/handler/google_analytics_spec.rb b/spec/handler/google_analytics_spec.rb
index a7cbc8f..6951a02 100644
--- a/spec/handler/google_analytics_spec.rb
+++ b/spec/handler/google_analytics_spec.rb
@@ -53,28 +53,24 @@ def env
end
describe '#tracker_options' do
- before do
- stub_const("#{described_class}::ALLOWED_TRACKER_OPTIONS", [:some_option])
- end
-
context 'with an allowed option configured with a static value' do
- subject { described_class.new(env, { some_option: 'value' }) }
+ subject { described_class.new(env, { user_id: 'value' }) }
it 'returns hash with option set' do
- expect(subject.tracker_options).to eql ({ someOption: 'value' })
+ expect(subject.tracker_options).to eql ({ userId: 'value' })
end
end
context 'with an allowed option configured with a block' do
- subject { described_class.new(env, { some_option: lambda { |env| return env[:misc] } }) }
+ subject { described_class.new(env, { user_id: lambda { |env| return env[:misc] } }) }
it 'returns hash with option set' do
- expect(subject.tracker_options).to eql ({ someOption: 'foobar' })
+ expect(subject.tracker_options).to eql ({ userId: 'foobar' })
end
end
context 'with an allowed option configured with a block returning nil' do
- subject { described_class.new(env, { some_option: lambda { |env| return env[:non_existing_key] } }) }
+ subject { described_class.new(env, { user_id: lambda { |env| return env[:non_existing_key] } }) }
it 'returns an empty hash' do
expect(subject.tracker_options).to eql ({})
@@ -192,7 +188,7 @@ def env
describe "with custom domain" do
subject { described_class.new(env, tracker: 'somebody', cookie_domain: "railslabs.com").render }
- it "will show asyncronous tracker with cookieDomain" do
+ it "will show asynchronous tracker with cookieDomain" do
expect(subject).to match(%r{ga\('create', 'somebody', {\"cookieDomain\":\"railslabs.com\"}\)})
expect(subject).to match(%r{ga\('send', 'pageview', window\.location\.pathname \+ window\.location\.search\)})
end
@@ -201,7 +197,7 @@ def env
describe "with user_id tracking" do
subject { described_class.new(env, tracker: 'somebody', user_id: lambda { |env| return env[:user_id] } ).render }
- it "will show asyncronous tracker with userId" do
+ it "will show asynchronous tracker with userId" do
expect(subject).to match(%r{ga\('create', 'somebody', {\"userId\":\"123\"}\)})
expect(subject).to match(%r{ga\('send', 'pageview', window\.location\.pathname \+ window\.location\.search\)})
end
@@ -239,6 +235,14 @@ def env
end
end
+ describe "with optimize" do
+ subject { described_class.new(env, tracker: 'happy', optimize: 'GTM-1234').render }
+
+ it "will require the optimize plugin with container ID" do
+ expect(subject).to match(%r{ga\('require', 'GTM-1234'\)})
+ end
+ end
+
describe "with anonymizeIp" do
subject { described_class.new(env, tracker: 'happy', anonymize_ip: true).render }
@@ -263,4 +267,38 @@ def env
expect(subject).to include %q{setTimeout(function() { ga('send', 'event', '30_seconds', 'read'); },30000)}
end
end
+
+ describe '#pageview_url_script' do
+ context 'without custom pageview url script' do
+ subject { described_class.new(env, {} ) }
+
+ it 'returns return the custom pageview url script' do
+ expect(subject.pageview_url_script).to eql ("window.location.pathname + window.location.search")
+ end
+ end
+
+ context 'with a custom pageview url script' do
+ subject { described_class.new(env, { pageview_url_script: "{ 'page': location.pathname + location.search + location.hash }"}) }
+
+ it 'returns return the custom pageview url script' do
+ expect(subject.pageview_url_script).to eql ("{ 'page': location.pathname + location.search + location.hash }")
+ end
+ end
+
+ context 'with explicit_pageview disabled' do
+ subject { described_class.new(env, {tracker: 'afake', explicit_pageview: false }).render }
+
+ it 'does not send a pageview event' do
+ expect(subject).not_to include %q{ga('send', 'pageview',}
+ end
+ end
+
+ context 'defaults to sending the pageview event' do
+ subject { described_class.new(env, {tracker: 'afake'}).render }
+
+ it 'does not send a pageview event' do
+ expect(subject).to include "ga('send', 'pageview'"
+ end
+ end
+ end
end
diff --git a/spec/handler/google_global_spec.rb b/spec/handler/google_global_spec.rb
new file mode 100644
index 0000000..0858020
--- /dev/null
+++ b/spec/handler/google_global_spec.rb
@@ -0,0 +1,225 @@
+RSpec.describe Rack::Tracker::GoogleGlobal do
+ def env
+ {
+ misc: 'foobar',
+ user_id: '123'
+ }
+ end
+
+ it 'will be placed in the head' do
+ expect(described_class.position).to eq(:head)
+ expect(described_class.new(env).position).to eq(:head)
+ expect(described_class.new(env, position: :body).position).to eq(:body)
+ end
+
+ describe '#tracker_options' do
+ context 'with an allowed option configured with a static value' do
+ subject { described_class.new(env, user_id: 'value') }
+
+ it 'returns hash with option set' do
+ expect(subject.tracker_options).to eql ({ user_id: 'value' })
+ end
+ end
+
+ context 'with an allowed option configured with a block' do
+ subject { described_class.new(env, user_id: lambda { |env| return env[:misc] }) }
+
+ it 'returns hash with option set' do
+ expect(subject.tracker_options).to eql ({ user_id: 'foobar' })
+ end
+ end
+
+ context 'with an allowed option configured with a block returning nil' do
+ subject { described_class.new(env, user_id: lambda { |env| return env[:non_existing_key] }) }
+
+ it 'returns an empty hash' do
+ expect(subject.tracker_options).to eql ({})
+ end
+ end
+
+ context 'with a non allowed option' do
+ subject { described_class.new(env, new_option: 'value') }
+
+ it 'returns an empty hash' do
+ expect(subject.tracker_options).to eql ({})
+ end
+ end
+ end
+
+ describe '#set_options' do
+ context 'with option configured with a static value' do
+ subject { described_class.new(env, set: { option: 'value' }) }
+
+ it 'returns hash with option set' do
+ expect(subject.set_options).to eql ({ option: 'value' })
+ end
+ end
+
+ context 'with option configured with a block' do
+ subject { described_class.new(env, set: lambda { |env| return { option: env[:misc] } }) }
+
+ it 'returns hash with option set' do
+ expect(subject.set_options).to eql ({ option: 'foobar' })
+ end
+ end
+
+ context 'with option configured with a block returning nil' do
+ subject { described_class.new(env, set: lambda { |env| return env[:non_existing_key] }) }
+
+ it 'returns nil' do
+ expect(subject.set_options).to be nil
+ end
+ end
+ end
+ describe "with custom domain" do
+ let(:tracker) { { id: 'somebody'}}
+ let(:options) { { cookie_domain: "railslabs.com", trackers: [tracker] } }
+ subject { described_class.new(env, options).render }
+
+ it "will show asyncronous tracker with cookie_domain" do
+ expect(subject).to match(%r{gtag\('config', 'somebody', {\"cookie_domain\":\"railslabs.com\"}\)})
+ end
+ end
+
+ describe "with user_id tracking" do
+ let(:tracker) { { id: 'somebody'}}
+ let(:options) { { user_id: lambda { |env| return env[:user_id] }, trackers: [tracker] } }
+ subject { described_class.new(env, options).render }
+
+ it "will show asyncronous tracker with userId" do
+ expect(subject).to match(%r{gtag\('config', 'somebody', {\"user_id\":\"123\"}\)})
+ end
+ end
+
+ describe "with link_attribution" do
+ let(:tracker) { { id: 'happy'}}
+ let(:options) { { link_attribution: true, trackers: [tracker] } }
+ subject { described_class.new(env, options).render }
+
+ it "will show asyncronous tracker with link_attribution" do
+ expect(subject).to match(%r{gtag\('config', 'happy', {\"link_attribution\":true}\)})
+ end
+ end
+
+ describe "with allow_display_features" do
+ let(:tracker) { { id: 'happy'}}
+ let(:options) { { allow_display_features: false, trackers: [tracker] } }
+ subject { described_class.new(env, options).render }
+
+ it "will disable display features" do
+ expect(subject).to match(%r{gtag\('config', 'happy', {\"allow_display_features\":false}\)})
+ end
+ end
+
+ describe "with anonymizeIp" do
+ let(:tracker) { { id: 'happy'}}
+ let(:options) { { anonymize_ip: true, trackers: [tracker] } }
+ subject { described_class.new(env, options).render }
+
+ it "will set anonymizeIp to true" do
+ expect(subject).to match(%r{gtag\('config', 'happy', {\"anonymize_ip\":true}\)})
+ end
+ end
+
+ describe "with dynamic tracker" do
+ let(:tracker) { { id: lambda { |env| return env[:misc] } }}
+ let(:options) { { trackers: [tracker] } }
+ subject { described_class.new(env, options).render }
+
+ it 'will call tracker lambdas to obtain tracking codes' do
+ expect(subject).to match(%r{gtag\('config', 'foobar', {}\)})
+ end
+ end
+
+ describe "with empty tracker" do
+ let(:present_tracker) { { id: 'present' }}
+ let(:empty_tracker) { { id: lambda { |env| return } }}
+ let(:options) { { trackers: [present_tracker, empty_tracker] } }
+ subject { described_class.new(env, options).render }
+
+ it 'will not render config' do
+ expect(subject).to match(%r{gtag\('config', 'present', {}\)})
+ expect(subject).not_to match(%r{gtag\('config', '', {}\)})
+ end
+ end
+
+ describe "with set options" do
+ let(:tracker) { { id: 'with_options' } }
+ let(:options) { { trackers: [tracker], set: { foo: 'bar' } } }
+ subject { described_class.new(env, options).render }
+
+ it 'will show set command' do
+ expect(subject).to match(%r{gtag\('set', {\"foo\":\"bar\"}\)})
+ end
+ end
+
+ describe "with virtual pages" do
+ subject { described_class.new(env, trackers: [{ id: 'somebody' }]).render }
+
+ describe "default" do
+ def env
+ {'tracker' => {
+ 'google_global' => [
+ { 'class_name' => 'Page', 'path' => '/virtual_page' }
+ ]
+ }}
+ end
+
+ it "will show virtual page" do
+ expect(subject).to match(%r{gtag\('config', 'somebody', {\"page_path\":\"/virtual_page\"}\);})
+ end
+ end
+
+ describe "with a event value" do
+ def env
+ {'tracker' => {
+ 'google_global' => [
+ { 'class_name' => 'Page', 'path' => '/virtual_page', 'location' => 'https://example.com/virtual_page', 'title' => 'Virtual Page' }
+ ]
+ }}
+ end
+
+ it "will show virtual page" do
+ expect(subject).to match(%r{gtag\('config', 'somebody', {\"page_title\":\"Virtual Page\",\"page_location\":\"https:\/\/example.com\/virtual_page\",\"page_path\":\"/virtual_page\"}\);})
+ end
+ end
+ end
+
+ describe "with events" do
+ subject { described_class.new(env, trackers: [{ id: 'somebody' }]).render }
+
+ describe "default" do
+ def env
+ {'tracker' => {
+ 'google_global' => [
+ { 'class_name' => 'Event', 'action' => 'login' }
+ ]
+ }}
+ end
+
+ it "will show the event" do
+ expect(subject).to match(%r{gtag\('event', 'login', {}\);})
+ end
+ end
+
+ describe "with event parameters" do
+ def env
+ {'tracker' => {
+ 'google_global' => [
+ { 'class_name' => 'Event',
+ 'action' => 'login',
+ 'category' => 'engagement',
+ 'label' => 'Github',
+ 'value' => 5,
+ 'transaction_id' => 1001,
+ }
+ ]
+ }}
+ end
+
+ it "will show event" do
+ expect(subject).to match(%r{gtag\('event', 'login', {\"event_category\":\"engagement\",\"event_label\":\"Github\",\"value\":5,\"transaction_id\":1001}\);})
+ end
+ end
+ end
+end
diff --git a/spec/handler/google_tag_manager_spec.rb b/spec/handler/google_tag_manager_spec.rb
index ed1e147..d86568e 100644
--- a/spec/handler/google_tag_manager_spec.rb
+++ b/spec/handler/google_tag_manager_spec.rb
@@ -32,4 +32,27 @@ def env
end
end
+ describe '#inject' do
+ subject { handler_object.inject(example_response) }
+ let(:handler_object) { described_class.new(env, container: 'somebody') }
+
+ before do
+ allow(handler_object).to receive(:render_head).and_return('')
+ allow(handler_object).to receive(:render_body).and_return('')
+ end
+
+ context 'with one line html response' do
+ let(:example_response) { "" }
+
+ it 'will have render_head content in head tag' do
+ expect(subject).to match(%r{.*.*})
+ end
+
+ it 'will have render_body content in body tag' do
+ expect(subject).to match(%r{.*.*})
+ end
+
+ end
+ end
+
end
diff --git a/spec/handler/handler_spec.rb b/spec/handler/handler_spec.rb
new file mode 100644
index 0000000..2fabeff
--- /dev/null
+++ b/spec/handler/handler_spec.rb
@@ -0,0 +1,38 @@
+RSpec.describe Rack::Tracker::Handler do
+ def env
+ { misc: 'foobar' }
+ end
+
+ describe '#tracker_options' do
+ context 'without overriding allowed_tracker_options' do
+ subject { described_class.new(env, { some_option: 'value' }) }
+
+ it 'returns an empty hash' do
+ expect(subject.tracker_options).to eql ({})
+ end
+ end
+
+ context 'with overridden allowed_tracker_options' do
+ subject do
+ handler = described_class.new(env, {
+ static_option: 'value',
+ dynamic_option: lambda { |env| return env[:misc] },
+ dynamic_nil_option: lambda { |env| return env[:non_existent_key] },
+ non_allowed_option: 'value'
+ })
+
+ handler.allowed_tracker_options =
+ [:static_option, :dynamic_option, :dynamic_nil_option]
+
+ handler
+ end
+
+ it 'evaluates dynamic options, rejecting nonallowed and nil ones' do
+ expect(subject.tracker_options).to eql ({
+ static_option: 'value',
+ dynamic_option: 'foobar'
+ })
+ end
+ end
+ end
+end
diff --git a/spec/handler/heap_spec.rb b/spec/handler/heap_spec.rb
new file mode 100644
index 0000000..7e48df4
--- /dev/null
+++ b/spec/handler/heap_spec.rb
@@ -0,0 +1,10 @@
+RSpec.describe Rack::Tracker::Heap do
+ def env
+ { foo: 'bar' }
+ end
+
+ it 'will be placed in the head' do
+ expect(described_class.position).to eq(:head)
+ expect(described_class.new(env).position).to eq(:head)
+ end
+end
diff --git a/spec/handler/hubspot_spec.rb b/spec/handler/hubspot_spec.rb
new file mode 100644
index 0000000..ea36178
--- /dev/null
+++ b/spec/handler/hubspot_spec.rb
@@ -0,0 +1,11 @@
+RSpec.describe Rack::Tracker::Hubspot do
+
+ def env
+ { misc: '42' }
+ end
+
+ it 'will be placed in the head' do
+ expect(described_class.position).to eq(:head)
+ expect(described_class.new(env).position).to eq(:head)
+ end
+end
diff --git a/spec/integration/bing_integration_spec.rb b/spec/integration/bing_integration_spec.rb
new file mode 100644
index 0000000..e68b8fd
--- /dev/null
+++ b/spec/integration/bing_integration_spec.rb
@@ -0,0 +1,17 @@
+require 'support/capybara_app_helper'
+
+RSpec.describe "Bing Integration" do
+ before do
+ setup_app(action: :bing) do |tracker|
+ tracker.handler :bing, { tracker: '12345678' }
+ end
+ visit '/'
+ end
+
+ subject { page }
+
+ it "embeds the script tag with tracker" do
+ expect(page.find("body")).to have_content('var o = {ti: "12345678"};')
+ end
+
+end
diff --git a/spec/integration/drift_integration_spec.rb b/spec/integration/drift_integration_spec.rb
new file mode 100644
index 0000000..498538f
--- /dev/null
+++ b/spec/integration/drift_integration_spec.rb
@@ -0,0 +1,18 @@
+require 'support/capybara_app_helper'
+
+RSpec.describe 'Drift Integration' do
+ before do
+ setup_app(action: :drift) do |tracker|
+ tracker.handler :drift, account_id: 'DRIFT_ID'
+ end
+
+ visit '/'
+ end
+
+ subject { page }
+
+ it 'embeds the script with account_id' do
+ expect(page.find('script')).to have_content('js.driftt.com')
+ expect(page.find('script')).to have_content('drift.load(\'DRIFT_ID\')')
+ end
+end
diff --git a/spec/integration/google_analytics_integration_spec.rb b/spec/integration/google_analytics_integration_spec.rb
index 7f5888a..ba59ea2 100644
--- a/spec/integration/google_analytics_integration_spec.rb
+++ b/spec/integration/google_analytics_integration_spec.rb
@@ -16,6 +16,10 @@
expect(page.find("head")).to have_content('ga("send",{"hitType":"event","eventCategory":"button","eventAction":"click","eventLabel":"nav-buttons","eventValue":"X"})')
end
+ it "will have default pageview url script" do
+ expect(page.find("head")).to have_content("ga('send', 'pageview', window.location.pathname + window.location.search);")
+ end
+
describe 'adjust tracker position via options' do
before do
setup_app(action: :google_analytics) do |tracker|
@@ -49,4 +53,18 @@
expect(page.find('head')).to have_content %q{Author\\'s name}
end
end
-end
\ No newline at end of file
+
+ describe 'Use custom pageview script' do
+ before do
+ setup_app(action: :google_analytics) do |tracker|
+ tracker.handler :google_analytics, { tracker: 'U-XXX-Y', pageview_url_script: "{ 'page': location.pathname + location.search + location.hash }"}
+ end
+ visit '/'
+ end
+
+ it "will use the custom pageview script for the pageview event" do
+ expect(page.find("head")).to have_content("ga('send', 'pageview', { 'page': location.pathname + location.search + location.hash });")
+ end
+ end
+
+end
diff --git a/spec/integration/google_global_integration_spec.rb b/spec/integration/google_global_integration_spec.rb
new file mode 100644
index 0000000..14df465
--- /dev/null
+++ b/spec/integration/google_global_integration_spec.rb
@@ -0,0 +1,41 @@
+require 'support/capybara_app_helper'
+
+RSpec.describe "Google Global Integration Integration" do
+ before do
+ setup_app(action: :google_global) do |tracker|
+ tracker.handler :google_global, tracker_options
+ end
+ visit '/'
+ end
+
+ let(:tracker_options) { { trackers: [{ id: 'U-XXX-Y' }] } }
+
+ it "embeds the script tag with tracking event from the controller action" do
+ expect(page.find("head")).to have_content('U-XXX-Y')
+ end
+
+ describe 'adjust tracker position via options' do
+ let(:tracker_options) { { trackers: [{ id: 'U-XXX-Y' }], position: :body } }
+
+ it "will be placed in the specified tag" do
+ expect(page.find("head")).to_not have_content('U-XXX-Y')
+ expect(page.find("body")).to have_content('U-XXX-Y')
+ end
+ end
+
+ describe "handles empty tracker id" do
+ let(:tracker_options) { { trackers: [{ id: nil }, { id: "" }, { id: " " }] } }
+
+ it "does not inject scripts" do
+ expect(page.find("head")).to_not have_content("
-
+
+