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 2622f17..4398e32 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,33 @@ +# 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) 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 caa936e..e1b7927 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ 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 services [mentioned below](#services) +but to get you started we're shipping support for the services [mentioned below](#services) out of the box: @@ -140,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 @@ -594,6 +595,37 @@ tracker do |t| 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 c3a72f4..4e10d87 100644 --- a/lib/rack/tracker.rb +++ b/lib/rack/tracker.rb @@ -24,6 +24,9 @@ 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 @@ -35,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"] @@ -49,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/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 4214ea3..5ccb817 100644 --- a/lib/rack/tracker/google_global/google_global.rb +++ b/lib/rack/tracker/google_global/google_global.rb @@ -11,11 +11,10 @@ def params class Event < OpenStruct PREFIXED_PARAMS = %i[category label] - LITERAL_PARAMS = %i[value] - PARAMS = PREFIXED_PARAMS + LITERAL_PARAMS + SKIP_PARAMS = %i[action] def params - Hash[to_h.slice(*PARAMS).map { |key, value| [param_key(key), value] }] + Hash[to_h.except(*SKIP_PARAMS).map { |key, value| [param_key(key), value] }] end private @@ -36,9 +35,7 @@ def events 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 @@ -47,6 +44,31 @@ 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 diff --git a/lib/rack/tracker/google_global/template/google_global.erb b/lib/rack/tracker/google_global/template/google_global.erb index b6141a8..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 %> - <% 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 c48fbdb..3cf2d99 100644 --- a/lib/rack/tracker/version.rb +++ b/lib/rack/tracker/version.rb @@ -1,5 +1,5 @@ module Rack class Tracker - VERSION = '1.9.0' + VERSION = '1.13.0' end end 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 dcf6fcc..0858020 100644 --- a/spec/handler/google_global_spec.rb +++ b/spec/handler/google_global_spec.rb @@ -210,13 +210,15 @@ def env 'action' => 'login', 'category' => 'engagement', 'label' => 'Github', - 'value' => 5 } + '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}\);}) + expect(subject).to match(%r{gtag\('event', 'login', {\"event_category\":\"engagement\",\"event_label\":\"Github\",\"value\":5,\"transaction_id\":1001}\);}) 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/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(" - + +