diff --git a/.travis.yml b/.travis.yml index b754765..3c8e02f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,8 +6,18 @@ rvm: - 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.2 + - Gemfile.rails-6.0 +matrix: + exclude: + - 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 3603277..4398e32 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,39 @@ +# 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) 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 499098a..e1b7927 100644 --- a/README.md +++ b/README.md @@ -18,18 +18,9 @@ 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: - -* [Google Global Site Tag](#google-global) -* [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) +but to get you started we're shipping support for the services [mentioned below](#services) +out of the box: + ## Respecting the Do Not Track (DNT) HTTP header @@ -112,6 +103,8 @@ 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. @@ -147,6 +140,7 @@ end * `: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 @@ -576,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 1a7c899..4e10d87 100644 --- a/lib/rack/tracker.rb +++ b/lib/rack/tracker.rb @@ -23,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 @@ -34,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"] @@ -48,16 +56,14 @@ 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| 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/google_analytics/google_analytics.rb b/lib/rack/tracker/google_analytics/google_analytics.rb index a18d1e0..f33663b 100644 --- a/lib/rack/tracker/google_analytics/google_analytics.rb +++ b/lib/rack/tracker/google_analytics/google_analytics.rb @@ -2,6 +2,11 @@ class Rack::Tracker::GoogleAnalytics < Rack::Tracker::Handler 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 = {}) attrs.reverse_merge!(type: 'event') diff --git a/lib/rack/tracker/google_analytics/template/google_analytics.erb b/lib/rack/tracker/google_analytics/template/google_analytics.erb index 000ff7c..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 index 764ee02..5ccb817 100644 --- a/lib/rack/tracker/google_global/google_global.rb +++ b/lib/rack/tracker/google_global/google_global.rb @@ -9,14 +9,33 @@ def params 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 - events # TODO: Filter pages after Event is implemented + select_handler_events(Page) + end + + alias handler_events events + + def events + select_handler_events(Event) end def trackers - options[:trackers].map { |tracker| - tracker[:id].respond_to?(:call) ? tracker.merge(id: tracker[:id].call(env)) : tracker - }.reject { |tracker| tracker[:id].nil? } + @_trackers ||= build_trackers end def set_options @@ -25,8 +44,37 @@ def set_options 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 index ea656a3..9103419 100644 --- a/lib/rack/tracker/google_global/template/google_global.erb +++ b/lib/rack/tracker/google_global/template/google_global.erb @@ -1,4 +1,4 @@ -<% if trackers %> +<% 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 2db9d2b..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,7 +10,7 @@ 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| 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 f6d5d5f..3cf2d99 100644 --- a/lib/rack/tracker/version.rb +++ b/lib/rack/tracker/version.rb @@ -1,5 +1,5 @@ module Rack class Tracker - VERSION = '1.8.0' + VERSION = '1.13.0' end end 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/google_analytics_spec.rb b/spec/handler/google_analytics_spec.rb index 5d46e8d..6951a02 100644 --- a/spec/handler/google_analytics_spec.rb +++ b/spec/handler/google_analytics_spec.rb @@ -284,5 +284,21 @@ def env 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 index d955bdd..0858020 100644 --- a/spec/handler/google_global_spec.rb +++ b/spec/handler/google_global_spec.rb @@ -184,4 +184,42 @@ def env 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/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_global_integration_spec.rb b/spec/integration/google_global_integration_spec.rb index 7079036..14df465 100644 --- a/spec/integration/google_global_integration_spec.rb +++ b/spec/integration/google_global_integration_spec.rb @@ -3,28 +3,39 @@ RSpec.describe "Google Global Integration Integration" do before do setup_app(action: :google_global) do |tracker| - tracker.handler :google_global, trackers: [{ id: 'U-XXX-Y' }] + tracker.handler :google_global, tracker_options end visit '/' end - subject { page } + 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 - before do - setup_app(action: :google_global) do |tracker| - tracker.handler :google_global, trackers: [{ id: 'U-XXX-Y' }], position: :body - end - visit '/' - end + 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(" - + +

welcome to metal#index

- + HTML diff --git a/spec/support/metal_controller.rb b/spec/support/metal_controller.rb index 2bb9493..f22447e 100644 --- a/spec/support/metal_controller.rb +++ b/spec/support/metal_controller.rb @@ -116,4 +116,20 @@ def zanox def hotjar render "metal/index" end + + def hubspot + render "metal/index" + end + + def bing + render "metal/index" + end + + def drift + render "metal/index" + end + + def heap + render "metal/index" + end end