From f2ead8576a97bc6e711cbcbf47f4d685456e657e Mon Sep 17 00:00:00 2001 From: Benjamin Hoffman Date: Wed, 22 Mar 2017 11:56:24 -0700 Subject: [PATCH 001/179] Enforces userId anonId requirement --- lib/segment/analytics/client.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/segment/analytics/client.rb b/lib/segment/analytics/client.rb index 138f819..f920ee5 100644 --- a/lib/segment/analytics/client.rb +++ b/lib/segment/analytics/client.rb @@ -355,7 +355,7 @@ def event attrs end def check_user_id! attrs - fail ArgumentError, 'Must supply either user_id or anonymous_id' unless attrs[:user_id] || attrs[:anonymous_id] + fail ArgumentError, 'Must supply either user_id or anonymous_id' unless attrs[:user_id].present? || attrs[:anonymous_id].present? end def ensure_worker_running From 74597d338b2662aec5f71300f97c9fb9c3f7713a Mon Sep 17 00:00:00 2001 From: Benjamin Date: Wed, 22 Mar 2017 20:46:42 -0700 Subject: [PATCH 002/179] Adds spec --- spec/segment/analytics/client_spec.rb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/spec/segment/analytics/client_spec.rb b/spec/segment/analytics/client_spec.rb index 251962b..8befd62 100644 --- a/spec/segment/analytics/client_spec.rb +++ b/spec/segment/analytics/client_spec.rb @@ -99,6 +99,10 @@ class Analytics expect { client.identify({}) }.to raise_error(ArgumentError) end + it 'errors on an empty string' do + expect { client.identify({ :user_id => '' }) }.to raise_error(ArgumentError) + end + it 'does not error with the required options' do expect do client.identify Queued::IDENTIFY From 4cb5548b68bd9533835fe33aa76ff0f4dcb51602 Mon Sep 17 00:00:00 2001 From: Anand Soni Date: Wed, 12 Apr 2017 18:46:58 +0530 Subject: [PATCH 003/179] removed gem wrong and created helper for method --- analytics-ruby.gemspec | 1 - spec/spec_helper.rb | 27 ++++++++++++++++++++++++--- 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/analytics-ruby.gemspec b/analytics-ruby.gemspec index 416bf16..1c18d8a 100644 --- a/analytics-ruby.gemspec +++ b/analytics-ruby.gemspec @@ -19,7 +19,6 @@ Gem::Specification.new do |spec| spec.add_dependency 'commander', '~> 4.4' spec.add_development_dependency 'rake', '~> 10.3' - spec.add_development_dependency 'wrong', '~> 0.0' spec.add_development_dependency 'rspec', '~> 2.0' spec.add_development_dependency 'tzinfo', '1.2.1' spec.add_development_dependency 'activesupport', '>= 3.0.0', '<4.0.0' diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 5fc2ff7..420be8a 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,9 +1,6 @@ require 'segment/analytics' -require 'wrong' require 'active_support/time' -include Wrong - # Setting timezone for ActiveSupport::TimeWithZone to UTC Time.zone = 'UTC' @@ -79,3 +76,27 @@ module Requested end end end + +# usage: +# it "should return a result of 5" do +# eventually(options: {timeout: 1}) { long_running_thing.result.should eq(5) } +# end + +module AsyncHelper + def eventually(options: {}) + timeout = options[:timeout] || 2 + interval = options[:interval] || 0.1 + time_limit = Time.now + timeout + loop do + begin + yield + rescue => error + end + return if error.nil? + raise error if Time.now >= time_limit + sleep interval + end + end +end + +include AsyncHelper \ No newline at end of file From 7dcb4026c6f3b6fa1f23420ee2bad28882c24880 Mon Sep 17 00:00:00 2001 From: Anand Soni Date: Wed, 12 Apr 2017 19:59:07 +0530 Subject: [PATCH 004/179] fix error in jruby --- spec/spec_helper.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 420be8a..ea815ae 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -83,7 +83,7 @@ module Requested # end module AsyncHelper - def eventually(options: {}) + def eventually(options = {}) timeout = options[:timeout] || 2 interval = options[:interval] || 0.1 time_limit = Time.now + timeout From 6b96679687f50c8e3d647682746ee6db1ac323cf Mon Sep 17 00:00:00 2001 From: Anand Soni Date: Wed, 12 Apr 2017 20:18:17 +0530 Subject: [PATCH 005/179] installing latest version of bunlder in travis to fix error in 1.9.3 --- .travis.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.travis.yml b/.travis.yml index 1a02c9f..c63d1b9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,3 +5,6 @@ rvm: - 1.9.3 - 2.0.0 - 2.1.0 + +before_install: + - gem install bundler \ No newline at end of file From cbd0fabdf6f502b5f78317f7f1ed210620d31477 Mon Sep 17 00:00:00 2001 From: Anand Soni Date: Wed, 12 Apr 2017 23:53:44 +0530 Subject: [PATCH 006/179] changing default timeout and default interval value in Asynchelper#eventually --- spec/spec_helper.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index ea815ae..024f380 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -84,8 +84,8 @@ module Requested module AsyncHelper def eventually(options = {}) - timeout = options[:timeout] || 2 - interval = options[:interval] || 0.1 + timeout = options[:timeout] || 5 #seconds + interval = options[:interval] || 0.25 #seconds time_limit = Time.now + timeout loop do begin From 90c7826b533f8c83b242aae3f4a61ed3def729e9 Mon Sep 17 00:00:00 2001 From: Anand Soni Date: Thu, 13 Apr 2017 11:49:25 +0530 Subject: [PATCH 007/179] fix failing test of wroker spec. Used present? method to check batch is currently processing. --- lib/segment/analytics/worker.rb | 4 +--- spec/segment/analytics/worker_spec.rb | 6 ++++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/segment/analytics/worker.rb b/lib/segment/analytics/worker.rb index 9288c85..90d21a2 100644 --- a/lib/segment/analytics/worker.rb +++ b/lib/segment/analytics/worker.rb @@ -41,11 +41,9 @@ def run @batch << @queue.pop end end - res = Request.new.post @write_key, @batch @on_error.call res.status, res.error unless res.status == 200 - @lock.synchronize { @batch.clear } end end @@ -53,7 +51,7 @@ def run # public: Check whether we have outstanding requests. # def is_requesting? - @lock.synchronize { !@batch.empty? } + @lock.synchronize { @batch.present? } end end end diff --git a/spec/segment/analytics/worker_spec.rb b/spec/segment/analytics/worker_spec.rb index 1accd22..766bb43 100644 --- a/spec/segment/analytics/worker_spec.rb +++ b/spec/segment/analytics/worker_spec.rb @@ -85,16 +85,18 @@ class Analytics end it 'returns true if there is a current batch' do + queue = Queue.new queue << Requested::TRACK worker = Segment::Analytics::Worker.new(queue, 'testsecret') Thread.new do worker.run - expect(worker.is_requesting?).to eq(false) + expect(worker.is_requesting?).to eq(true) end - eventually { expect(worker.is_requesting?).to eq(true) } + eventually { expect(worker.is_requesting?).to eq(false) } + end end end From bba8c7f4fafa25f1fb2e31445921343c457775b9 Mon Sep 17 00:00:00 2001 From: Anand Soni Date: Thu, 13 Apr 2017 20:30:03 +0530 Subject: [PATCH 008/179] adding comment for race condition and fix suggested changes --- lib/segment/analytics/worker.rb | 3 ++- spec/segment/analytics/worker_spec.rb | 1 - 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/segment/analytics/worker.rb b/lib/segment/analytics/worker.rb index 90d21a2..d105bc2 100644 --- a/lib/segment/analytics/worker.rb +++ b/lib/segment/analytics/worker.rb @@ -41,6 +41,7 @@ def run @batch << @queue.pop end end + res = Request.new.post @write_key, @batch @on_error.call res.status, res.error unless res.status == 200 @@ -51,7 +52,7 @@ def run # public: Check whether we have outstanding requests. # def is_requesting? - @lock.synchronize { @batch.present? } + @lock.synchronize { @batch.any? } end end end diff --git a/spec/segment/analytics/worker_spec.rb b/spec/segment/analytics/worker_spec.rb index 766bb43..de7839b 100644 --- a/spec/segment/analytics/worker_spec.rb +++ b/spec/segment/analytics/worker_spec.rb @@ -85,7 +85,6 @@ class Analytics end it 'returns true if there is a current batch' do - queue = Queue.new queue << Requested::TRACK worker = Segment::Analytics::Worker.new(queue, 'testsecret') From 949612e93475da5d30d5ab60dca31c82e02f4b47 Mon Sep 17 00:00:00 2001 From: Anand Soni Date: Thu, 13 Apr 2017 20:38:18 +0530 Subject: [PATCH 009/179] adding comment to worker spec about race condition --- spec/segment/analytics/worker_spec.rb | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/spec/segment/analytics/worker_spec.rb b/spec/segment/analytics/worker_spec.rb index de7839b..ad879d8 100644 --- a/spec/segment/analytics/worker_spec.rb +++ b/spec/segment/analytics/worker_spec.rb @@ -84,6 +84,12 @@ class Analytics expect(worker.is_requesting?).to eq(false) end + # Race Condition + # When we call worker.ran it creates new Request object and + # do post request. So we expect to check a batch is running. + # In eventually function we check for 5 seconds that + # all batches finished its process. + it 'returns true if there is a current batch' do queue = Queue.new queue << Requested::TRACK From cedf380eb90407471e47230281f7154796c4655f Mon Sep 17 00:00:00 2001 From: Benjamin Hoffman Date: Fri, 14 Apr 2017 00:22:38 -0700 Subject: [PATCH 010/179] adds spec for anonymous_id --- spec/segment/analytics/client_spec.rb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/spec/segment/analytics/client_spec.rb b/spec/segment/analytics/client_spec.rb index 8befd62..c41a7e6 100644 --- a/spec/segment/analytics/client_spec.rb +++ b/spec/segment/analytics/client_spec.rb @@ -99,6 +99,10 @@ class Analytics expect { client.identify({}) }.to raise_error(ArgumentError) end + it 'errors on an empty anonymous_id' do + expect { client.identify({ :anonymous_id => '' }) }.to raise_error(ArgumentError) + end + it 'errors on an empty string' do expect { client.identify({ :user_id => '' }) }.to raise_error(ArgumentError) end From 675867197db193a6839465693272cab669935701 Mon Sep 17 00:00:00 2001 From: Benjamin Hoffman Date: Fri, 14 Apr 2017 00:49:02 -0700 Subject: [PATCH 011/179] run spec --- spec/segment/analytics/client_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/segment/analytics/client_spec.rb b/spec/segment/analytics/client_spec.rb index c41a7e6..94ba084 100644 --- a/spec/segment/analytics/client_spec.rb +++ b/spec/segment/analytics/client_spec.rb @@ -103,7 +103,7 @@ class Analytics expect { client.identify({ :anonymous_id => '' }) }.to raise_error(ArgumentError) end - it 'errors on an empty string' do + it 'errors on an empty user_id' do expect { client.identify({ :user_id => '' }) }.to raise_error(ArgumentError) end From 4f59494c3b07551d4e2ee0070b6daa08cee21651 Mon Sep 17 00:00:00 2001 From: Lucas Oliveira Date: Mon, 8 May 2017 22:07:30 -0300 Subject: [PATCH 012/179] Update readme (#89) * Fix typo and split sample use into different code blocks --- README.md | 51 ++++++++++++++++++++++----------------------------- 1 file changed, 22 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index 7e1e1c4..c7c07fd 100644 --- a/README.md +++ b/README.md @@ -8,50 +8,43 @@ analytics-ruby is a ruby client for [Segment](https://segment.com) ## Install Into Gemfile from rubygems.org: -``` -gem 'analytics-ruby', :require => "segment" +```ruby +gem 'analytics-ruby', require: "segment" ``` Into environment gems from rubygems.org: -``` +```ruby gem install 'analytics-ruby' ``` ## Usage Create an instance of the Analytics object: +```ruby +analytics = Segment::Analytics.new(write_key: 'YOUR_WRITE_KEY') ``` -analytics = Segment::Analytics.new({ - write_key: 'YOUR_WRITE_KEY' -}) + +Identify the user for the people section, see more [here](https://segment.com/docs/libraries/ruby/#identify). +```ruby +analytics.identify(user_id: 42, + traits: { + email: 'name@example.com', + first_name: 'Foo', + last_name: 'Bar' + }) ``` -Sample usage: +Alias an user, see more [here](https://segment.com/docs/libraries/ruby/#alias). +```ruby +analytics.alias(user_id: 41) ``` -user = User.last - -# Identify the user for the people section -analytics.identify( - { - user_id: user.id, - traits: { - email: user.email, - first_name: user.first_name, - last_name: user.last_name - } - } -) - -# Track a user event -analytics.track( - { - user_id: user.id, - event: 'Created Account' - } -) + +Track a user event, see more [here](https://segment.com/docs/libraries/ruby/#track). +```ruby +analytics.track(user_id: 42, event: 'Created Account') ``` -Refer to the section below for documenation on individual available calls. +There are a few calls available, please check the documentation section. ## Documentation From 7b71cb37fa676e52b8402ce768925b052135e27c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20M=C3=BAnera=20S=C3=A1nchez?= Date: Thu, 14 Sep 2017 20:49:37 -0500 Subject: [PATCH 013/179] implement respond_to_missing? instead of respond_to? (#120) --- lib/segment/analytics.rb | 2 +- spec/segment/analytics_spec.rb | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/lib/segment/analytics.rb b/lib/segment/analytics.rb index 78d6c28..da0b400 100644 --- a/lib/segment/analytics.rb +++ b/lib/segment/analytics.rb @@ -22,7 +22,7 @@ def method_missing message, *args, &block end end - def respond_to? method_name, include_private = false + def respond_to_missing?(method_name, include_private = false) @client.respond_to?(method_name) || super end diff --git a/spec/segment/analytics_spec.rb b/spec/segment/analytics_spec.rb index a2aa885..bc06703 100644 --- a/spec/segment/analytics_spec.rb +++ b/spec/segment/analytics_spec.rb @@ -101,6 +101,20 @@ class Analytics end.to_not raise_error end end + + describe '#respond_to?' do + it 'responds to all public instance methods of Segment::Analytics::Client' do + expect(analytics).to respond_to(*Segment::Analytics::Client.public_instance_methods(false)) + end + end + + describe '#method' do + Segment::Analytics::Client.public_instance_methods(false).each do |public_method| + it "returns a Method object with '#{public_method}' as argument" do + expect(analytics.method(public_method).class).to eq(Method) + end + end + end end end end From 4cc38b9c1ed79c86233c638f41bc52e2fd3574bd Mon Sep 17 00:00:00 2001 From: Prateek Srivastava Date: Thu, 14 Sep 2017 21:14:15 -0700 Subject: [PATCH 014/179] Revert "Enforces userId anonId requirement" --- lib/segment/analytics/client.rb | 2 +- spec/segment/analytics/client_spec.rb | 8 -------- 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/lib/segment/analytics/client.rb b/lib/segment/analytics/client.rb index f920ee5..138f819 100644 --- a/lib/segment/analytics/client.rb +++ b/lib/segment/analytics/client.rb @@ -355,7 +355,7 @@ def event attrs end def check_user_id! attrs - fail ArgumentError, 'Must supply either user_id or anonymous_id' unless attrs[:user_id].present? || attrs[:anonymous_id].present? + fail ArgumentError, 'Must supply either user_id or anonymous_id' unless attrs[:user_id] || attrs[:anonymous_id] end def ensure_worker_running diff --git a/spec/segment/analytics/client_spec.rb b/spec/segment/analytics/client_spec.rb index 94ba084..251962b 100644 --- a/spec/segment/analytics/client_spec.rb +++ b/spec/segment/analytics/client_spec.rb @@ -99,14 +99,6 @@ class Analytics expect { client.identify({}) }.to raise_error(ArgumentError) end - it 'errors on an empty anonymous_id' do - expect { client.identify({ :anonymous_id => '' }) }.to raise_error(ArgumentError) - end - - it 'errors on an empty user_id' do - expect { client.identify({ :user_id => '' }) }.to raise_error(ArgumentError) - end - it 'does not error with the required options' do expect do client.identify Queued::IDENTIFY From 6fc06965905a62937e1cb4cf3a124b2dfa7afdbc Mon Sep 17 00:00:00 2001 From: Prateek Srivastava Date: Thu, 14 Sep 2017 21:15:56 -0700 Subject: [PATCH 015/179] Revert "Fix failing test in worker spec" --- lib/segment/analytics/worker.rb | 3 ++- spec/segment/analytics/worker_spec.rb | 11 ++--------- spec/spec_helper.rb | 4 ++-- 3 files changed, 6 insertions(+), 12 deletions(-) diff --git a/lib/segment/analytics/worker.rb b/lib/segment/analytics/worker.rb index d105bc2..9288c85 100644 --- a/lib/segment/analytics/worker.rb +++ b/lib/segment/analytics/worker.rb @@ -45,6 +45,7 @@ def run res = Request.new.post @write_key, @batch @on_error.call res.status, res.error unless res.status == 200 + @lock.synchronize { @batch.clear } end end @@ -52,7 +53,7 @@ def run # public: Check whether we have outstanding requests. # def is_requesting? - @lock.synchronize { @batch.any? } + @lock.synchronize { !@batch.empty? } end end end diff --git a/spec/segment/analytics/worker_spec.rb b/spec/segment/analytics/worker_spec.rb index ad879d8..1accd22 100644 --- a/spec/segment/analytics/worker_spec.rb +++ b/spec/segment/analytics/worker_spec.rb @@ -84,12 +84,6 @@ class Analytics expect(worker.is_requesting?).to eq(false) end - # Race Condition - # When we call worker.ran it creates new Request object and - # do post request. So we expect to check a batch is running. - # In eventually function we check for 5 seconds that - # all batches finished its process. - it 'returns true if there is a current batch' do queue = Queue.new queue << Requested::TRACK @@ -97,11 +91,10 @@ class Analytics Thread.new do worker.run - expect(worker.is_requesting?).to eq(true) + expect(worker.is_requesting?).to eq(false) end - eventually { expect(worker.is_requesting?).to eq(false) } - + eventually { expect(worker.is_requesting?).to eq(true) } end end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 024f380..ea815ae 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -84,8 +84,8 @@ module Requested module AsyncHelper def eventually(options = {}) - timeout = options[:timeout] || 5 #seconds - interval = options[:interval] || 0.25 #seconds + timeout = options[:timeout] || 2 + interval = options[:interval] || 0.1 time_limit = Time.now + timeout loop do begin From 246e8bac180da18159c1c06079b36a4899115b3a Mon Sep 17 00:00:00 2001 From: Prateek Srivastava Date: Thu, 14 Sep 2017 21:25:26 -0700 Subject: [PATCH 016/179] Release 2.2.3.pre --- History.md | 6 ++++++ lib/segment/analytics/version.rb | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/History.md b/History.md index c188aed..7dfa15c 100644 --- a/History.md +++ b/History.md @@ -1,4 +1,10 @@ +2.2.3.pre / 2017-09-14 +================== + + * [Fix](https://github.com/segmentio/analytics-ruby/pull/120): Override `respond_to_missing` instead of `respond_to?` to facilitate mock the library in tests. + + 2.2.2 / 2016-08-03 ================== diff --git a/lib/segment/analytics/version.rb b/lib/segment/analytics/version.rb index 939d338..df7b3d0 100644 --- a/lib/segment/analytics/version.rb +++ b/lib/segment/analytics/version.rb @@ -1,5 +1,5 @@ module Segment class Analytics - VERSION = '2.2.2' + VERSION = '2.2.3.pre' end end From fb3d56a9ea22a596ca68adec67154c6c0b803ff0 Mon Sep 17 00:00:00 2001 From: Prateek Srivastava Date: Fri, 1 Sep 2017 15:06:54 -0700 Subject: [PATCH 017/179] Update platforms to test against on CI. * Use latest 2.1 release (2.1.10). * Add latest 2.2, 2.3 and 2.4 releases. --- .travis.yml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index c63d1b9..0638b59 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,7 +4,10 @@ rvm: - jruby-19mode - 1.9.3 - 2.0.0 - - 2.1.0 + - 2.1.10 + - 2.2.8 + - 2.3.5 + - 2.4.2 before_install: - - gem install bundler \ No newline at end of file + - gem install bundler From 3fefcf0274508abcd20de02807c904883931b9a4 Mon Sep 17 00:00:00 2001 From: Paul Kuruvilla Date: Tue, 21 Nov 2017 11:51:24 +0530 Subject: [PATCH 018/179] Update rspec to ~> 3.0 --- analytics-ruby.gemspec | 2 +- spec/spec_helper.rb | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/analytics-ruby.gemspec b/analytics-ruby.gemspec index 1c18d8a..2df0d8a 100644 --- a/analytics-ruby.gemspec +++ b/analytics-ruby.gemspec @@ -19,7 +19,7 @@ Gem::Specification.new do |spec| spec.add_dependency 'commander', '~> 4.4' spec.add_development_dependency 'rake', '~> 10.3' - spec.add_development_dependency 'rspec', '~> 2.0' + spec.add_development_dependency 'rspec', '~> 3.0' spec.add_development_dependency 'tzinfo', '1.2.1' spec.add_development_dependency 'activesupport', '>= 3.0.0', '<4.0.0' end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index ea815ae..2e06002 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -90,7 +90,7 @@ def eventually(options = {}) loop do begin yield - rescue => error + rescue RSpec::Expectations::ExpectationNotMetError => error end return if error.nil? raise error if Time.now >= time_limit @@ -99,4 +99,4 @@ def eventually(options = {}) end end -include AsyncHelper \ No newline at end of file +include AsyncHelper From e152c6b82ac1f85f0188b0ba0b3d2bf572c8e0bb Mon Sep 17 00:00:00 2001 From: Paul Kuruvilla Date: Tue, 21 Nov 2017 15:33:17 +0530 Subject: [PATCH 019/179] Update ActiveSupport ActiveSupport is used as a testing dependency to ensure that ActiveSupport::TimeWithZone objects are converted to iso8601 format correctly. Because of recent changes in ActiveSupport, there is a change in the way `DateTime`s are converted to `Time`s, specs have been changed to reflect that. Reference: https://github.com/rails/rails/commit/b79adc4323ff289aed3f5787fdfbb9542aa4f89f --- analytics-ruby.gemspec | 2 +- spec/segment/analytics/client_spec.rb | 33 +++++++++++++++------------ 2 files changed, 19 insertions(+), 16 deletions(-) diff --git a/analytics-ruby.gemspec b/analytics-ruby.gemspec index 2df0d8a..d00fe5a 100644 --- a/analytics-ruby.gemspec +++ b/analytics-ruby.gemspec @@ -21,5 +21,5 @@ Gem::Specification.new do |spec| spec.add_development_dependency 'rake', '~> 10.3' spec.add_development_dependency 'rspec', '~> 3.0' spec.add_development_dependency 'tzinfo', '1.2.1' - spec.add_development_dependency 'activesupport', '>= 3.0.0', '<4.0.0' + spec.add_development_dependency 'activesupport', '~> 4.1.11' end diff --git a/spec/segment/analytics/client_spec.rb b/spec/segment/analytics/client_spec.rb index 251962b..d39dc7a 100644 --- a/spec/segment/analytics/client_spec.rb +++ b/spec/segment/analytics/client_spec.rb @@ -85,11 +85,12 @@ class Analytics }) message = queue.pop - expect(message[:properties][:time]).to eq('2013-01-01T00:00:00.000Z') - expect(message[:properties][:time_with_zone]).to eq('2013-01-01T00:00:00.000Z') - expect(message[:properties][:date_time]).to eq('2013-01-01T00:00:00.000Z') - expect(message[:properties][:date]).to eq('2013-01-01') - expect(message[:properties][:nottime]).to eq('x') + properties = message[:properties] + expect(properties[:time]).to eq('2013-01-01T00:00:00.000Z') + expect(properties[:time_with_zone]).to eq('2013-01-01T00:00:00.000Z') + expect(properties[:date_time]).to eq('2013-01-01T00:00:00.000+00:00') + expect(properties[:date]).to eq('2013-01-01') + expect(properties[:nottime]).to eq('x') end end @@ -127,11 +128,12 @@ class Analytics message = queue.pop - expect(message[:traits][:time]).to eq('2013-01-01T00:00:00.000Z') - expect(message[:traits][:time_with_zone]).to eq('2013-01-01T00:00:00.000Z') - expect(message[:traits][:date_time]).to eq('2013-01-01T00:00:00.000Z') - expect(message[:traits][:date]).to eq('2013-01-01') - expect(message[:traits][:nottime]).to eq('x') + traits = message[:traits] + expect(traits[:time]).to eq('2013-01-01T00:00:00.000Z') + expect(traits[:time_with_zone]).to eq('2013-01-01T00:00:00.000Z') + expect(traits[:date_time]).to eq('2013-01-01T00:00:00.000+00:00') + expect(traits[:date]).to eq('2013-01-01') + expect(traits[:nottime]).to eq('x') end end @@ -191,11 +193,12 @@ class Analytics message = queue.pop - expect(message[:traits][:time]).to eq('2013-01-01T00:00:00.000Z') - expect(message[:traits][:time_with_zone]).to eq('2013-01-01T00:00:00.000Z') - expect(message[:traits][:date_time]).to eq('2013-01-01T00:00:00.000Z') - expect(message[:traits][:date]).to eq('2013-01-01') - expect(message[:traits][:nottime]).to eq('x') + traits = message[:traits] + expect(traits[:time]).to eq('2013-01-01T00:00:00.000Z') + expect(traits[:time_with_zone]).to eq('2013-01-01T00:00:00.000Z') + expect(traits[:date_time]).to eq('2013-01-01T00:00:00.000+00:00') + expect(traits[:date]).to eq('2013-01-01') + expect(traits[:nottime]).to eq('x') end end From 0388eea671bb706bea82f17725433bc783c11e30 Mon Sep 17 00:00:00 2001 From: Paul Kuruvilla Date: Tue, 21 Nov 2017 17:55:35 +0530 Subject: [PATCH 020/179] When testing client, force worker to not pick jobs When calling `enqueue` on the client, it automatically spawns a worker thread to consume items from the queue. This causes race conditions between the tests and worker picking items from `queue`. --- spec/segment/analytics/client_spec.rb | 29 +++++++++++++++------------ spec/spec_helper.rb | 7 +++++++ 2 files changed, 23 insertions(+), 13 deletions(-) diff --git a/spec/segment/analytics/client_spec.rb b/spec/segment/analytics/client_spec.rb index d39dc7a..89cde66 100644 --- a/spec/segment/analytics/client_spec.rb +++ b/spec/segment/analytics/client_spec.rb @@ -3,7 +3,12 @@ module Segment class Analytics describe Client do - let(:client) { Client.new :write_key => WRITE_KEY } + let(:client) do + Client.new(:write_key => WRITE_KEY).tap { |client| + # Ensure that worker doesn't consume items from the queue + client.instance_variable_set(:@worker, NoopWorker.new) + } + end let(:queue) { client.instance_variable_get :@queue } describe '#initialize' do @@ -158,10 +163,6 @@ class Analytics end describe '#group' do - after do - client.flush - end - it 'errors without group_id' do expect { client.group :user_id => 'foo' }.to raise_error(ArgumentError) end @@ -235,21 +236,23 @@ class Analytics end describe '#flush' do + let(:client_with_worker) { Client.new(:write_key => WRITE_KEY) } + it 'waits for the queue to finish on a flush' do - client.identify Queued::IDENTIFY - client.track Queued::TRACK - client.flush + client_with_worker.identify Queued::IDENTIFY + client_with_worker.track Queued::TRACK + client_with_worker.flush - expect(client.queued_messages).to eq(0) + expect(client_with_worker.queued_messages).to eq(0) end it 'completes when the process forks' do - client.identify Queued::IDENTIFY + client_with_worker.identify Queued::IDENTIFY Process.fork do - client.track Queued::TRACK - client.flush - expect(client.queued_messages).to eq(0) + client_with_worker.track Queued::TRACK + client_with_worker.flush + expect(client_with_worker.queued_messages).to eq(0) end Process.wait diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 2e06002..e67ca5d 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -77,6 +77,13 @@ module Requested end end +# A worker that doesn't consume jobs +class NoopWorker + def run + # Does nothing + end +end + # usage: # it "should return a result of 5" do # eventually(options: {timeout: 1}) { long_running_thing.result.should eq(5) } From 3c44ee9ffb7543c4779800e605877f10301266d7 Mon Sep 17 00:00:00 2001 From: Paul Kuruvilla Date: Wed, 29 Nov 2017 22:44:17 +0530 Subject: [PATCH 021/179] Add linter (#130) * Add rubocop (style linter), run as part of default rake task * Generate rubocop_todo.yml * Autocorrect simple cops * Use consistent style for indenting hashes * Auto-correct cops * Avoid redefining #stub via attr_accessor * Avoid assignment within condition * Avoid suppressing exception in spec_helper.rb * Auto-correct cops * Autocorrect symbol arrays * Auto-correct string literals * Auto-correct quotes within interpolation * Autocorrect raising exceptions * Avoid using semi-colon as a separator * Set target ruby version * Auto-correct Proc style * Specify 'verbose' style for Hash#has_key? * Allow parallel assignment * Allow 6 digits without underscores * Allow mutable constants This behaviour could've been relied on by users, not worth changing. * Avoid multi-line if modifier usage * Use parentheses for all method definitions * Allow 1.9 hash syntax in specs * Allow one-liners to be wrapped in conditionals * Allow % for formatting * Use each_with_object instead of inject * Allow DateTime in specs * Remove colon method call * Allow bracket symbol arrays * Disable doc checks * Allow all block delimiters in specs * Remove non-ascii quote in comment * Allow is_requesting? as an exception for predicate names * Disable BracesAroundHashParameters check * Move legit items from .rubucop_todo.yml to .rubocop.yml * Disable metrics check for specs --- .rubocop.yml | 86 +++++++++++++++++++++++++++ .rubocop_todo.yml | 40 +++++++++++++ Rakefile | 17 +++++- analytics-ruby.gemspec | 4 ++ lib/segment/analytics.rb | 4 +- lib/segment/analytics/client.rb | 41 +++++++------ lib/segment/analytics/logging.rb | 6 +- lib/segment/analytics/request.rb | 6 +- lib/segment/analytics/response.rb | 1 - lib/segment/analytics/utils.rb | 35 ++++++----- lib/segment/analytics/worker.rb | 2 +- spec/segment/analytics/client_spec.rb | 41 ++++++------- spec/segment/analytics/worker_spec.rb | 12 ++-- spec/spec_helper.rb | 8 +-- 14 files changed, 227 insertions(+), 76 deletions(-) create mode 100644 .rubocop.yml create mode 100644 .rubocop_todo.yml diff --git a/.rubocop.yml b/.rubocop.yml new file mode 100644 index 0000000..dc74787 --- /dev/null +++ b/.rubocop.yml @@ -0,0 +1,86 @@ +inherit_from: .rubocop_todo.yml + +AllCops: + # Rubocop doesn't support 1.9, so we'll use the minimum available + TargetRubyVersion: 2.1 + +Layout/IndentHash: + EnforcedStyle: consistent + +Metrics/AbcSize: + Exclude: + - "spec/**/*.rb" + +Metrics/BlockLength: + Exclude: + - "spec/**/*.rb" + +Metrics/ClassLength: + Exclude: + - "spec/**/*.rb" + +Metrics/CyclomaticComplexity: + Exclude: + - "spec/**/*.rb" + +Metrics/LineLength: + Exclude: + - "spec/**/*.rb" + +Metrics/MethodLength: + Exclude: + - "spec/**/*.rb" + +Metrics/PerceivedComplexity: + Exclude: + - "spec/**/*.rb" + +Naming/PredicateName: + NameWhitelist: + - is_requesting? # Can't be renamed, backwards compatibility + +Style/BlockDelimiters: + Exclude: + - 'spec/**/*' + +Style/BracesAroundHashParameters: + Enabled: false + +Style/DateTime: + Exclude: + - 'spec/**/*.rb' + +Style/Documentation: + Enabled: false + +Style/FormatString: + EnforcedStyle: percent + +# Allow one-liner functions to be wrapped in conditionals rather +# than forcing a guard clause +Style/GuardClause: + MinBodyLength: 2 + +Style/HashSyntax: + EnforcedStyle: hash_rockets + Exclude: + - 'spec/**/*.rb' + +Style/ModuleFunction: + Enabled: false + +Style/MutableConstant: + Enabled: false + +Style/NumericLiterals: + MinDigits: 6 + +Style/ParallelAssignment: + Enabled: false + +Style/PreferredHashMethods: + EnforcedStyle: verbose + +# Ruby 1.9 doesn't support percent-styled symbol arrays +Style/SymbolArray: + EnforcedStyle: brackets diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml new file mode 100644 index 0000000..8057bd2 --- /dev/null +++ b/.rubocop_todo.yml @@ -0,0 +1,40 @@ +# This configuration was generated by +# `rubocop --auto-gen-config` +# on 2017-11-23 15:34:00 +0530 using RuboCop version 0.51.0. +# The point is for the user to remove these configuration records +# one by one as the offenses are removed from the code base. +# Note that changes in the inspected code, or installation of new +# versions of RuboCop, may require this file to be generated again. + +# Offense count: 1 +Lint/RescueException: + Exclude: + - 'lib/segment/analytics/request.rb' + +# Offense count: 9 +Metrics/AbcSize: + Max: 32 + +# Offense count: 1 +# Configuration parameters: CountComments. +Metrics/ClassLength: + Max: 223 + +# Offense count: 1 +Metrics/CyclomaticComplexity: + Max: 8 + +# Offense count: 36 +# Configuration parameters: AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, IgnoredPatterns. +# URISchemes: http, https +Metrics/LineLength: + Max: 223 + +# Offense count: 9 +# Configuration parameters: CountComments. +Metrics/MethodLength: + Max: 29 + +# Offense count: 1 +Metrics/PerceivedComplexity: + Max: 8 diff --git a/Rakefile b/Rakefile index 8c4fe80..cc4f987 100644 --- a/Rakefile +++ b/Rakefile @@ -1,7 +1,22 @@ require 'rspec/core/rake_task' +default_tasks = [] + RSpec::Core::RakeTask.new(:spec) do |spec| spec.pattern = 'spec/**/*_spec.rb' end -task :default => :spec +default_tasks << :spec + +# Rubocop doesn't support < 2.1 +if RUBY_VERSION >= "2.1" + require 'rubocop/rake_task' + + RuboCop::RakeTask.new(:rubocop) do |task| + task.patterns = ['lib/**/*.rb','spec/**/*.rb',] + end + + default_tasks << :rubocop +end + +task :default => default_tasks diff --git a/analytics-ruby.gemspec b/analytics-ruby.gemspec index d00fe5a..0fc6467 100644 --- a/analytics-ruby.gemspec +++ b/analytics-ruby.gemspec @@ -22,4 +22,8 @@ Gem::Specification.new do |spec| spec.add_development_dependency 'rspec', '~> 3.0' spec.add_development_dependency 'tzinfo', '1.2.1' spec.add_development_dependency 'activesupport', '~> 4.1.11' + + if RUBY_VERSION >= "2.1" + spec.add_development_dependency 'rubocop', '~> 0.51.0' + end end diff --git a/lib/segment/analytics.rb b/lib/segment/analytics.rb index da0b400..0105c16 100644 --- a/lib/segment/analytics.rb +++ b/lib/segment/analytics.rb @@ -9,12 +9,12 @@ module Segment class Analytics - def initialize options = {} + def initialize(options = {}) Request.stub = options[:stub] if options.has_key?(:stub) @client = Segment::Analytics::Client.new options end - def method_missing message, *args, &block + def method_missing(message, *args, &block) if @client.respond_to? message @client.send message, *args, &block else diff --git a/lib/segment/analytics/client.rb b/lib/segment/analytics/client.rb index 138f819..c0f0d67 100644 --- a/lib/segment/analytics/client.rb +++ b/lib/segment/analytics/client.rb @@ -15,7 +15,7 @@ class Client # :write_key - String of your project's write_key # :max_queue_size - Fixnum of the max calls to remain queued (optional) # :on_error - Proc which handles error calls from the API - def initialize attrs = {} + def initialize(attrs = {}) symbolize_keys! attrs @queue = Queue.new @@ -53,7 +53,7 @@ def flush # :timestamp - Time of when the event occurred. (optional) # :user_id - String of the user id. # :message_id - String of the message id that uniquely identified a message across the API. (optional) - def track attrs + def track(attrs) symbolize_keys! attrs check_user_id! attrs @@ -66,10 +66,10 @@ def track attrs check_timestamp! timestamp if event.nil? || event.empty? - fail ArgumentError, 'Must supply event as a non-empty string' + raise ArgumentError, 'Must supply event as a non-empty string' end - fail ArgumentError, 'Properties must be a Hash' unless properties.is_a? Hash + raise ArgumentError, 'Properties must be a Hash' unless properties.is_a? Hash isoify_dates! properties add_context context @@ -99,7 +99,7 @@ def track attrs # :traits - Hash of user traits. (optional) # :user_id - String of the user id # :message_id - String of the message id that uniquely identified a message across the API. (optional) - def identify attrs + def identify(attrs) symbolize_keys! attrs check_user_id! attrs @@ -110,7 +110,7 @@ def identify attrs check_timestamp! timestamp - fail ArgumentError, 'Must supply traits as a hash' unless traits.is_a? Hash + raise ArgumentError, 'Must supply traits as a hash' unless traits.is_a? Hash isoify_dates! traits add_context context @@ -185,7 +185,7 @@ def group(attrs) context = attrs[:context] || {} message_id = attrs[:message_id].to_s if attrs[:message_id] - fail ArgumentError, '.traits must be a hash' unless traits.is_a? Hash + raise ArgumentError, '.traits must be a hash' unless traits.is_a? Hash isoify_dates! traits check_presence! group_id, 'group_id' @@ -228,7 +228,7 @@ def page(attrs) context = attrs[:context] || {} message_id = attrs[:message_id].to_s if attrs[:message_id] - fail ArgumentError, '.properties must be a hash' unless properties.is_a? Hash + raise ArgumentError, '.properties must be a hash' unless properties.is_a? Hash isoify_dates! properties check_timestamp! timestamp @@ -248,6 +248,7 @@ def page(attrs) :type => 'page' }) end + # public: Records a screen view (for a mobile app) # # attrs - Hash @@ -270,7 +271,7 @@ def screen(attrs) context = attrs[:context] || {} message_id = attrs[:message_id].to_s if attrs[:message_id] - fail ArgumentError, '.properties must be a hash' unless properties.is_a? Hash + raise ArgumentError, '.properties must be a hash' unless properties.is_a? Hash isoify_dates! properties check_timestamp! timestamp @@ -306,11 +307,15 @@ def queued_messages def enqueue(action) # add our request id for tracing purposes action[:messageId] ||= uid - unless queue_full = @queue.length >= @max_queue_size + + if @queue.length < @max_queue_size ensure_worker_running @queue << action + + true + else + false # Queue is full end - !queue_full end # private: Ensures that a string is non-empty @@ -320,7 +325,7 @@ def enqueue(action) # def check_presence!(obj, name) if obj.nil? || (obj.is_a?(String) && obj.empty?) - fail ArgumentError, "#{name} must be given" + raise ArgumentError, "#{name} must be given" end end @@ -328,20 +333,20 @@ def check_presence!(obj, name) # # context - Hash of call context def add_context(context) - context[:library] = { :name => "analytics-ruby", :version => Segment::Analytics::VERSION.to_s } + context[:library] = { :name => 'analytics-ruby', :version => Segment::Analytics::VERSION.to_s } end # private: Checks that the write_key is properly initialized def check_write_key! - fail ArgumentError, 'Write key must be initialized' if @write_key.nil? + raise ArgumentError, 'Write key must be initialized' if @write_key.nil? end # private: Checks the timstamp option to make sure it is a Time. def check_timestamp!(timestamp) - fail ArgumentError, 'Timestamp must be a Time' unless timestamp.is_a? Time + raise ArgumentError, 'Timestamp must be a Time' unless timestamp.is_a? Time end - def event attrs + def event(attrs) symbolize_keys! attrs { @@ -354,8 +359,8 @@ def event attrs } end - def check_user_id! attrs - fail ArgumentError, 'Must supply either user_id or anonymous_id' unless attrs[:user_id] || attrs[:anonymous_id] + def check_user_id!(attrs) + raise ArgumentError, 'Must supply either user_id or anonymous_id' unless attrs[:user_id] || attrs[:anonymous_id] end def ensure_worker_running diff --git a/lib/segment/analytics/logging.rb b/lib/segment/analytics/logging.rb index e7f5229..20b1a7f 100644 --- a/lib/segment/analytics/logging.rb +++ b/lib/segment/analytics/logging.rb @@ -14,12 +14,10 @@ def logger end end - def logger= logger - @logger = logger - end + attr_writer :logger end - def self.included base + def self.included(base) class << base def logger Logging.logger diff --git a/lib/segment/analytics/request.rb b/lib/segment/analytics/request.rb index 99df829..0a490f1 100644 --- a/lib/segment/analytics/request.rb +++ b/lib/segment/analytics/request.rb @@ -53,10 +53,10 @@ def post(write_key, batch) res = @http.request(request, payload) status = res.code.to_i body = JSON.parse(res.body) - error = body["error"] + error = body['error'] end rescue Exception => e - unless (remaining_retries -=1).zero? + unless (remaining_retries -= 1).zero? sleep(backoff) retry end @@ -71,7 +71,7 @@ def post(write_key, batch) end class << self - attr_accessor :stub + attr_writer :stub def stub @stub || ENV['STUB'] diff --git a/lib/segment/analytics/response.rb b/lib/segment/analytics/response.rb index ebee226..7306ac0 100644 --- a/lib/segment/analytics/response.rb +++ b/lib/segment/analytics/response.rb @@ -13,4 +13,3 @@ def initialize(status = 200, error = nil) end end end - diff --git a/lib/segment/analytics/utils.rb b/lib/segment/analytics/utils.rb index db96405..60bfa52 100644 --- a/lib/segment/analytics/utils.rb +++ b/lib/segment/analytics/utils.rb @@ -8,7 +8,9 @@ module Utils # public: Return a new hash with keys converted from strings to symbols # def symbolize_keys(hash) - hash.inject({}) { |memo, (k,v)| memo[k.to_sym] = v; memo } + hash.each_with_object({}) do |(k, v), memo| + memo[k.to_sym] = v + end end # public: Convert hash keys from strings to symbols in place @@ -20,17 +22,18 @@ def symbolize_keys!(hash) # public: Return a new hash with keys as strings # def stringify_keys(hash) - hash.inject({}) { |memo, (k,v)| memo[k.to_s] = v; memo } + hash.each_with_object({}) do |(k, v), memo| + memo[k.to_s] = v + end end # public: Returns a new hash with all the date values in the into iso8601 # strings # def isoify_dates(hash) - hash.inject({}) { |memo, (k, v)| + hash.each_with_object({}) do |(k, v), memo| memo[k] = datetime_in_iso8601(v) - memo - } + end end # public: Converts all the date values in the into iso8601 strings in place @@ -42,18 +45,18 @@ def isoify_dates!(hash) # public: Returns a uid string # def uid - arr = SecureRandom.random_bytes(16).unpack("NnnnnN") + arr = SecureRandom.random_bytes(16).unpack('NnnnnN') arr[2] = (arr[2] & 0x0fff) | 0x4000 arr[3] = (arr[3] & 0x3fff) | 0x8000 - "%08x-%04x-%04x-%04x-%04x%08x" % arr + '%08x-%04x-%04x-%04x-%04x%08x' % arr end - def datetime_in_iso8601 datetime + def datetime_in_iso8601(datetime) case datetime when Time - time_in_iso8601 datetime + time_in_iso8601 datetime when DateTime - time_in_iso8601 datetime.to_time + time_in_iso8601 datetime.to_time when Date date_in_iso8601 datetime else @@ -61,19 +64,19 @@ def datetime_in_iso8601 datetime end end - def time_in_iso8601 time, fraction_digits = 3 + def time_in_iso8601(time, fraction_digits = 3) fraction = if fraction_digits > 0 - (".%06i" % time.usec)[0, fraction_digits + 1] + ('.%06i' % time.usec)[0, fraction_digits + 1] end - "#{time.strftime("%Y-%m-%dT%H:%M:%S")}#{fraction}#{formatted_offset(time, true, 'Z')}" + "#{time.strftime('%Y-%m-%dT%H:%M:%S')}#{fraction}#{formatted_offset(time, true, 'Z')}" end - def date_in_iso8601 date - date.strftime("%F") + def date_in_iso8601(date) + date.strftime('%F') end - def formatted_offset time, colon = true, alternate_utc_string = nil + def formatted_offset(time, colon = true, alternate_utc_string = nil) time.utc? && alternate_utc_string || seconds_to_utc_offset(time.utc_offset, colon) end diff --git a/lib/segment/analytics/worker.rb b/lib/segment/analytics/worker.rb index 9288c85..f983d72 100644 --- a/lib/segment/analytics/worker.rb +++ b/lib/segment/analytics/worker.rb @@ -25,7 +25,7 @@ def initialize(queue, write_key, options = {}) @queue = queue @write_key = write_key @batch_size = options[:batch_size] || Queue::BATCH_SIZE - @on_error = options[:on_error] || Proc.new { |status, error| } + @on_error = options[:on_error] || proc { |status, error| } @batch = [] @lock = Mutex.new end diff --git a/spec/segment/analytics/client_spec.rb b/spec/segment/analytics/client_spec.rb index 89cde66..ebc9a0a 100644 --- a/spec/segment/analytics/client_spec.rb +++ b/spec/segment/analytics/client_spec.rb @@ -43,13 +43,13 @@ class Analytics client.track({ :user_id => 'user', :event => 'Event', - :properties => [1,2,3] + :properties => [1, 2, 3] }) }.to raise_error(ArgumentError) end it 'uses the timestamp given' do - time = Time.parse("1990-07-16 13:30:00.123 UTC") + time = Time.parse('1990-07-16 13:30:00.123 UTC') client.track({ :event => 'testing the timestamp', @@ -83,8 +83,8 @@ class Analytics :properties => { :time => Time.utc(2013), :time_with_zone => Time.zone.parse('2013-01-01'), - :date_time => DateTime.new(2013,1,1), - :date => Date.new(2013,1,1), + :date_time => DateTime.new(2013, 1, 1), + :date => Date.new(2013, 1, 1), :nottime => 'x' } }) @@ -99,7 +99,6 @@ class Analytics end end - describe '#identify' do it 'errors without any user id' do expect { client.identify({}) }.to raise_error(ArgumentError) @@ -125,8 +124,8 @@ class Analytics :traits => { :time => Time.utc(2013), :time_with_zone => Time.zone.parse('2013-01-01'), - :date_time => DateTime.new(2013,1,1), - :date => Date.new(2013,1,1), + :date_time => DateTime.new(2013, 1, 1), + :date => Date.new(2013, 1, 1), :nottime => 'x' } }) @@ -186,8 +185,8 @@ class Analytics :traits => { :time => Time.utc(2013), :time_with_zone => Time.zone.parse('2013-01-01'), - :date_time => DateTime.new(2013,1,1), - :date => Date.new(2013,1,1), + :date_time => DateTime.new(2013, 1, 1), + :date => Date.new(2013, 1, 1), :nottime => 'x' } }) @@ -246,23 +245,25 @@ class Analytics expect(client_with_worker.queued_messages).to eq(0) end - it 'completes when the process forks' do - client_with_worker.identify Queued::IDENTIFY + unless defined? JRUBY_VERSION + it 'completes when the process forks' do + client_with_worker.identify Queued::IDENTIFY - Process.fork do - client_with_worker.track Queued::TRACK - client_with_worker.flush - expect(client_with_worker.queued_messages).to eq(0) - end + Process.fork do + client_with_worker.track Queued::TRACK + client_with_worker.flush + expect(client_with_worker.queued_messages).to eq(0) + end - Process.wait - end unless defined? JRUBY_VERSION + Process.wait + end + end end context 'common' do check_property = proc { |msg, k, v| msg[k] && msg[k] == v } - let(:data) { { :user_id => 1, :group_id => 2, :previous_id => 3, :anonymous_id => 4, :message_id => 5, :event => "coco barked", :name => "coco" } } + let(:data) { { :user_id => 1, :group_id => 2, :previous_id => 3, :anonymous_id => 4, :message_id => 5, :event => 'coco barked', :name => 'coco' } } it 'does not convert ids given as fixnums to strings' do [:track, :screen, :page, :identify].each do |s| @@ -305,7 +306,7 @@ class Analytics it 'sends integrations' do [:track, :screen, :page, :group, :identify, :alias].each do |s| - client.send s, :integrations => { :All => true, :Salesforce => false }, :user_id => 1, :group_id => 2, :previous_id => 3, :anonymous_id => 4, :event => "coco barked", :name => "coco" + client.send s, :integrations => { :All => true, :Salesforce => false }, :user_id => 1, :group_id => 2, :previous_id => 3, :anonymous_id => 4, :event => 'coco barked', :name => 'coco' message = queue.pop(true) expect(message[:integrations][:All]).to eq(true) expect(message[:integrations][:Salesforce]).to eq(false) diff --git a/spec/segment/analytics/worker_spec.rb b/spec/segment/analytics/worker_spec.rb index 1accd22..3abd4d8 100644 --- a/spec/segment/analytics/worker_spec.rb +++ b/spec/segment/analytics/worker_spec.rb @@ -3,7 +3,7 @@ module Segment class Analytics describe Worker do - describe "#init" do + describe '#init' do it 'accepts string keys' do queue = Queue.new worker = Segment::Analytics::Worker.new(queue, 'secret', 'batch_size' => 100) @@ -36,10 +36,10 @@ class Analytics end it 'executes the error handler, before the request phase ends, if the request is invalid' do - Segment::Analytics::Request.any_instance.stub(:post).and_return(Segment::Analytics::Response.new(400, "Some error")) + Segment::Analytics::Request.any_instance.stub(:post).and_return(Segment::Analytics::Response.new(400, 'Some error')) status = error = nil - on_error = Proc.new do |yielded_status, yielded_error| + on_error = proc do |yielded_status, yielded_error| sleep 0.2 # Make this take longer than thread spin-up (below) status, error = yielded_status, yielded_error end @@ -48,12 +48,12 @@ class Analytics queue << {} worker = Segment::Analytics::Worker.new queue, 'secret', :on_error => on_error - # This is to ensure that Client#flush doesn’t finish before calling the error handler. + # This is to ensure that Client#flush doesn't finish before calling the error handler. Thread.new { worker.run } sleep 0.1 # First give thread time to spin-up. sleep 0.01 while worker.is_requesting? - Segment::Analytics::Request::any_instance.unstub(:post) + Segment::Analytics::Request.any_instance.unstub(:post) expect(queue).to be_empty expect(status).to eq(400) @@ -61,7 +61,7 @@ class Analytics end it 'does not call on_error if the request is good' do - on_error = Proc.new do |status, error| + on_error = proc do |status, error| puts "#{status}, #{error}" end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index e67ca5d..33cf320 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -18,7 +18,7 @@ class Analytics } } - IDENTIFY = { + IDENTIFY = { :traits => { :likes_animals => true, :instrument => 'Guitar', @@ -97,11 +97,11 @@ def eventually(options = {}) loop do begin yield + return rescue RSpec::Expectations::ExpectationNotMetError => error + raise error if Time.now >= time_limit + sleep interval end - return if error.nil? - raise error if Time.now >= time_limit - sleep interval end end end From 940345fd08e90a442b99084f4c71f402eff47d43 Mon Sep 17 00:00:00 2001 From: Paul Kuruvilla Date: Wed, 29 Nov 2017 22:52:07 +0530 Subject: [PATCH 022/179] Upload coverage stats to codecov.io --- analytics-ruby.gemspec | 2 ++ spec/spec_helper.rb | 6 ++++++ 2 files changed, 8 insertions(+) diff --git a/analytics-ruby.gemspec b/analytics-ruby.gemspec index 0fc6467..ff9636f 100644 --- a/analytics-ruby.gemspec +++ b/analytics-ruby.gemspec @@ -26,4 +26,6 @@ Gem::Specification.new do |spec| if RUBY_VERSION >= "2.1" spec.add_development_dependency 'rubocop', '~> 0.51.0' end + + spec.add_development_dependency 'codecov', '~> 0.1.4' end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 33cf320..8d8bdb8 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,3 +1,9 @@ +# https://github.com/codecov/codecov-ruby#usage +require 'simplecov' +SimpleCov.start +require 'codecov' +SimpleCov.formatter = SimpleCov::Formatter::Codecov + require 'segment/analytics' require 'active_support/time' From 1dd39ef885e0a40f804c35b47070bc431008528d Mon Sep 17 00:00:00 2001 From: Paul Kuruvilla Date: Fri, 1 Dec 2017 12:31:26 +0530 Subject: [PATCH 023/179] Handle HTTP status code failures (#132) * Separate request construction logic from retries * Trim lines in request_spec.rb to < 80 chars * Add function to retry a block without exceptions raised * Retry requests on HTTP status code errors --- lib/segment/analytics/request.rb | 104 +++++++++++++++++------- lib/segment/analytics/worker.rb | 2 +- spec/segment/analytics/request_spec.rb | 106 ++++++++++++++++++------- 3 files changed, 154 insertions(+), 58 deletions(-) diff --git a/lib/segment/analytics/request.rb b/lib/segment/analytics/request.rb index 0a490f1..e2d8073 100644 --- a/lib/segment/analytics/request.rb +++ b/lib/segment/analytics/request.rb @@ -36,38 +36,86 @@ def initialize(options = {}) # # returns - Response of the status and error if it exists def post(write_key, batch) - status, error = nil, nil - remaining_retries = @retries - backoff = @backoff - headers = { 'Content-Type' => 'application/json', 'accept' => 'application/json' } + last_response, exception = retry_with_backoff(@retries, @backoff) do + status_code, body = send_request(write_key, batch) + error = JSON.parse(body)['error'] + should_retry = should_retry_request?(status_code, body) + + [Response.new(status_code, error), should_retry] + end + + if exception + logger.error(exception.message) + exception.backtrace.each { |line| logger.error(line) } + Response.new(-1, "Connection error: #{exception}") + else + last_response + end + end + + private + + def should_retry_request?(status_code, body) + if status_code >= 500 + true # Server error + elsif status_code == 429 + true # Rate limited + elsif status_code >= 400 + logger.error(body) + false # Client error. Do not retry, but log + else + false + end + end + + # Takes a block that returns [result, should_retry]. + # + # Retries upto `retries_remaining` times, if `should_retry` is false or + # an exception is raised. + # + # Returns [last_result, raised_exception] + def retry_with_backoff(retries_remaining, backoff, &block) + result, caught_exception = nil + should_retry = false + begin - payload = JSON.generate :sentAt => datetime_in_iso8601(Time.new), :batch => batch - request = Net::HTTP::Post.new(@path, headers) - request.basic_auth write_key, nil - - if self.class.stub - status = 200 - error = nil - logger.debug "stubbed request to #{@path}: write key = #{write_key}, payload = #{payload}" - else - res = @http.request(request, payload) - status = res.code.to_i - body = JSON.parse(res.body) - error = body['error'] - end + result, should_retry = yield + return [result, nil] unless should_retry rescue Exception => e - unless (remaining_retries -= 1).zero? - sleep(backoff) - retry - end - - logger.error e.message - e.backtrace.each { |line| logger.error line } - status = -1 - error = "Connection error: #{e}" + should_retry = true + caught_exception = e end - Response.new status, error + if should_retry && (retries_remaining > 1) + sleep(backoff) + retry_with_backoff(retries_remaining - 1, backoff, &block) + else + [result, caught_exception] + end + end + + # Sends a request for the batch, returns [status_code, body] + def send_request(write_key, batch) + headers = { + 'Content-Type' => 'application/json', + 'accept' => 'application/json' + } + payload = JSON.generate( + :sentAt => datetime_in_iso8601(Time.now), + :batch => batch + ) + request = Net::HTTP::Post.new(@path, headers) + request.basic_auth(write_key, nil) + + if self.class.stub + logger.debug "stubbed request to #{@path}: " \ + "write key = #{write_key}, batch = JSON.generate(#{batch})" + + [200, '{}'] + else + response = @http.request(request, payload) + [response.code.to_i, response.body] + end end class << self diff --git a/lib/segment/analytics/worker.rb b/lib/segment/analytics/worker.rb index f983d72..f619a1b 100644 --- a/lib/segment/analytics/worker.rb +++ b/lib/segment/analytics/worker.rb @@ -44,7 +44,7 @@ def run res = Request.new.post @write_key, @batch - @on_error.call res.status, res.error unless res.status == 200 + @on_error.call(res.status, res.error) unless res.status == 200 @lock.synchronize { @batch.clear } end diff --git a/spec/segment/analytics/request_spec.rb b/spec/segment/analytics/request_spec.rb index 40bdc3d..d556eb9 100644 --- a/spec/segment/analytics/request_spec.rb +++ b/spec/segment/analytics/request_spec.rb @@ -37,19 +37,25 @@ class Analytics context 'no options are set' do it 'sets a default path' do - expect(subject.instance_variable_get(:@path)).to eq(described_class::PATH) + path = subject.instance_variable_get(:@path) + expect(path).to eq(described_class::PATH) end it 'sets a default retries' do - expect(subject.instance_variable_get(:@retries)).to eq(described_class::RETRIES) + retries = subject.instance_variable_get(:@retries) + expect(retries).to eq(described_class::RETRIES) end it 'sets a default backoff' do - expect(subject.instance_variable_get(:@backoff)).to eq(described_class::BACKOFF) + backoff = subject.instance_variable_get(:@backoff) + expect(backoff).to eq(described_class::BACKOFF) end it 'initializes a new Net::HTTP with default host and port' do - expect(Net::HTTP).to receive(:new).with(described_class::HOST, described_class::PORT) + expect(Net::HTTP).to receive(:new).with( + described_class::HOST, + described_class::PORT + ) described_class.new end end @@ -92,7 +98,9 @@ class Analytics end describe '#post' do - let(:response) { Net::HTTPResponse.new(http_version, status_code, response_body) } + let(:response) { + Net::HTTPResponse.new(http_version, status_code, response_body) + } let(:http_version) { 1.1 } let(:status_code) { 200 } let(:response_body) { {}.to_json } @@ -100,19 +108,28 @@ class Analytics let(:batch) { [] } before do - allow(subject.instance_variable_get(:@http)).to receive(:request) { response } + http = subject.instance_variable_get(:@http) + allow(http).to receive(:request) { response } allow(response).to receive(:body) { response_body } end it 'initalizes a new Net::HTTP::Post with path and default headers' do path = subject.instance_variable_get(:@path) - default_headers = { 'Content-Type' => 'application/json', 'accept' => 'application/json' } - expect(Net::HTTP::Post).to receive(:new).with(path, default_headers).and_call_original + default_headers = { + 'Content-Type' => 'application/json', + 'accept' => 'application/json' + } + expect(Net::HTTP::Post).to receive(:new).with( + path, default_headers + ).and_call_original + subject.post(write_key, batch) end it 'adds basic auth to the Net::HTTP::Post' do - expect_any_instance_of(Net::HTTP::Post).to receive(:basic_auth).with(write_key, nil) + expect_any_instance_of(Net::HTTP::Post).to receive(:basic_auth) + .with(write_key, nil) + subject.post(write_key, batch) end @@ -136,6 +153,38 @@ class Analytics end context 'a real request' do + RSpec.shared_examples('retried request') do |status_code, body| + let(:status_code) { status_code } + let(:body) { body } + let(:retries) { 4 } + let(:backoff) { 1 } + subject { described_class.new(retries: retries, backoff: backoff) } + + it 'retries the request' do + expect(subject) + .to receive(:sleep) + .exactly(retries - 1).times + .with(backoff) + .and_return(nil) + subject.post(write_key, batch) + end + end + + RSpec.shared_examples('non-retried request') do |status_code, body| + let(:status_code) { status_code } + let(:body) { body } + let(:retries) { 4 } + let(:backoff) { 1 } + subject { described_class.new(retries: retries, backoff: backoff) } + + it 'does not retry the request' do + expect(subject) + .to receive(:sleep) + .never + subject.post(write_key, batch) + end + end + context 'request is successful' do let(:status_code) { 201 } it 'returns a response code' do @@ -156,33 +205,32 @@ class Analytics end end - context 'request or parsing of response results in an exception' do - let(:response_body) { 'Malformed JSON ---' } + context 'a request returns a failure status code' do + # Server errors must be retried + it_behaves_like('retried request', 500, '{}') + it_behaves_like('retried request', 503, '{}') - let(:backoff) { 0 } + # All 4xx errors other than 429 (rate limited) must be retried + it_behaves_like('retried request', 429, '{}') + it_behaves_like('non-retried request', 404, '{}') + it_behaves_like('non-retried request', 400, '{}') + end - subject { described_class.new(retries: retries, backoff: backoff) } + context 'request or parsing of response results in an exception' do + let(:response_body) { 'Malformed JSON ---' } - context 'remaining retries is > 1' do - let(:retries) { 2 } + subject { described_class.new(backoff: 0) } - it 'sleeps' do - expect(subject).to receive(:sleep).exactly(retries - 1).times - subject.post(write_key, batch) - end + it 'returns a -1 for status' do + expect(subject.post(write_key, batch).status).to eq(-1) end - context 'remaining retries is 1' do - let(:retries) { 1 } - - it 'returns a -1 for status' do - expect(subject.post(write_key, batch).status).to eq(-1) - end - - it 'has a connection error' do - expect(subject.post(write_key, batch).error).to match(/Connection error/) - end + it 'has a connection error' do + error = subject.post(write_key, batch).error + expect(error).to match(/Connection error/) end + + it_behaves_like('retried request', 200, 'Malformed JSON ---') end end end From d75a19c2c68ac54381c094353ce7dc7b922a5b2d Mon Sep 17 00:00:00 2001 From: Gopal Patel Date: Sun, 28 Feb 2016 21:54:20 -0800 Subject: [PATCH 024/179] Add analytics-ruby.rb for easier Gemfile use --- README.md | 4 ++-- lib/analytics-ruby.rb | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) create mode 100644 lib/analytics-ruby.rb diff --git a/README.md b/README.md index c7c07fd..e611131 100644 --- a/README.md +++ b/README.md @@ -9,11 +9,11 @@ analytics-ruby is a ruby client for [Segment](https://segment.com) Into Gemfile from rubygems.org: ```ruby -gem 'analytics-ruby', require: "segment" +gem 'analytics-ruby' ``` Into environment gems from rubygems.org: -```ruby +``` gem install 'analytics-ruby' ``` diff --git a/lib/analytics-ruby.rb b/lib/analytics-ruby.rb new file mode 100644 index 0000000..801ad01 --- /dev/null +++ b/lib/analytics-ruby.rb @@ -0,0 +1 @@ +require 'segment' From 9c3104781a6b6566b4fa32566243e95d04cd13c8 Mon Sep 17 00:00:00 2001 From: Paul Kuruvilla Date: Mon, 4 Dec 2017 12:40:20 +0530 Subject: [PATCH 025/179] Add file naming exception to rubocop.yml --- .rubocop.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.rubocop.yml b/.rubocop.yml index dc74787..d9751fd 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -35,6 +35,10 @@ Metrics/PerceivedComplexity: Exclude: - "spec/**/*.rb" +Naming/FileName: + Exclude: + - lib/analytics-ruby.rb # Gem name, added for easier Gemfile usage + Naming/PredicateName: NameWhitelist: - is_requesting? # Can't be renamed, backwards compatibility From 954a2c91dd8b893a786fdf55f5db500846d14f7c Mon Sep 17 00:00:00 2001 From: Paul Kuruvilla Date: Mon, 4 Dec 2017 12:33:50 +0530 Subject: [PATCH 026/179] Use HEADERS from defaults.rb --- lib/segment/analytics/defaults.rb | 3 ++- lib/segment/analytics/request.rb | 8 ++------ spec/segment/analytics/request_spec.rb | 2 +- 3 files changed, 5 insertions(+), 8 deletions(-) diff --git a/lib/segment/analytics/defaults.rb b/lib/segment/analytics/defaults.rb index aeb933e..b675e90 100644 --- a/lib/segment/analytics/defaults.rb +++ b/lib/segment/analytics/defaults.rb @@ -6,7 +6,8 @@ module Request PORT = 443 PATH = '/v1/import' SSL = true - HEADERS = { :accept => 'application/json' } + HEADERS = { 'Accept' => 'application/json', + 'Content-Type' => 'application/json' } RETRIES = 4 BACKOFF = 30.0 end diff --git a/lib/segment/analytics/request.rb b/lib/segment/analytics/request.rb index e2d8073..74df4d0 100644 --- a/lib/segment/analytics/request.rb +++ b/lib/segment/analytics/request.rb @@ -19,7 +19,7 @@ def initialize(options = {}) options[:host] ||= HOST options[:port] ||= PORT options[:ssl] ||= SSL - options[:headers] ||= HEADERS + @headers = options[:headers] || HEADERS @path = options[:path] || PATH @retries = options[:retries] || RETRIES @backoff = options[:backoff] || BACKOFF @@ -96,15 +96,11 @@ def retry_with_backoff(retries_remaining, backoff, &block) # Sends a request for the batch, returns [status_code, body] def send_request(write_key, batch) - headers = { - 'Content-Type' => 'application/json', - 'accept' => 'application/json' - } payload = JSON.generate( :sentAt => datetime_in_iso8601(Time.now), :batch => batch ) - request = Net::HTTP::Post.new(@path, headers) + request = Net::HTTP::Post.new(@path, @headers) request.basic_auth(write_key, nil) if self.class.stub diff --git a/spec/segment/analytics/request_spec.rb b/spec/segment/analytics/request_spec.rb index d556eb9..1695b5f 100644 --- a/spec/segment/analytics/request_spec.rb +++ b/spec/segment/analytics/request_spec.rb @@ -117,7 +117,7 @@ class Analytics path = subject.instance_variable_get(:@path) default_headers = { 'Content-Type' => 'application/json', - 'accept' => 'application/json' + 'Accept' => 'application/json' } expect(Net::HTTP::Post).to receive(:new).with( path, default_headers From bd084e6af945c47c0047a5626dde232d684b18ba Mon Sep 17 00:00:00 2001 From: Paul Kuruvilla Date: Mon, 4 Dec 2017 14:44:15 +0530 Subject: [PATCH 027/179] Add exponential backoff --- lib/segment/analytics/backoff_policy.rb | 49 ++++++++++ lib/segment/analytics/defaults.rb | 8 +- lib/segment/analytics/request.rb | 15 +-- spec/segment/analytics/backoff_policy_spec.rb | 92 +++++++++++++++++++ spec/segment/analytics/request_spec.rb | 26 +++--- spec/spec_helper.rb | 12 +++ 6 files changed, 184 insertions(+), 18 deletions(-) create mode 100644 lib/segment/analytics/backoff_policy.rb create mode 100644 spec/segment/analytics/backoff_policy_spec.rb diff --git a/lib/segment/analytics/backoff_policy.rb b/lib/segment/analytics/backoff_policy.rb new file mode 100644 index 0000000..394a2c1 --- /dev/null +++ b/lib/segment/analytics/backoff_policy.rb @@ -0,0 +1,49 @@ +require 'segment/analytics/defaults' + +module Segment + class Analytics + class BackoffPolicy + include Segment::Analytics::Defaults::BackoffPolicy + + # @param [Hash] opts + # @option opts [Numeric] :min_timeout_ms The minimum backoff timeout + # @option opts [Numeric] :max_timeout_ms The maximum backoff timeout + # @option opts [Numeric] :multiplier The value to multiply the current + # interval with for each retry attempt + # @option opts [Numeric] :randomization_factor The randomization factor + # to use to create a range around the retry interval + def initialize(opts = {}) + @min_timeout_ms = opts[:min_timeout_ms] || MIN_TIMEOUT_MS + @max_timeout_ms = opts[:max_timeout_ms] || MAX_TIMEOUT_MS + @multiplier = opts[:multiplier] || MULTIPLIER + @randomization_factor = opts[:randomization_factor] || RANDOMIZATION_FACTOR + + @attempts = 0 + end + + # @return [Numeric] the next backoff interval, in milliseconds. + def next_interval + interval = @min_timeout_ms * (@multiplier**@attempts) + interval = add_jitter(interval, @randomization_factor) + + @attempts += 1 + + [interval, @max_timeout_ms].min + end + + private + + def add_jitter(base, randomization_factor) + random_number = rand + max_deviation = base * randomization_factor + deviation = random_number * max_deviation + + if random_number < 0.5 + base - deviation + else + base + deviation + end + end + end + end +end diff --git a/lib/segment/analytics/defaults.rb b/lib/segment/analytics/defaults.rb index b675e90..f5a7b18 100644 --- a/lib/segment/analytics/defaults.rb +++ b/lib/segment/analytics/defaults.rb @@ -9,13 +9,19 @@ module Request HEADERS = { 'Accept' => 'application/json', 'Content-Type' => 'application/json' } RETRIES = 4 - BACKOFF = 30.0 end module Queue BATCH_SIZE = 100 MAX_SIZE = 10000 end + + module BackoffPolicy + MIN_TIMEOUT_MS = 100 + MAX_TIMEOUT_MS = 10000 + MULTIPLIER = 1.5 + RANDOMIZATION_FACTOR = 0.5 + end end end end diff --git a/lib/segment/analytics/request.rb b/lib/segment/analytics/request.rb index 74df4d0..026950a 100644 --- a/lib/segment/analytics/request.rb +++ b/lib/segment/analytics/request.rb @@ -2,6 +2,7 @@ require 'segment/analytics/utils' require 'segment/analytics/response' require 'segment/analytics/logging' +require 'segment/analytics/backoff_policy' require 'net/http' require 'net/https' require 'json' @@ -22,7 +23,8 @@ def initialize(options = {}) @headers = options[:headers] || HEADERS @path = options[:path] || PATH @retries = options[:retries] || RETRIES - @backoff = options[:backoff] || BACKOFF + @backoff_policy = + options[:backoff_policy] || Segment::Analytics::BackoffPolicy.new http = Net::HTTP.new(options[:host], options[:port]) http.use_ssl = options[:ssl] @@ -36,7 +38,7 @@ def initialize(options = {}) # # returns - Response of the status and error if it exists def post(write_key, batch) - last_response, exception = retry_with_backoff(@retries, @backoff) do + last_response, exception = retry_with_backoff(@retries) do status_code, body = send_request(write_key, batch) error = JSON.parse(body)['error'] should_retry = should_retry_request?(status_code, body) @@ -71,10 +73,11 @@ def should_retry_request?(status_code, body) # Takes a block that returns [result, should_retry]. # # Retries upto `retries_remaining` times, if `should_retry` is false or - # an exception is raised. + # an exception is raised. `@backoff_policy` is used to determine the + # duration to sleep between attempts # # Returns [last_result, raised_exception] - def retry_with_backoff(retries_remaining, backoff, &block) + def retry_with_backoff(retries_remaining, &block) result, caught_exception = nil should_retry = false @@ -87,8 +90,8 @@ def retry_with_backoff(retries_remaining, backoff, &block) end if should_retry && (retries_remaining > 1) - sleep(backoff) - retry_with_backoff(retries_remaining - 1, backoff, &block) + sleep(@backoff_policy.next_interval.to_f / 1000) + retry_with_backoff(retries_remaining - 1, &block) else [result, caught_exception] end diff --git a/spec/segment/analytics/backoff_policy_spec.rb b/spec/segment/analytics/backoff_policy_spec.rb new file mode 100644 index 0000000..75dc9fc --- /dev/null +++ b/spec/segment/analytics/backoff_policy_spec.rb @@ -0,0 +1,92 @@ +require 'spec_helper' + +module Segment + class Analytics + describe BackoffPolicy do + describe '#initialize' do + context 'no options are given' do + it 'sets default min_timeout_ms' do + actual = subject.instance_variable_get(:@min_timeout_ms) + expect(actual).to eq(described_class::MIN_TIMEOUT_MS) + end + + it 'sets default max_timeout_ms' do + actual = subject.instance_variable_get(:@max_timeout_ms) + expect(actual).to eq(described_class::MAX_TIMEOUT_MS) + end + + it 'sets default multiplier' do + actual = subject.instance_variable_get(:@multiplier) + expect(actual).to eq(described_class::MULTIPLIER) + end + + it 'sets default randomization factor' do + actual = subject.instance_variable_get(:@randomization_factor) + expect(actual).to eq(described_class::RANDOMIZATION_FACTOR) + end + end + + context 'options are given' do + let(:min_timeout_ms) { 1234 } + let(:max_timeout_ms) { 5678 } + let(:multiplier) { 24 } + let(:randomization_factor) { 0.4 } + + let(:options) do + { + min_timeout_ms: min_timeout_ms, + max_timeout_ms: max_timeout_ms, + multiplier: multiplier, + randomization_factor: randomization_factor + } + end + + subject { described_class.new(options) } + + it 'sets passed in min_timeout_ms' do + actual = subject.instance_variable_get(:@min_timeout_ms) + expect(actual).to eq(min_timeout_ms) + end + + it 'sets passed in max_timeout_ms' do + actual = subject.instance_variable_get(:@max_timeout_ms) + expect(actual).to eq(max_timeout_ms) + end + + it 'sets passed in multiplier' do + actual = subject.instance_variable_get(:@multiplier) + expect(actual).to eq(multiplier) + end + + it 'sets passed in randomization_factor' do + actual = subject.instance_variable_get(:@randomization_factor) + expect(actual).to eq(randomization_factor) + end + end + end + + describe '#next_interval' do + subject { + described_class.new( + min_timeout_ms: 1000, + max_timeout_ms: 10000, + multiplier: 2, + randomization_factor: 0.5 + ) + } + + it 'returns exponentially increasing durations' do + expect(subject.next_interval).to be_within(500).of(1000) + expect(subject.next_interval).to be_within(1000).of(2000) + expect(subject.next_interval).to be_within(2000).of(4000) + expect(subject.next_interval).to be_within(4000).of(8000) + end + + it 'caps maximum duration at max_timeout_secs' do + 10.times { subject.next_interval } + expect(subject.next_interval).to eq(10000) + end + end + end + end +end diff --git a/spec/segment/analytics/request_spec.rb b/spec/segment/analytics/request_spec.rb index 1695b5f..babc928 100644 --- a/spec/segment/analytics/request_spec.rb +++ b/spec/segment/analytics/request_spec.rb @@ -46,9 +46,9 @@ class Analytics expect(retries).to eq(described_class::RETRIES) end - it 'sets a default backoff' do - backoff = subject.instance_variable_get(:@backoff) - expect(backoff).to eq(described_class::BACKOFF) + it 'sets a default backoff policy' do + backoff_policy = subject.instance_variable_get(:@backoff_policy) + expect(backoff_policy).to be_a(Segment::Analytics::BackoffPolicy) end it 'initializes a new Net::HTTP with default host and port' do @@ -63,14 +63,14 @@ class Analytics context 'options are given' do let(:path) { 'my/cool/path' } let(:retries) { 1234 } - let(:backoff) { 10 } + let(:backoff_policy) { FakeBackoffPolicy.new([1, 2, 3]) } let(:host) { 'http://www.example.com' } let(:port) { 8080 } let(:options) do { path: path, retries: retries, - backoff: backoff, + backoff_policy: backoff_policy, host: host, port: port } @@ -86,8 +86,9 @@ class Analytics expect(subject.instance_variable_get(:@retries)).to eq(retries) end - it 'sets passed in backoff' do - expect(subject.instance_variable_get(:@backoff)).to eq(backoff) + it 'sets passed in backoff backoff policy' do + expect(subject.instance_variable_get(:@backoff_policy)) + .to eq(backoff_policy) end it 'initializes a new Net::HTTP with passed in host and port' do @@ -157,14 +158,17 @@ class Analytics let(:status_code) { status_code } let(:body) { body } let(:retries) { 4 } - let(:backoff) { 1 } - subject { described_class.new(retries: retries, backoff: backoff) } + let(:backoff_policy) { FakeBackoffPolicy.new([1000, 1000, 1000]) } + subject { + described_class.new(retries: retries, + backoff_policy: backoff_policy) + } it 'retries the request' do expect(subject) .to receive(:sleep) .exactly(retries - 1).times - .with(backoff) + .with(1) .and_return(nil) subject.post(write_key, batch) end @@ -219,7 +223,7 @@ class Analytics context 'request or parsing of response results in an exception' do let(:response_body) { 'Malformed JSON ---' } - subject { described_class.new(backoff: 0) } + subject { described_class.new(retries: 0) } it 'returns a -1 for status' do expect(subject.post(write_key, batch).status).to eq(-1) diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 8d8bdb8..8bc41b0 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -90,6 +90,18 @@ def run end end +# A backoff policy that returns a fixed list of values +class FakeBackoffPolicy + def initialize(interval_values) + @interval_values = interval_values + end + + def next_interval + raise 'FakeBackoffPolicy has no values left' if @interval_values.empty? + @interval_values.shift + end +end + # usage: # it "should return a result of 5" do # eventually(options: {timeout: 1}) { long_running_thing.result.should eq(5) } From 0c6ccddc20ed5646552da46bb6846d79dc356d03 Mon Sep 17 00:00:00 2001 From: Paul Kuruvilla Date: Tue, 5 Dec 2017 13:09:29 +0530 Subject: [PATCH 028/179] Tweak retry count to match segmentio/analytics-go --- lib/segment/analytics/defaults.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/segment/analytics/defaults.rb b/lib/segment/analytics/defaults.rb index f5a7b18..d641420 100644 --- a/lib/segment/analytics/defaults.rb +++ b/lib/segment/analytics/defaults.rb @@ -8,7 +8,7 @@ module Request SSL = true HEADERS = { 'Accept' => 'application/json', 'Content-Type' => 'application/json' } - RETRIES = 4 + RETRIES = 10 end module Queue From 58a955eb216264131e00a2f531d74182fb2c6c41 Mon Sep 17 00:00:00 2001 From: Paul Kuruvilla Date: Thu, 7 Dec 2017 14:22:28 +0530 Subject: [PATCH 029/179] Add User-Agent header --- lib/segment/analytics/defaults.rb | 3 ++- spec/segment/analytics/request_spec.rb | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/segment/analytics/defaults.rb b/lib/segment/analytics/defaults.rb index d641420..550bf67 100644 --- a/lib/segment/analytics/defaults.rb +++ b/lib/segment/analytics/defaults.rb @@ -7,7 +7,8 @@ module Request PATH = '/v1/import' SSL = true HEADERS = { 'Accept' => 'application/json', - 'Content-Type' => 'application/json' } + 'Content-Type' => 'application/json', + 'User-Agent' => "analytics-ruby/#{Analytics::VERSION}" } RETRIES = 10 end diff --git a/spec/segment/analytics/request_spec.rb b/spec/segment/analytics/request_spec.rb index babc928..721d7e9 100644 --- a/spec/segment/analytics/request_spec.rb +++ b/spec/segment/analytics/request_spec.rb @@ -118,7 +118,8 @@ class Analytics path = subject.instance_variable_get(:@path) default_headers = { 'Content-Type' => 'application/json', - 'Accept' => 'application/json' + 'Accept' => 'application/json', + 'User-Agent' => "analytics-ruby/#{Analytics::VERSION}" } expect(Net::HTTP::Post).to receive(:new).with( path, default_headers From aa35213c1dd082c462c7cbadbfa55f5f1c0e0626 Mon Sep 17 00:00:00 2001 From: Paul Kuruvilla Date: Fri, 15 Dec 2017 21:23:36 +0530 Subject: [PATCH 030/179] Add an end-to-end test (#139) * Add an end-to-end test * Enable E2E tests in .travis.yml * Replace httparty with faraday for ruby 1.9 compat * Ignore spec folder for code coverage * Don't set E2E env var, as it is set in Travis --- Rakefile | 1 + analytics-ruby.gemspec | 2 ++ codecov.yml | 2 ++ spec/helpers/runscope_client.rb | 38 +++++++++++++++++++++++ spec/segment/analytics/e2e_spec.rb | 48 ++++++++++++++++++++++++++++++ spec/spec_helper.rb | 1 + 6 files changed, 92 insertions(+) create mode 100644 codecov.yml create mode 100644 spec/helpers/runscope_client.rb create mode 100644 spec/segment/analytics/e2e_spec.rb diff --git a/Rakefile b/Rakefile index cc4f987..626fcbe 100644 --- a/Rakefile +++ b/Rakefile @@ -4,6 +4,7 @@ default_tasks = [] RSpec::Core::RakeTask.new(:spec) do |spec| spec.pattern = 'spec/**/*_spec.rb' + spec.rspec_opts = "--tag ~e2e" if ENV["RUN_E2E_TESTS"] != "true" end default_tasks << :spec diff --git a/analytics-ruby.gemspec b/analytics-ruby.gemspec index ff9636f..1670cf2 100644 --- a/analytics-ruby.gemspec +++ b/analytics-ruby.gemspec @@ -22,6 +22,8 @@ Gem::Specification.new do |spec| spec.add_development_dependency 'rspec', '~> 3.0' spec.add_development_dependency 'tzinfo', '1.2.1' spec.add_development_dependency 'activesupport', '~> 4.1.11' + spec.add_development_dependency 'faraday', '~> 0.13' + spec.add_development_dependency 'pmap', '~> 1.1' if RUBY_VERSION >= "2.1" spec.add_development_dependency 'rubocop', '~> 0.51.0' diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 0000000..c6e5dff --- /dev/null +++ b/codecov.yml @@ -0,0 +1,2 @@ +ignore: + - "spec/**/*" diff --git a/spec/helpers/runscope_client.rb b/spec/helpers/runscope_client.rb new file mode 100644 index 0000000..7d0e7f8 --- /dev/null +++ b/spec/helpers/runscope_client.rb @@ -0,0 +1,38 @@ +require 'faraday' +require 'pmap' + +class RunscopeClient + def initialize(api_token) + headers = { 'Authorization' => "Bearer #{api_token}" } + @conn = Faraday.new('https://api.runscope.com', headers: headers) + end + + def requests(bucket_key) + with_retries(3) do + response = @conn.get("/buckets/#{bucket_key}/messages", count: 10) + + raise "Runscope error. #{response.body}" unless response.status == 200 + + message_uuids = JSON.parse(response.body)['data'].map { |message| + message.fetch('uuid') + } + + message_uuids.pmap { |uuid| + response = @conn.get("/buckets/#{bucket_key}/messages/#{uuid}") + raise "Runscope error. #{response.body}" unless response.status == 200 + JSON.parse(response.body).fetch('data').fetch('request') + } + end + end + + private + + def with_retries(max_retries) + retries ||= 0 + yield + rescue StandardError => e + retries += 1 + retry if retries < max_retries + raise e + end +end diff --git a/spec/segment/analytics/e2e_spec.rb b/spec/segment/analytics/e2e_spec.rb new file mode 100644 index 0000000..e528d59 --- /dev/null +++ b/spec/segment/analytics/e2e_spec.rb @@ -0,0 +1,48 @@ +require 'spec_helper' + +module Segment + # End-to-end tests that send events to a segment source and verifies that a + # webhook connected to the source (configured manually via the app) is able + # to receive the data sent by this library. + describe 'End-to-end tests', e2e: true do + # Segment write key for + # https://app.segment.com/segment-libraries/sources/analytics_ruby_e2e_test/overview. + # + # This source is configured to send events to the Runscope bucket used by + # this test. + WRITE_KEY = 'qhdMksLsQTi9MES3CHyzsWRRt4ub5VM6' + + # Runscope bucket key for https://www.runscope.com/stream/umkvkgv7ndby + RUNSCOPE_BUCKET_KEY = 'umkvkgv7ndby' + + let(:client) { Segment::Analytics.new(write_key: WRITE_KEY) } + let(:runscope_client) { RunscopeClient.new(ENV.fetch('RUNSCOPE_TOKEN')) } + + it 'tracks events' do + id = SecureRandom.uuid + client.track( + user_id: 'dummy_user_id', + event: 'E2E Test', + properties: { id: id } + ) + client.flush + + # Allow events to propagate to runscope + eventually(timeout: 30) { + expect(has_matching_request?(id)).to eq(true) + } + end + + def has_matching_request?(id) + captured_requests = runscope_client.requests(RUNSCOPE_BUCKET_KEY) + captured_requests.any? do |request| + begin + body = JSON.parse(request['body']) + body['properties'] && body['properties']['id'] == id + rescue JSON::ParserError + false + end + end + end + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 8bc41b0..7cd6c2f 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -6,6 +6,7 @@ require 'segment/analytics' require 'active_support/time' +require './spec/helpers/runscope_client' # Setting timezone for ActiveSupport::TimeWithZone to UTC Time.zone = 'UTC' From c429653cd16e59bda15bc79742be9c26e0f954de Mon Sep 17 00:00:00 2001 From: Paul Kuruvilla Date: Sat, 16 Dec 2017 03:41:28 +0530 Subject: [PATCH 031/179] Spawn worker after adding item to queue (#138) --- lib/segment/analytics/client.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/segment/analytics/client.rb b/lib/segment/analytics/client.rb index c0f0d67..b216c7a 100644 --- a/lib/segment/analytics/client.rb +++ b/lib/segment/analytics/client.rb @@ -309,8 +309,8 @@ def enqueue(action) action[:messageId] ||= uid if @queue.length < @max_queue_size - ensure_worker_running @queue << action + ensure_worker_running true else From 779cc774c976814b5ae4a0e08687910871215446 Mon Sep 17 00:00:00 2001 From: Paul Kuruvilla Date: Wed, 20 Dec 2017 22:48:02 +0530 Subject: [PATCH 032/179] Avoid catching Exception, only catch StandardError instead (#141) --- lib/segment/analytics/request.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/segment/analytics/request.rb b/lib/segment/analytics/request.rb index 026950a..d077a3a 100644 --- a/lib/segment/analytics/request.rb +++ b/lib/segment/analytics/request.rb @@ -84,7 +84,7 @@ def retry_with_backoff(retries_remaining, &block) begin result, should_retry = yield return [result, nil] unless should_retry - rescue Exception => e + rescue StandardError => e should_retry = true caught_exception = e end From 114af0b4ac8784eb781ba0cf295a4ae7a5bfadef Mon Sep 17 00:00:00 2001 From: Paul Kuruvilla Date: Fri, 5 Jan 2018 17:46:19 +0530 Subject: [PATCH 033/179] Use a custom 'Message' class --- lib/segment/analytics/message.rb | 17 +++++++++++++++++ lib/segment/analytics/worker.rb | 6 +++--- 2 files changed, 20 insertions(+), 3 deletions(-) create mode 100644 lib/segment/analytics/message.rb diff --git a/lib/segment/analytics/message.rb b/lib/segment/analytics/message.rb new file mode 100644 index 0000000..47b47b0 --- /dev/null +++ b/lib/segment/analytics/message.rb @@ -0,0 +1,17 @@ +require 'forwardable' + +module Segment + class Analytics + # Represents a message to be sent to the API + class Message + extend Forwardable + + def initialize(hash) + @hash = hash + end + + def_delegators :@hash, :to_json # TODO: Cache and reuse + def_delegators :@hash, :[] + end + end +end diff --git a/lib/segment/analytics/worker.rb b/lib/segment/analytics/worker.rb index f619a1b..5e94b5f 100644 --- a/lib/segment/analytics/worker.rb +++ b/lib/segment/analytics/worker.rb @@ -1,7 +1,7 @@ require 'segment/analytics/defaults' -require 'segment/analytics/utils' -require 'segment/analytics/defaults' +require 'segment/analytics/message' require 'segment/analytics/request' +require 'segment/analytics/utils' module Segment class Analytics @@ -38,7 +38,7 @@ def run @lock.synchronize do until @batch.length >= @batch_size || @queue.empty? - @batch << @queue.pop + @batch << Message.new(@queue.pop) end end From 5174fbdec01fdaf639a59ff1dc0faccee94abee9 Mon Sep 17 00:00:00 2001 From: Paul Kuruvilla Date: Fri, 5 Jan 2018 18:03:22 +0530 Subject: [PATCH 034/179] Add custom 'MessageBatch' class --- lib/segment/analytics/message_batch.rb | 18 ++++++++++++++++++ lib/segment/analytics/worker.rb | 3 ++- 2 files changed, 20 insertions(+), 1 deletion(-) create mode 100644 lib/segment/analytics/message_batch.rb diff --git a/lib/segment/analytics/message_batch.rb b/lib/segment/analytics/message_batch.rb new file mode 100644 index 0000000..bc9c95c --- /dev/null +++ b/lib/segment/analytics/message_batch.rb @@ -0,0 +1,18 @@ +module Segment + class Analytics + # A batch of `Message`s to be sent to the API + class MessageBatch + extend Forwardable + + def initialize + @messages = [] + end + + def_delegators :@messages, :to_json # TODO: Cache and reuse + def_delegators :@messages, :<< + def_delegators :@messages, :clear + def_delegators :@messages, :empty? + def_delegators :@messages, :length + end + end +end diff --git a/lib/segment/analytics/worker.rb b/lib/segment/analytics/worker.rb index 5e94b5f..93e2067 100644 --- a/lib/segment/analytics/worker.rb +++ b/lib/segment/analytics/worker.rb @@ -1,5 +1,6 @@ require 'segment/analytics/defaults' require 'segment/analytics/message' +require 'segment/analytics/message_batch' require 'segment/analytics/request' require 'segment/analytics/utils' @@ -26,7 +27,7 @@ def initialize(queue, write_key, options = {}) @write_key = write_key @batch_size = options[:batch_size] || Queue::BATCH_SIZE @on_error = options[:on_error] || proc { |status, error| } - @batch = [] + @batch = MessageBatch.new @lock = Mutex.new end From f6e659b44be55a27d2d7ec94a8f7a61d226be425 Mon Sep 17 00:00:00 2001 From: Paul Kuruvilla Date: Fri, 5 Jan 2018 18:04:14 +0530 Subject: [PATCH 035/179] Reject messages that exceed the maximum allowed size --- lib/segment/analytics/defaults.rb | 4 ++++ lib/segment/analytics/message.rb | 6 +++++ lib/segment/analytics/message_batch.rb | 12 +++++++++- spec/segment/analytics/message_batch_spec.rb | 25 ++++++++++++++++++++ 4 files changed, 46 insertions(+), 1 deletion(-) create mode 100644 spec/segment/analytics/message_batch_spec.rb diff --git a/lib/segment/analytics/defaults.rb b/lib/segment/analytics/defaults.rb index 550bf67..18cf861 100644 --- a/lib/segment/analytics/defaults.rb +++ b/lib/segment/analytics/defaults.rb @@ -17,6 +17,10 @@ module Queue MAX_SIZE = 10000 end + module Message + MAX_BYTES = 32768 # 32Kb + end + module BackoffPolicy MIN_TIMEOUT_MS = 100 MAX_TIMEOUT_MS = 10000 diff --git a/lib/segment/analytics/message.rb b/lib/segment/analytics/message.rb index 47b47b0..8b62f82 100644 --- a/lib/segment/analytics/message.rb +++ b/lib/segment/analytics/message.rb @@ -1,5 +1,7 @@ require 'forwardable' +require 'segment/analytics/defaults' + module Segment class Analytics # Represents a message to be sent to the API @@ -10,6 +12,10 @@ def initialize(hash) @hash = hash end + def too_big? + to_json.bytesize > Defaults::Message::MAX_BYTES + end + def_delegators :@hash, :to_json # TODO: Cache and reuse def_delegators :@hash, :[] end diff --git a/lib/segment/analytics/message_batch.rb b/lib/segment/analytics/message_batch.rb index bc9c95c..6bf77bf 100644 --- a/lib/segment/analytics/message_batch.rb +++ b/lib/segment/analytics/message_batch.rb @@ -1,15 +1,25 @@ +require 'segment/analytics/logging' + module Segment class Analytics # A batch of `Message`s to be sent to the API class MessageBatch extend Forwardable + include Segment::Analytics::Logging def initialize @messages = [] end + def <<(message) + if message.too_big? + logger.error('a message exceeded the maximum allowed size') + else + @messages << message + end + end + def_delegators :@messages, :to_json # TODO: Cache and reuse - def_delegators :@messages, :<< def_delegators :@messages, :clear def_delegators :@messages, :empty? def_delegators :@messages, :length diff --git a/spec/segment/analytics/message_batch_spec.rb b/spec/segment/analytics/message_batch_spec.rb new file mode 100644 index 0000000..e50f4d0 --- /dev/null +++ b/spec/segment/analytics/message_batch_spec.rb @@ -0,0 +1,25 @@ +require 'spec_helper' + +module Segment + class Analytics + describe MessageBatch do + describe '#<<' do + subject { described_class.new } + + it 'appends messages' do + subject << Message.new('a' => 'b') + expect(subject.length).to eq(1) + end + + it 'rejects messages that exceed the maximum allowed size' do + max_bytes = Segment::Analytics::Defaults::Message::MAX_BYTES + hash = { 'a' => 'b' * max_bytes } + message = Message.new(hash) + + subject << message + expect(subject.length).to eq(0) + end + end + end + end +end From feedf1a345f8f5c6e5386401fba858b3af32a7a5 Mon Sep 17 00:00:00 2001 From: Paul Kuruvilla Date: Fri, 5 Jan 2018 18:24:38 +0530 Subject: [PATCH 036/179] Cache JSON generation for individual messages --- lib/segment/analytics/message.rb | 11 ++++---- lib/segment/analytics/message_batch.rb | 2 +- spec/segment/analytics/message_spec.rb | 35 ++++++++++++++++++++++++++ 3 files changed, 41 insertions(+), 7 deletions(-) create mode 100644 spec/segment/analytics/message_spec.rb diff --git a/lib/segment/analytics/message.rb b/lib/segment/analytics/message.rb index 8b62f82..b11b121 100644 --- a/lib/segment/analytics/message.rb +++ b/lib/segment/analytics/message.rb @@ -1,13 +1,9 @@ -require 'forwardable' - require 'segment/analytics/defaults' module Segment class Analytics # Represents a message to be sent to the API class Message - extend Forwardable - def initialize(hash) @hash = hash end @@ -16,8 +12,11 @@ def too_big? to_json.bytesize > Defaults::Message::MAX_BYTES end - def_delegators :@hash, :to_json # TODO: Cache and reuse - def_delegators :@hash, :[] + # Since the hash is expected to not be modified (set at initialization), + # the JSON version can be cached after the first computation. + def to_json(*args) + @json ||= @hash.to_json(*args) + end end end end diff --git a/lib/segment/analytics/message_batch.rb b/lib/segment/analytics/message_batch.rb index 6bf77bf..0cf1dc4 100644 --- a/lib/segment/analytics/message_batch.rb +++ b/lib/segment/analytics/message_batch.rb @@ -19,7 +19,7 @@ def <<(message) end end - def_delegators :@messages, :to_json # TODO: Cache and reuse + def_delegators :@messages, :to_json def_delegators :@messages, :clear def_delegators :@messages, :empty? def_delegators :@messages, :length diff --git a/spec/segment/analytics/message_spec.rb b/spec/segment/analytics/message_spec.rb new file mode 100644 index 0000000..b156183 --- /dev/null +++ b/spec/segment/analytics/message_spec.rb @@ -0,0 +1,35 @@ +require 'spec_helper' + +module Segment + class Analytics + describe Message do + describe '#to_json' do + it 'caches JSON conversions' do + # Keeps track of the number of times to_json was called + nested_obj = Class.new do + attr_reader :to_json_call_count + + def initialize + @to_json_call_count = 0 + end + + def to_json(*_) + @to_json_call_count += 1 + '{}' + end + end.new + + message = Message.new('some_key' => nested_obj) + expect(nested_obj.to_json_call_count).to eq(0) + + message.to_json + expect(nested_obj.to_json_call_count).to eq(1) + + # When called a second time, the call count shouldn't increase + message.to_json + expect(nested_obj.to_json_call_count).to eq(1) + end + end + end + end +end From 6682f02945050fa16a4e8d58b7bfd1c00983721c Mon Sep 17 00:00:00 2001 From: Paul Kuruvilla Date: Tue, 9 Jan 2018 18:42:40 +0530 Subject: [PATCH 037/179] Limit batch size to 500kb --- lib/segment/analytics/defaults.rb | 6 +++- lib/segment/analytics/message.rb | 6 +++- lib/segment/analytics/message_batch.rb | 34 ++++++++++++++++++-- lib/segment/analytics/worker.rb | 7 ++-- spec/segment/analytics/message_batch_spec.rb | 2 +- spec/segment/analytics/worker_spec.rb | 7 ++-- 6 files changed, 51 insertions(+), 11 deletions(-) diff --git a/lib/segment/analytics/defaults.rb b/lib/segment/analytics/defaults.rb index 18cf861..8562346 100644 --- a/lib/segment/analytics/defaults.rb +++ b/lib/segment/analytics/defaults.rb @@ -13,7 +13,6 @@ module Request end module Queue - BATCH_SIZE = 100 MAX_SIZE = 10000 end @@ -21,6 +20,11 @@ module Message MAX_BYTES = 32768 # 32Kb end + module MessageBatch + MAX_BYTES = 512_000 # 500Kb + MAX_SIZE = 100 + end + module BackoffPolicy MIN_TIMEOUT_MS = 100 MAX_TIMEOUT_MS = 10000 diff --git a/lib/segment/analytics/message.rb b/lib/segment/analytics/message.rb index b11b121..e00a74d 100644 --- a/lib/segment/analytics/message.rb +++ b/lib/segment/analytics/message.rb @@ -9,7 +9,11 @@ def initialize(hash) end def too_big? - to_json.bytesize > Defaults::Message::MAX_BYTES + json_size > Defaults::Message::MAX_BYTES + end + + def json_size + to_json.bytesize end # Since the hash is expected to not be modified (set at initialization), diff --git a/lib/segment/analytics/message_batch.rb b/lib/segment/analytics/message_batch.rb index 0cf1dc4..bc44971 100644 --- a/lib/segment/analytics/message_batch.rb +++ b/lib/segment/analytics/message_batch.rb @@ -6,9 +6,12 @@ class Analytics class MessageBatch extend Forwardable include Segment::Analytics::Logging + include Segment::Analytics::Defaults::MessageBatch - def initialize + def initialize(max_message_count) @messages = [] + @max_message_count = max_message_count + @json_size = 0 end def <<(message) @@ -16,13 +19,40 @@ def <<(message) logger.error('a message exceeded the maximum allowed size') else @messages << message + @json_size += message.json_size + 1 # One byte for the comma end end + def full? + item_count_exhausted? || size_exhausted? + end + + def clear + @messages.clear + @json_size = 0 + end + def_delegators :@messages, :to_json - def_delegators :@messages, :clear def_delegators :@messages, :empty? def_delegators :@messages, :length + + private + + def item_count_exhausted? + @messages.length >= @max_message_count + end + + # We consider the max size here as just enough to leave room for one more + # message of the largest size possible. This is a shortcut that allows us + # to use a native Ruby `Queue` that doesn't allow peeking. The tradeoff + # here is that we might fit in less messages than possible into a batch. + # + # The alternative is to use our own `Queue` implementation that allows + # peeking, and to consider the next message size when calculating whether + # the message can be accomodated in this batch. + def size_exhausted? + @json_size >= (MAX_BYTES - Defaults::Message::MAX_BYTES) + end end end end diff --git a/lib/segment/analytics/worker.rb b/lib/segment/analytics/worker.rb index 93e2067..c57e219 100644 --- a/lib/segment/analytics/worker.rb +++ b/lib/segment/analytics/worker.rb @@ -25,9 +25,9 @@ def initialize(queue, write_key, options = {}) symbolize_keys! options @queue = queue @write_key = write_key - @batch_size = options[:batch_size] || Queue::BATCH_SIZE @on_error = options[:on_error] || proc { |status, error| } - @batch = MessageBatch.new + batch_size = options[:batch_size] || Defaults::MessageBatch::MAX_SIZE + @batch = MessageBatch.new(batch_size) @lock = Mutex.new end @@ -38,13 +38,12 @@ def run return if @queue.empty? @lock.synchronize do - until @batch.length >= @batch_size || @queue.empty? + until @batch.full? || @queue.empty? @batch << Message.new(@queue.pop) end end res = Request.new.post @write_key, @batch - @on_error.call(res.status, res.error) unless res.status == 200 @lock.synchronize { @batch.clear } diff --git a/spec/segment/analytics/message_batch_spec.rb b/spec/segment/analytics/message_batch_spec.rb index e50f4d0..850c9bd 100644 --- a/spec/segment/analytics/message_batch_spec.rb +++ b/spec/segment/analytics/message_batch_spec.rb @@ -4,7 +4,7 @@ module Segment class Analytics describe MessageBatch do describe '#<<' do - subject { described_class.new } + subject { described_class.new(100) } it 'appends messages' do subject << Message.new('a' => 'b') diff --git a/spec/segment/analytics/worker_spec.rb b/spec/segment/analytics/worker_spec.rb index 3abd4d8..7668124 100644 --- a/spec/segment/analytics/worker_spec.rb +++ b/spec/segment/analytics/worker_spec.rb @@ -6,8 +6,11 @@ class Analytics describe '#init' do it 'accepts string keys' do queue = Queue.new - worker = Segment::Analytics::Worker.new(queue, 'secret', 'batch_size' => 100) - expect(worker.instance_variable_get(:@batch_size)).to eq(100) + worker = Segment::Analytics::Worker.new(queue, + 'secret', + 'batch_size' => 100) + batch = worker.instance_variable_get(:@batch) + expect(batch.instance_variable_get(:@max_message_count)).to eq(100) end end From 39232c2135f48cb98f14628c2044c8b28a148ee7 Mon Sep 17 00:00:00 2001 From: Paul Kuruvilla Date: Tue, 9 Jan 2018 18:55:21 +0530 Subject: [PATCH 038/179] Add specs for MessageBatch#full? --- spec/segment/analytics/message_batch_spec.rb | 39 ++++++++++++++++++-- 1 file changed, 36 insertions(+), 3 deletions(-) diff --git a/spec/segment/analytics/message_batch_spec.rb b/spec/segment/analytics/message_batch_spec.rb index 850c9bd..562196a 100644 --- a/spec/segment/analytics/message_batch_spec.rb +++ b/spec/segment/analytics/message_batch_spec.rb @@ -3,16 +3,25 @@ module Segment class Analytics describe MessageBatch do - describe '#<<' do - subject { described_class.new(100) } + subject { described_class.new(100) } + describe '#<<' do it 'appends messages' do subject << Message.new('a' => 'b') expect(subject.length).to eq(1) end it 'rejects messages that exceed the maximum allowed size' do - max_bytes = Segment::Analytics::Defaults::Message::MAX_BYTES + max_bytes = Defaults::Message::MAX_BYTES + hash = { 'a' => 'b' * max_bytes } + message = Message.new(hash) + + subject << message + expect(subject.length).to eq(0) + end + + it 'rejects messages that exceed the maximum allowed size' do + max_bytes = Defaults::Message::MAX_BYTES hash = { 'a' => 'b' * max_bytes } message = Message.new(hash) @@ -20,6 +29,30 @@ class Analytics expect(subject.length).to eq(0) end end + + describe '#full?' do + it 'returns true once item count is exceeded' do + 99.times { subject << Message.new(a: 'b') } + expect(subject.full?).to be(false) + + subject << Message.new(a: 'b') + expect(subject.full?).to be(true) + end + + it 'returns true once max size is almost exceeded' do + message = Message.new(a: 'b' * (Defaults::Message::MAX_BYTES - 10)) + + # Each message is under the individual limit + expect(message.json_size).to be < Defaults::Message::MAX_BYTES + + # Size of the batch is over the limit + expect(50 * message.json_size).to be > Defaults::MessageBatch::MAX_BYTES + + expect(subject.full?).to be(false) + 50.times { subject << message } + expect(subject.full?).to be(true) + end + end end end end From c8cf2f10cdceaf7651fb02b7f2179b25d9e7c2e7 Mon Sep 17 00:00:00 2001 From: Paul Kuruvilla Date: Tue, 9 Jan 2018 19:03:24 +0530 Subject: [PATCH 039/179] Increase number of runscope messages checked for e2e tests Since builds run in parallel, the e2e tests sometimes fail because their runscope messages are pushed down by other builds. We currently test against 7 ruby versions, and a maximum of 2 builds are triggered per PR (push + PR build). Checking 20 items should be sufficient to handle this workload. --- spec/helpers/runscope_client.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/helpers/runscope_client.rb b/spec/helpers/runscope_client.rb index 7d0e7f8..a68be26 100644 --- a/spec/helpers/runscope_client.rb +++ b/spec/helpers/runscope_client.rb @@ -9,7 +9,7 @@ def initialize(api_token) def requests(bucket_key) with_retries(3) do - response = @conn.get("/buckets/#{bucket_key}/messages", count: 10) + response = @conn.get("/buckets/#{bucket_key}/messages", count: 20) raise "Runscope error. #{response.body}" unless response.status == 200 From 1fcc9583383bde7ffdde09b3f95e30a0c9ca2720 Mon Sep 17 00:00:00 2001 From: Paul Kuruvilla Date: Wed, 10 Jan 2018 13:53:50 +0530 Subject: [PATCH 040/179] Fix long lines in worker_spec.rb --- spec/segment/analytics/worker_spec.rb | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/spec/segment/analytics/worker_spec.rb b/spec/segment/analytics/worker_spec.rb index 7668124..c85861a 100644 --- a/spec/segment/analytics/worker_spec.rb +++ b/spec/segment/analytics/worker_spec.rb @@ -38,8 +38,11 @@ class Analytics end.to_not raise_error end - it 'executes the error handler, before the request phase ends, if the request is invalid' do - Segment::Analytics::Request.any_instance.stub(:post).and_return(Segment::Analytics::Response.new(400, 'Some error')) + it 'executes the error handler if the request is invalid' do + Segment::Analytics::Request + .any_instance + .stub(:post) + .and_return(Segment::Analytics::Response.new(400, 'Some error')) status = error = nil on_error = proc do |yielded_status, yielded_error| @@ -49,9 +52,10 @@ class Analytics queue = Queue.new queue << {} - worker = Segment::Analytics::Worker.new queue, 'secret', :on_error => on_error + worker = described_class.new(queue, 'secret', :on_error => on_error) - # This is to ensure that Client#flush doesn't finish before calling the error handler. + # This is to ensure that Client#flush doesn't finish before calling + # the error handler. Thread.new { worker.run } sleep 0.1 # First give thread time to spin-up. sleep 0.01 while worker.is_requesting? @@ -72,7 +76,9 @@ class Analytics queue = Queue.new queue << Requested::TRACK - worker = Segment::Analytics::Worker.new queue, 'testsecret', :on_error => on_error + worker = described_class.new(queue, + 'testsecret', + :on_error => on_error) worker.run expect(queue).to be_empty From c76f94a39d66dfaad8965ac2b1e3f5a50a5fbdfc Mon Sep 17 00:00:00 2001 From: Paul Kuruvilla Date: Wed, 10 Jan 2018 14:00:43 +0530 Subject: [PATCH 041/179] Fix indeterminate test in worker_spec.rb The assertion in the thread wouldn't trigger if the test exited first. --- spec/segment/analytics/worker_spec.rb | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/spec/segment/analytics/worker_spec.rb b/spec/segment/analytics/worker_spec.rb index c85861a..e66db64 100644 --- a/spec/segment/analytics/worker_spec.rb +++ b/spec/segment/analytics/worker_spec.rb @@ -98,12 +98,11 @@ class Analytics queue << Requested::TRACK worker = Segment::Analytics::Worker.new(queue, 'testsecret') - Thread.new do - worker.run - expect(worker.is_requesting?).to eq(false) - end - + worker_thread = Thread.new { worker.run } eventually { expect(worker.is_requesting?).to eq(true) } + + worker_thread.join + expect(worker.is_requesting?).to eq(false) end end end From 42b28619c9602d14830ed746ab96d4a1c01bb3d7 Mon Sep 17 00:00:00 2001 From: Paul Kuruvilla Date: Thu, 11 Jan 2018 23:43:38 +0530 Subject: [PATCH 042/179] Log errors for messages that exceed the maximum size (#143) * Use a custom 'Message' class * Add custom 'MessageBatch' class * Reject messages that exceed the maximum allowed size * Cache JSON generation for individual messages --- lib/segment/analytics/defaults.rb | 4 +++ lib/segment/analytics/message.rb | 22 ++++++++++++ lib/segment/analytics/message_batch.rb | 28 ++++++++++++++++ lib/segment/analytics/worker.rb | 9 ++--- spec/segment/analytics/message_batch_spec.rb | 25 ++++++++++++++ spec/segment/analytics/message_spec.rb | 35 ++++++++++++++++++++ 6 files changed, 119 insertions(+), 4 deletions(-) create mode 100644 lib/segment/analytics/message.rb create mode 100644 lib/segment/analytics/message_batch.rb create mode 100644 spec/segment/analytics/message_batch_spec.rb create mode 100644 spec/segment/analytics/message_spec.rb diff --git a/lib/segment/analytics/defaults.rb b/lib/segment/analytics/defaults.rb index 550bf67..18cf861 100644 --- a/lib/segment/analytics/defaults.rb +++ b/lib/segment/analytics/defaults.rb @@ -17,6 +17,10 @@ module Queue MAX_SIZE = 10000 end + module Message + MAX_BYTES = 32768 # 32Kb + end + module BackoffPolicy MIN_TIMEOUT_MS = 100 MAX_TIMEOUT_MS = 10000 diff --git a/lib/segment/analytics/message.rb b/lib/segment/analytics/message.rb new file mode 100644 index 0000000..b11b121 --- /dev/null +++ b/lib/segment/analytics/message.rb @@ -0,0 +1,22 @@ +require 'segment/analytics/defaults' + +module Segment + class Analytics + # Represents a message to be sent to the API + class Message + def initialize(hash) + @hash = hash + end + + def too_big? + to_json.bytesize > Defaults::Message::MAX_BYTES + end + + # Since the hash is expected to not be modified (set at initialization), + # the JSON version can be cached after the first computation. + def to_json(*args) + @json ||= @hash.to_json(*args) + end + end + end +end diff --git a/lib/segment/analytics/message_batch.rb b/lib/segment/analytics/message_batch.rb new file mode 100644 index 0000000..0cf1dc4 --- /dev/null +++ b/lib/segment/analytics/message_batch.rb @@ -0,0 +1,28 @@ +require 'segment/analytics/logging' + +module Segment + class Analytics + # A batch of `Message`s to be sent to the API + class MessageBatch + extend Forwardable + include Segment::Analytics::Logging + + def initialize + @messages = [] + end + + def <<(message) + if message.too_big? + logger.error('a message exceeded the maximum allowed size') + else + @messages << message + end + end + + def_delegators :@messages, :to_json + def_delegators :@messages, :clear + def_delegators :@messages, :empty? + def_delegators :@messages, :length + end + end +end diff --git a/lib/segment/analytics/worker.rb b/lib/segment/analytics/worker.rb index f619a1b..93e2067 100644 --- a/lib/segment/analytics/worker.rb +++ b/lib/segment/analytics/worker.rb @@ -1,7 +1,8 @@ require 'segment/analytics/defaults' -require 'segment/analytics/utils' -require 'segment/analytics/defaults' +require 'segment/analytics/message' +require 'segment/analytics/message_batch' require 'segment/analytics/request' +require 'segment/analytics/utils' module Segment class Analytics @@ -26,7 +27,7 @@ def initialize(queue, write_key, options = {}) @write_key = write_key @batch_size = options[:batch_size] || Queue::BATCH_SIZE @on_error = options[:on_error] || proc { |status, error| } - @batch = [] + @batch = MessageBatch.new @lock = Mutex.new end @@ -38,7 +39,7 @@ def run @lock.synchronize do until @batch.length >= @batch_size || @queue.empty? - @batch << @queue.pop + @batch << Message.new(@queue.pop) end end diff --git a/spec/segment/analytics/message_batch_spec.rb b/spec/segment/analytics/message_batch_spec.rb new file mode 100644 index 0000000..e50f4d0 --- /dev/null +++ b/spec/segment/analytics/message_batch_spec.rb @@ -0,0 +1,25 @@ +require 'spec_helper' + +module Segment + class Analytics + describe MessageBatch do + describe '#<<' do + subject { described_class.new } + + it 'appends messages' do + subject << Message.new('a' => 'b') + expect(subject.length).to eq(1) + end + + it 'rejects messages that exceed the maximum allowed size' do + max_bytes = Segment::Analytics::Defaults::Message::MAX_BYTES + hash = { 'a' => 'b' * max_bytes } + message = Message.new(hash) + + subject << message + expect(subject.length).to eq(0) + end + end + end + end +end diff --git a/spec/segment/analytics/message_spec.rb b/spec/segment/analytics/message_spec.rb new file mode 100644 index 0000000..b156183 --- /dev/null +++ b/spec/segment/analytics/message_spec.rb @@ -0,0 +1,35 @@ +require 'spec_helper' + +module Segment + class Analytics + describe Message do + describe '#to_json' do + it 'caches JSON conversions' do + # Keeps track of the number of times to_json was called + nested_obj = Class.new do + attr_reader :to_json_call_count + + def initialize + @to_json_call_count = 0 + end + + def to_json(*_) + @to_json_call_count += 1 + '{}' + end + end.new + + message = Message.new('some_key' => nested_obj) + expect(nested_obj.to_json_call_count).to eq(0) + + message.to_json + expect(nested_obj.to_json_call_count).to eq(1) + + # When called a second time, the call count shouldn't increase + message.to_json + expect(nested_obj.to_json_call_count).to eq(1) + end + end + end + end +end From 8f987c7568059a66d2a33b27c7594241dcee16b3 Mon Sep 17 00:00:00 2001 From: Paul Kuruvilla Date: Thu, 11 Jan 2018 23:43:49 +0530 Subject: [PATCH 043/179] Add logging if queue is full (#145) --- .rubocop_todo.yml | 2 +- lib/segment/analytics/client.rb | 12 ++++++++++-- spec/segment/analytics/client_spec.rb | 10 ++++++++++ 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 8057bd2..b2a25cb 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -18,7 +18,7 @@ Metrics/AbcSize: # Offense count: 1 # Configuration parameters: CountComments. Metrics/ClassLength: - Max: 223 + Max: 229 # Offense count: 1 Metrics/CyclomaticComplexity: diff --git a/lib/segment/analytics/client.rb b/lib/segment/analytics/client.rb index b216c7a..0475dda 100644 --- a/lib/segment/analytics/client.rb +++ b/lib/segment/analytics/client.rb @@ -1,13 +1,16 @@ require 'thread' require 'time' + +require 'segment/analytics/defaults' +require 'segment/analytics/logging' require 'segment/analytics/utils' require 'segment/analytics/worker' -require 'segment/analytics/defaults' module Segment class Analytics class Client include Segment::Analytics::Utils + include Segment::Analytics::Logging # public: Creates a new client # @@ -314,7 +317,12 @@ def enqueue(action) true else - false # Queue is full + logger.warn( + 'Queue is full, dropping events. The :max_queue_size ' \ + 'configuration parameter can be increased to prevent this from ' \ + 'happening.' + ) + false end end diff --git a/spec/segment/analytics/client_spec.rb b/spec/segment/analytics/client_spec.rb index ebc9a0a..f8a52e3 100644 --- a/spec/segment/analytics/client_spec.rb +++ b/spec/segment/analytics/client_spec.rb @@ -275,6 +275,16 @@ class Analytics end end + it 'returns false if queue is full' do + client.instance_variable_set(:@max_queue_size, 1) + + [:track, :screen, :page, :group, :identify, :alias].each do |s| + expect(client.send(s, data)).to eq(true) + expect(client.send(s, data)).to eq(false) # Queue is full + queue.pop(true) + end + end + it 'converts message id to string' do [:track, :screen, :page, :group, :identify, :alias].each do |s| client.send(s, data) From 204d5a27bdde62c130b2a575c78d6e0ccd6f0ef8 Mon Sep 17 00:00:00 2001 From: Paul Kuruvilla Date: Thu, 11 Jan 2018 23:44:13 +0530 Subject: [PATCH 044/179] Reuse TCP connections (#149) --- lib/segment/analytics/request.rb | 5 +++++ lib/segment/analytics/worker.rb | 4 ++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/lib/segment/analytics/request.rb b/lib/segment/analytics/request.rb index d077a3a..502d8e1 100644 --- a/lib/segment/analytics/request.rb +++ b/lib/segment/analytics/request.rb @@ -112,6 +112,11 @@ def send_request(write_key, batch) [200, '{}'] else + # If `start` is not called, Ruby adds a 'Connection: close' header to + # all requests, preventing us from reusing a connection for multiple + # HTTP requests + @http.start unless @http.started? + response = @http.request(request, payload) [response.code.to_i, response.body] end diff --git a/lib/segment/analytics/worker.rb b/lib/segment/analytics/worker.rb index 93e2067..a5fb638 100644 --- a/lib/segment/analytics/worker.rb +++ b/lib/segment/analytics/worker.rb @@ -29,6 +29,7 @@ def initialize(queue, write_key, options = {}) @on_error = options[:on_error] || proc { |status, error| } @batch = MessageBatch.new @lock = Mutex.new + @request = Request.new end # public: Continuously runs the loop to check for new events @@ -43,8 +44,7 @@ def run end end - res = Request.new.post @write_key, @batch - + res = @request.post(@write_key, @batch) @on_error.call(res.status, res.error) unless res.status == 200 @lock.synchronize { @batch.clear } From 1c63a85fbbf45c0293c2b0a0e0fc4e26ba3bf2b3 Mon Sep 17 00:00:00 2001 From: Paul Kuruvilla Date: Fri, 12 Jan 2018 22:13:36 +0530 Subject: [PATCH 045/179] Cleanup docs (#150) * Cleanup docs for Segment::Analytics::Client * Add docs for top-level Analytics class --- .rubocop_todo.yml | 2 +- lib/segment/analytics.rb | 7 ++ lib/segment/analytics/client.rb | 198 +++++++++++++++++++------------- 3 files changed, 125 insertions(+), 82 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index b2a25cb..17dd033 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -18,7 +18,7 @@ Metrics/AbcSize: # Offense count: 1 # Configuration parameters: CountComments. Metrics/ClassLength: - Max: 229 + Max: 231 # Offense count: 1 Metrics/CyclomaticComplexity: diff --git a/lib/segment/analytics.rb b/lib/segment/analytics.rb index 0105c16..13ca1ec 100644 --- a/lib/segment/analytics.rb +++ b/lib/segment/analytics.rb @@ -9,6 +9,13 @@ module Segment class Analytics + # Initializes a new instance of {Segment::Analytics::Client}, to which all + # method calls are proxied. + # + # @param options includes options that are passed down to + # {Segment::Analytics::Client#initialize} + # @option options [Boolean] :stub (false) If true, requests don't hit the + # server and are stubbed to be successful. def initialize(options = {}) Request.stub = options[:stub] if options.has_key?(:stub) @client = Segment::Analytics::Client.new options diff --git a/lib/segment/analytics/client.rb b/lib/segment/analytics/client.rb index 0475dda..ce830a3 100644 --- a/lib/segment/analytics/client.rb +++ b/lib/segment/analytics/client.rb @@ -12,31 +12,30 @@ class Client include Segment::Analytics::Utils include Segment::Analytics::Logging - # public: Creates a new client - # - # attrs - Hash - # :write_key - String of your project's write_key - # :max_queue_size - Fixnum of the max calls to remain queued (optional) - # :on_error - Proc which handles error calls from the API - def initialize(attrs = {}) - symbolize_keys! attrs + # @param [Hash] opts + # @option opts [String] :write_key Your project's write_key + # @option opts [FixNum] :max_queue_size Maximum number of calls to be + # remain queued. + # @option opts [Proc] :on_error Handles error calls from the API. + def initialize(opts = {}) + symbolize_keys!(opts) @queue = Queue.new - @write_key = attrs[:write_key] - @max_queue_size = attrs[:max_queue_size] || Defaults::Queue::MAX_SIZE - @options = attrs + @write_key = opts[:write_key] + @max_queue_size = opts[:max_queue_size] || Defaults::Queue::MAX_SIZE + @options = opts @worker_mutex = Mutex.new - @worker = Worker.new @queue, @write_key, @options + @worker = Worker.new(@queue, @write_key, @options) check_write_key! at_exit { @worker_thread && @worker_thread[:should_exit] = true } end - # public: Synchronously waits until the worker has flushed the queue. - # Use only for scripts which are not long-running, and will - # specifically exit + # Synchronously waits until the worker has flushed the queue. # + # Use only for scripts which are not long-running, and will specifically + # exit def flush while !@queue.empty? || @worker.is_requesting? ensure_worker_running @@ -44,18 +43,25 @@ def flush end end - # public: Tracks an event + # Tracks an event + # + # @see https://segment.com/docs/sources/server/ruby/#track # - # attrs - Hash - # :anonymous_id - String of the user's id when you don't know who they are yet. (optional but you must provide either an anonymous_id or user_id. See: https://segment.io/docs/tracking - api/track/#user - id) - # :context - Hash of context. (optional) - # :event - String of event name. - # :integrations - Hash specifying what integrations this event goes to. (optional) - # :options - Hash specifying options such as user traits. (optional) - # :properties - Hash of event properties. (optional) - # :timestamp - Time of when the event occurred. (optional) - # :user_id - String of the user id. - # :message_id - String of the message id that uniquely identified a message across the API. (optional) + # @param [Hash] attrs + # @option attrs [String] :anonymous_id ID for a user when you don't know + # who they are yet. (optional but you must provide either an + # `anonymous_id` or `user_id`) + # @option attrs [Hash] :context ({}) + # @option attrs [String] :event Event name + # @option attrs [Hash] :integrations What integrations this event + # goes to (optional) + # @option attrs [Hash] :options Options such as user traits (optional) + # @option attrs [Hash] :properties Event properties (optional) + # @option attrs [Time] :timestamp When the event occurred (optional) + # @option attrs [String] :user_id The ID for this user in your database + # (optional but you must provide either an `anonymous_id` or `user_id`) + # @option attrs [String] :message_id ID that uniquely + # identifies a message across the API. (optional) def track(attrs) symbolize_keys! attrs check_user_id! attrs @@ -91,17 +97,24 @@ def track(attrs) }) end - # public: Identifies a user + # Identifies a user # - # attrs - Hash - # :anonymous_id - String of the user's id when you don't know who they are yet. (optional but you must provide either an anonymous_id or user_id. See: https://segment.io/docs/tracking - api/track/#user - id) - # :context - Hash of context. (optional) - # :integrations - Hash specifying what integrations this event goes to. (optional) - # :options - Hash specifying options such as user traits. (optional) - # :timestamp - Time of when the event occurred. (optional) - # :traits - Hash of user traits. (optional) - # :user_id - String of the user id - # :message_id - String of the message id that uniquely identified a message across the API. (optional) + # @see https://segment.com/docs/sources/server/ruby/#identify + # + # @param [Hash] attrs + # @option attrs [String] :anonymous_id ID for a user when you don't know + # who they are yet. (optional but you must provide either an + # `anonymous_id` or `user_id`) + # @option attrs [Hash] :context ({}) + # @option attrs [Hash] :integrations What integrations this event + # goes to (optional) + # @option attrs [Hash] :options Options such as user traits (optional) + # @option attrs [Time] :timestamp When the event occurred (optional) + # @option attrs [Hash] :traits User traits (optional) + # @option attrs [String] :user_id The ID for this user in your database + # (optional but you must provide either an `anonymous_id` or `user_id`) + # @option attrs [String] :message_id ID that uniquely identifies a + # message across the API. (optional) def identify(attrs) symbolize_keys! attrs check_user_id! attrs @@ -131,16 +144,20 @@ def identify(attrs) }) end - # public: Aliases a user from one id to another + # Aliases a user from one id to another + # + # @see https://segment.com/docs/sources/server/ruby/#alias # - # attrs - Hash - # :context - Hash of context (optional) - # :integrations - Hash specifying what integrations this event goes to. (optional) - # :options - Hash specifying options such as user traits. (optional) - # :previous_id - String of the id to alias from - # :timestamp - Time of when the alias occured (optional) - # :user_id - String of the id to alias to - # :message_id - String of the message id that uniquely identified a message across the API. (optional) + # @param [Hash] attrs + # @option attrs [Hash] :context ({}) + # @option attrs [Hash] :integrations What integrations this must be + # sent to (optional) + # @option attrs [Hash] :options Options such as user traits (optional) + # @option attrs [String] :previous_id The ID to alias from + # @option attrs [Time] :timestamp When the alias occurred (optional) + # @option attrs [String] :user_id The ID to alias to + # @option attrs [String] :message_id ID that uniquely identifies a + # message across the API. (optional) def alias(attrs) symbolize_keys! attrs @@ -167,16 +184,24 @@ def alias(attrs) }) end - # public: Associates a user identity with a group. + # Associates a user identity with a group. # - # attrs - Hash - # :context - Hash of context (optional) - # :integrations - Hash specifying what integrations this event goes to. (optional) - # :options - Hash specifying options such as user traits. (optional) - # :previous_id - String of the id to alias from - # :timestamp - Time of when the alias occured (optional) - # :user_id - String of the id to alias to - # :message_id - String of the message id that uniquely identified a message across the API. (optional) + # @see https://segment.com/docs/sources/server/ruby/#group + # + # @param [Hash] attrs + # @option attrs [String] :anonymous_id ID for a user when you don't know + # who they are yet. (optional but you must provide either an + # `anonymous_id` or `user_id`) + # @option attrs [Hash] :context ({}) + # @option attrs [String] :group_id The ID of the group + # @option attrs [Hash] :integrations What integrations this event + # goes to (optional) + # @option attrs [Hash] :options Options such as user traits (optional) + # @option attrs [Time] :timestamp When the event occurred (optional) + # @option attrs [String] :user_id The ID for the user that is part of + # the group + # @option attrs [String] :message_id ID that uniquely identifies a + # message across the API. (optional) def group(attrs) symbolize_keys! attrs check_user_id! attrs @@ -208,19 +233,25 @@ def group(attrs) }) end - # public: Records a page view + # Records a page view + # + # @see https://segment.com/docs/sources/server/ruby/#page # - # attrs - Hash - # :anonymous_id - String of the user's id when you don't know who they are yet. (optional but you must provide either an anonymous_id or user_id. See: https://segment.io/docs/tracking - api/track/#user - id) - # :category - String of the page category (optional) - # :context - Hash of context (optional) - # :integrations - Hash specifying what integrations this event goes to. (optional) - # :name - String name of the page - # :options - Hash specifying options such as user traits. (optional) - # :properties - Hash of page properties (optional) - # :timestamp - Time of when the pageview occured (optional) - # :user_id - String of the id to alias from - # :message_id - String of the message id that uniquely identified a message across the API. (optional) + # @param [Hash] attrs + # @option attrs [String] :anonymous_id ID for a user when you don't know + # who they are yet. (optional but you must provide either an + # `anonymous_id` or `user_id`) + # @option attrs [String] :category The page category (optional) + # @option attrs [Hash] :context ({}) + # @option attrs [Hash] :integrations What integrations this event + # goes to (optional) + # @option attrs [String] :name Name of the page + # @option attrs [Hash] :options Options such as user traits (optional) + # @option attrs [Hash] :properties Page properties (optional) + # @option attrs [Time] :timestamp When the pageview occurred (optional) + # @option attrs [String] :user_id The ID of the user viewing the page + # @option attrs [String] :message_id ID that uniquely identifies a + # message across the API. (optional) def page(attrs) symbolize_keys! attrs check_user_id! attrs @@ -252,18 +283,23 @@ def page(attrs) }) end - # public: Records a screen view (for a mobile app) + # Records a screen view (for a mobile app) # - # attrs - Hash - # :anonymous_id - String of the user's id when you don't know who they are yet. (optional but you must provide either an anonymous_id or user_id. See: https://segment.io/docs/tracking - api/track/#user - id) - # :category - String screen category (optional) - # :context - Hash of context (optional) - # :integrations - Hash specifying what integrations this event goes to. (optional) - # :name - String name of the screen - # :options - Hash specifying options such as user traits. (optional) - # :properties - Hash of screen properties (optional) - # :timestamp - Time of when the screen occured (optional) - # :user_id - String of the id to alias from + # @param [Hash] attrs + # @option attrs [String] :anonymous_id ID for a user when you don't know + # who they are yet. (optional but you must provide either an + # `anonymous_id` or `user_id`) + # @option attrs [String] :category The screen category (optional) + # @option attrs [Hash] :context ({}) + # @option attrs [Hash] :integrations What integrations this event + # goes to (optional) + # @option attrs [String] :name Name of the screen + # @option attrs [Hash] :options Options such as user traits (optional) + # @option attrs [Hash] :properties Page properties (optional) + # @option attrs [Time] :timestamp When the pageview occurred (optional) + # @option attrs [String] :user_id The ID of the user viewing the screen + # @option attrs [String] :message_id ID that uniquely identifies a + # message across the API. (optional) def screen(attrs) symbolize_keys! attrs check_user_id! attrs @@ -295,9 +331,7 @@ def screen(attrs) }) end - # public: Returns the number of queued messages - # - # returns Fixnum of messages in the queue + # @return [Fixnum] number of messages in the queue def queued_messages @queue.length end @@ -368,7 +402,9 @@ def event(attrs) end def check_user_id!(attrs) - raise ArgumentError, 'Must supply either user_id or anonymous_id' unless attrs[:user_id] || attrs[:anonymous_id] + unless attrs[:user_id] || attrs[:anonymous_id] + raise ArgumentError, 'Must supply either user_id or anonymous_id' + end end def ensure_worker_running From 5aea697248d312be57d7b967c1cf88672cec3e78 Mon Sep 17 00:00:00 2001 From: Paul Kuruvilla Date: Tue, 16 Jan 2018 11:32:09 +0530 Subject: [PATCH 046/179] Add Makefile --- Makefile | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/Makefile b/Makefile index 5082529..a027075 100644 --- a/Makefile +++ b/Makefile @@ -1,8 +1,17 @@ - -test: - rake spec - -build: - gem build ./analytics-ruby.gemspec - -.PHONY: test build + +# Install any tools required to build this library, e.g. Ruby, Bundler etc. +bootstrap: + brew install ruby + gem install bundler + +# Install any library dependencies. +dependencies: + bundle install --verbose + +# Run all tests and checks (including linters). +check: + bundle exec rake + +# Compile the code and produce any binaries where applicable. +build: + gem build ./analytics-ruby.gemspec From 93a0f285d8cec01358246c3e6080d3311b433239 Mon Sep 17 00:00:00 2001 From: Paul Kuruvilla Date: Wed, 17 Jan 2018 19:55:20 +0530 Subject: [PATCH 047/179] Remove bundle install travis step, use makefile for test script --- .travis.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 0638b59..69c91be 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,5 +9,4 @@ rvm: - 2.3.5 - 2.4.2 -before_install: - - gem install bundler +script: make check From 86683acc744dd0dfc555c0d20771688e568d6550 Mon Sep 17 00:00:00 2001 From: Paul Kuruvilla Date: Sun, 4 Feb 2018 17:04:37 +0530 Subject: [PATCH 048/179] Deploy tagged commits to RubyGems --- .travis.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.travis.yml b/.travis.yml index 69c91be..abeae2b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,3 +10,10 @@ rvm: - 2.4.2 script: make check + +# Deploy tagged commits to Rubygems +# See https://docs.travis-ci.com/user/deployment/rubygems/ for more details +deploy: + provider: rubygems + on: + tags: true From 4f8ca5b419b8549b873bae38d7b191ee3b3257ad Mon Sep 17 00:00:00 2001 From: Paul Kuruvilla Date: Sun, 4 Feb 2018 17:20:39 +0530 Subject: [PATCH 049/179] Update version and History.md for 2.2.4.pre --- History.md | 16 ++++++++++++++++ lib/segment/analytics/version.rb | 2 +- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/History.md b/History.md index 7dfa15c..29315a0 100644 --- a/History.md +++ b/History.md @@ -1,3 +1,19 @@ +2.2.4.pre / 2018-02-04 +================== + + * [Fix](https://github.com/segmentio/analytics-ruby/pull/147): Prevent 'batch + size exceeded' errors by automatically batching + items according to size + * [Performance](https://github.com/segmentio/analytics-ruby/pull/149): Reuse + TCP connections + * [Improvement](https://github.com/segmentio/analytics-ruby/pull/145): Emit logs + when in-memory queue is full + * [Improvement](https://github.com/segmentio/analytics-ruby/pull/143): Emit logs + when messages exceed maximum allowed size + * [Improvement](https://github.com/segmentio/analytics-ruby/pull/134): Add + exponential backoff to retries + * [Improvement](https://github.com/segmentio/analytics-ruby/pull/132): Handle + HTTP status code failure appropriately 2.2.3.pre / 2017-09-14 ================== diff --git a/lib/segment/analytics/version.rb b/lib/segment/analytics/version.rb index df7b3d0..8a55e5a 100644 --- a/lib/segment/analytics/version.rb +++ b/lib/segment/analytics/version.rb @@ -1,5 +1,5 @@ module Segment class Analytics - VERSION = '2.2.3.pre' + VERSION = '2.2.4.pre' end end From ded04b0cf8fb5dd8475ae6e1480a69bd3809afa2 Mon Sep 17 00:00:00 2001 From: Paul Kuruvilla Date: Sun, 4 Feb 2018 17:26:12 +0530 Subject: [PATCH 050/179] Update RELEASING.md --- RELEASING.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/RELEASING.md b/RELEASING.md index 0edd12b..4c54840 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -5,6 +5,5 @@ Releasing 2. Bump version in [`version.rb`](https://github.com/segmentio/analytics-ruby/blob/master/lib/segment/analytics/version.rb). 3. Update [`History.md`](https://github.com/segmentio/analytics-ruby/blob/master/History.md). 4. Commit and tag `git commit -am "Release {version}" && git tag -a {version} -m "Version {version}"`. - 5. Build the gem with the tagged version `make build`. - 6. Upload to RubyGems with `gem push analytics-ruby-{version}.gem`. - 7. Upload to Github with `git push -u origin master && git push --tags`. + 5. Upload to Github with `git push -u origin master && git push --tags`. +The tagged commit will be pushed to RubyGems via Travis. From d8a645d316810a831fb25da055653e4bac547bdd Mon Sep 17 00:00:00 2001 From: Prateek Srivastava Date: Tue, 13 Feb 2018 17:32:42 -0800 Subject: [PATCH 051/179] Add RubyGems API key --- .travis.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.travis.yml b/.travis.yml index 69c91be..9913504 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,3 +10,7 @@ rvm: - 2.4.2 script: make check + +deploy: + api_key: + secure: Ceq6J4aBpsoqRfSiC7z+/J4moOXNjcPMFb2Bfm5qE51cIZzeyuOIOc6zhrad9tUgoX6uTRRxLxkybyu4wNYSluMA3IXW20CJyXZeJEHIaTYIDTWFAIYyerBJyMujJycSo7XueWb0faKBENrBQKx1K1tS0EiXpA2rMhdA6RM3DOY= From ffd69ea92e15e8bcb51d53055b2f7231583c9d45 Mon Sep 17 00:00:00 2001 From: Nicolas Leger Date: Thu, 15 Feb 2018 15:09:26 +0100 Subject: [PATCH 052/179] [CI] Test against Ruby 2.5 --- .travis.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index c12531f..f0571ec 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,9 +5,10 @@ rvm: - 1.9.3 - 2.0.0 - 2.1.10 - - 2.2.8 - - 2.3.5 - - 2.4.2 + - 2.2.9 + - 2.3.6 + - 2.4.3 + - 2.5.0 script: make check From d6c4e7f9b00940fe5b74519352b9ff5b61f1d3e4 Mon Sep 17 00:00:00 2001 From: Prateek Srivastava Date: Mon, 5 Mar 2018 14:00:08 -0800 Subject: [PATCH 053/179] Update analytics binary to be compatible with the testing harness. https://paper.dropbox.com/doc/Libraries-End-to-End-Testing-Harness-dnotyIVIGYR7lnO1LcZtM#:uid=457845578223014078216812&h2=Contract Main changes are: * writeKey is a flag instead of an env var * method is a flag, instead of a top level command * context, properties, traits etc are optional Also fixed a bug with `screen` in the binary. Previously it was invoking `identify` instead of screen. --- bin/analytics | 189 +++++++++++++++++--------------------------------- 1 file changed, 65 insertions(+), 124 deletions(-) diff --git a/bin/analytics b/bin/analytics index e7543cb..e9e107e 100755 --- a/bin/analytics +++ b/bin/analytics @@ -10,143 +10,84 @@ program :name, 'simulator.rb' program :version, '0.0.1' program :description, 'scripting simulator' -# use an env var for write key, instead of a flag -Analytics = Segment::Analytics.new({ - write_key: ENV['SEGMENT_WRITE_KEY'], - on_error: Proc.new { |status, msg| print msg } -}) - -def toObject(str) - return JSON.parse(str) -end - -# high level -# analytics [options] -# SEGMENT_WRITE_KEY= ./analytics.rb [options] -# SEGMENT_WRITE_KEY= ./analytics.rb track --event testing --user 1234 --anonymous 567 --properties '{"hello": "goodbye"}' --context '{"slow":"poke"}' - - - -# track -command :track do |c| - c.description = 'track a user event' - c.option '--user ', String, 'the user id to send the event as' - c.option '--event ', String, 'the event name to send with the event' - c.option '--anonymous ', String, 'the anonymous user id to send the event as' - c.option '--properties ', 'the event properties to send (JSON-encoded)' - c.option '--context ', 'additional context for the event (JSON-encoded)' - - c.action do |args, options| - Analytics.track({ - user_id: options.user, - event: options.event, - anonymous_id: options.anonymous, - properties: toObject(options.properties), - context: toObject(options.context) - }) - Analytics.flush +def json_hash(str) + if str + return JSON.parse(str) end - end -# page -command :page do |c| - c.description = 'track a page view' - c.option '--user ', String, 'the user id to send the event as' - c.option '--anonymous ', String, 'the anonymous user id to send the event as' - c.option '--name ', String, 'the page name' - c.option '--properties ', 'the event properties to send (JSON-encoded)' - c.option '--context ', 'additional context for the event (JSON-encoded)' - c.option '--category ', 'the category of the page' - c.action do |args, options| - Analytics.page({ - user_id: options.user, - anonymous_id: options.anonymous, - name: options.name, - properties: toObject(options.properties), - context: toObject(options.context), - category: options.category - }) - Analytics.flush - end +# analytics -method= -segment-write-key= [options] -end +default_command :send -# identify -command :identify do |c| - c.description = 'identify a user' - c.option '--user ', String, 'the user id to send the event as' - c.option '--anonymous ', String, 'the anonymous user id to send the event as' - c.option '--traits ', String, 'the user traits to send (JSON-encoded)' - c.option '--context ', 'additional context for the event (JSON-encoded)' - c.action do |args, options| - Analytics.identify({ - user_id: options.user, - anonymous_id: options.anonymous, - traits: toObject(options.traits), - context: toObject(options.context) - }) - Analytics.flush - end +command :send do |c| + c.description = 'send a segment message' -end + c.option '--writeKey=', String, 'the Segment writeKey' + c.option '--type=', String, 'The Segment message type' -# screen -command :screen do |c| - c.description = 'track a screen view' - c.option '--user ', String, 'the user id to send the event as' - c.option '--anonymous ', String, 'the anonymous user id to send the event as' - c.option '--name ', String, 'the screen name' - c.option '--properties ', String, 'the event properties to send (JSON-encoded)' - c.option '--context ', 'additional context for the event (JSON-encoded)' - c.action do |args, options| - Analytics.identify({ - user_id: options.user, - anonymous_id: options.anonymous, - name: option.name, - traits: toObject(options.traits), - properties: toObject(option.properties), - context: toObject(options.context) - }) - Analytics.flush - end + c.option '--userId=', String, 'the user id to send the event as' + c.option '--anonymousId=', String, 'the anonymous user id to send the event as' + c.option '--context=', 'additional context for the event (JSON-encoded)' -end + c.option '--event=', String, 'the event name to send with the event' + c.option '--properties=', 'the event properties to send (JSON-encoded)' + c.option '--name=', 'name of the screen or page to send with the message' -# group -command :group do |c| - c.description = 'identify a group of users' - c.option '--user ', String, 'the user id to send the event as' - c.option '--anonymous ', String, 'the anonymous user id to send the event as' - c.option '--group ', String, 'the group id to associate this user with' - c.option '--traits ', String, 'attributes about the group (JSON-encoded)' - c.option '--context ', 'additional context for the event (JSON-encoded)' - c.action do |args, options| - Analytics.group({ - user_id: options.user, - anonymous_id: options.anonymous, - group_id: options.group, - traits: toObject(options.traits), - context: toObject(options.context) - }) - Analytics.flush - end + c.option '--traits=', 'the identify/group traits to send (JSON-encoded)' -end + c.option '--groupId=', String, 'the group id' -# alias -command :alias do |c| - c.description = 'remap a user to a new id' - c.option '--user ', String, 'the user id to send the event as' - c.option '--previous ', String, 'the previous user id (to add the alias for)' c.action do |args, options| - Analytics.alias({ - user_id: options.user, - previous_id: options.previous - }) + Analytics = Segment::Analytics.new({ + write_key: options.writeKey, + on_error: Proc.new { |status, msg| print msg } + }) + + case options.type + when "track" + Analytics.track({ + user_id: options.userId, + event: options.event, + anonymous_id: options.anonymousId, + properties: json_hash(options.properties), + context: json_hash(options.context) + }) + when "page" + Analytics.page({ + user_id: options.userId, + anonymous_id: options.anonymousId, + name: options.name, + properties: json_hash(options.properties), + context: json_hash(options.context) + }) + when "screen" + Analytics.screen({ + user_id: options.userId, + anonymous_id: options.anonymousId, + name: option.name, + traits: json_hash(options.traits), + properties: json_hash(option.properties) + }) + when "identify" + Analytics.identify({ + user_id: options.userId, + anonymous_id: options.anonymousId, + traits: json_hash(options.traits), + context: json_hash(options.context) + }) + when "group" + Analytics.group({ + user_id: options.userId, + anonymous_id: options.anonymousId, + group_id: options.groupId, + traits: json_hash(options.traits), + context: json_hash(options.context) + }) + else + raise "Invalid Message Type #{options.type}" + end Analytics.flush end - end - From c9c6a156d8f9a6e85362be5c93276f21bf08d570 Mon Sep 17 00:00:00 2001 From: Prateek Srivastava Date: Mon, 30 Apr 2018 16:30:29 -0700 Subject: [PATCH 054/179] Release 2.2.4 --- History.md | 7 ++++++- lib/segment/analytics/version.rb | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/History.md b/History.md index 29315a0..0797770 100644 --- a/History.md +++ b/History.md @@ -1,6 +1,11 @@ -2.2.4.pre / 2018-02-04 +2.2.4 / 2018-04-30 ================== + * Promote pre-release version to stable. + +2.2.4.pre / 2018-02-04 +====================== + * [Fix](https://github.com/segmentio/analytics-ruby/pull/147): Prevent 'batch size exceeded' errors by automatically batching items according to size diff --git a/lib/segment/analytics/version.rb b/lib/segment/analytics/version.rb index 8a55e5a..3fd4098 100644 --- a/lib/segment/analytics/version.rb +++ b/lib/segment/analytics/version.rb @@ -1,5 +1,5 @@ module Segment class Analytics - VERSION = '2.2.4.pre' + VERSION = '2.2.4' end end From dbc6ae08fc4410bcc3f01e05c6e3c3fb1c859a5b Mon Sep 17 00:00:00 2001 From: Tomasz Pajor Date: Tue, 1 May 2018 11:53:58 +0200 Subject: [PATCH 055/179] require version first as it is used in the defaults now --- lib/segment/analytics.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/segment/analytics.rb b/lib/segment/analytics.rb index 13ca1ec..c03f8cc 100644 --- a/lib/segment/analytics.rb +++ b/lib/segment/analytics.rb @@ -1,6 +1,6 @@ +require 'segment/analytics/version' require 'segment/analytics/defaults' require 'segment/analytics/utils' -require 'segment/analytics/version' require 'segment/analytics/client' require 'segment/analytics/worker' require 'segment/analytics/request' From 927e70c2d5e603f75a06e72ff782c3f43b7ff610 Mon Sep 17 00:00:00 2001 From: Prateek Srivastava Date: Tue, 1 May 2018 10:16:47 -0700 Subject: [PATCH 056/179] Release 2.2.5 --- History.md | 5 +++++ RELEASING.md | 2 +- lib/segment/analytics/version.rb | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/History.md b/History.md index 0797770..2c38a4b 100644 --- a/History.md +++ b/History.md @@ -1,3 +1,8 @@ +2.2.5 / 2018-05-01 +================== + + * [Fix](https://github.com/segmentio/analytics-ruby/pull/158): Require `version` module first. + 2.2.4 / 2018-04-30 ================== diff --git a/RELEASING.md b/RELEASING.md index 4c54840..2a60d9c 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -1,7 +1,7 @@ Releasing ========= - 1. Verify everything works with `make test build`. + 1. Verify everything works with `make check build`. 2. Bump version in [`version.rb`](https://github.com/segmentio/analytics-ruby/blob/master/lib/segment/analytics/version.rb). 3. Update [`History.md`](https://github.com/segmentio/analytics-ruby/blob/master/History.md). 4. Commit and tag `git commit -am "Release {version}" && git tag -a {version} -m "Version {version}"`. diff --git a/lib/segment/analytics/version.rb b/lib/segment/analytics/version.rb index 3fd4098..258a714 100644 --- a/lib/segment/analytics/version.rb +++ b/lib/segment/analytics/version.rb @@ -1,5 +1,5 @@ module Segment class Analytics - VERSION = '2.2.4' + VERSION = '2.2.5' end end From 293d224eee54326ebd2d614f290ca549f29375bf Mon Sep 17 00:00:00 2001 From: Paul Kuruvilla Date: Wed, 2 May 2018 02:25:44 +0530 Subject: [PATCH 057/179] Avoid rubygems race condition with parallel pushes Earlier, all our Travis build jobs (one for each ruby version) used to attempt to push a tagged version to Rubygems. The idea here was that one of these would succeed and all the other would fail saying 'repushing versions isn't allowed'. Turns out, there's a race condition in Rubygems in the controller we're hitting. Details: https://github.com/rubygems/rubygems.org/issues/1551 This commits instructs Travis to deploy only on a single build, so that we're very unlikely to trigger said race condition. --- .travis.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index f0571ec..d6fc9ca 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,7 +8,7 @@ rvm: - 2.2.9 - 2.3.6 - 2.4.3 - - 2.5.0 + - 2.5.0 # Performs deploys. Change condition below when changing this. script: make check @@ -18,5 +18,6 @@ deploy: provider: rubygems on: tags: true + condition: "$TRAVIS_RUBY_VERSION == 2.5.0" api_key: secure: Ceq6J4aBpsoqRfSiC7z+/J4moOXNjcPMFb2Bfm5qE51cIZzeyuOIOc6zhrad9tUgoX6uTRRxLxkybyu4wNYSluMA3IXW20CJyXZeJEHIaTYIDTWFAIYyerBJyMujJycSo7XueWb0faKBENrBQKx1K1tS0EiXpA2rMhdA6RM3DOY= From c135f87d46721eb1e8d44f70028642625692c2bf Mon Sep 17 00:00:00 2001 From: Paul Kuruvilla Date: Tue, 15 May 2018 10:22:48 +0530 Subject: [PATCH 058/179] Add missing forwardable requirement This is unlikely to occur on most setups because another gem must've 'required' forwardable already --- lib/segment/analytics/message_batch.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/segment/analytics/message_batch.rb b/lib/segment/analytics/message_batch.rb index bc44971..1864f16 100644 --- a/lib/segment/analytics/message_batch.rb +++ b/lib/segment/analytics/message_batch.rb @@ -1,3 +1,4 @@ +require 'forwardable' require 'segment/analytics/logging' module Segment From b55df2695b6f745cf126e5622a87503e67904da3 Mon Sep 17 00:00:00 2001 From: Paul Kuruvilla Date: Sat, 19 May 2018 16:09:29 +0530 Subject: [PATCH 059/179] Add [analytics-ruby] prefix to all log messages --- lib/segment/analytics/logging.rb | 41 ++++++++++++++++++++++++++------ 1 file changed, 34 insertions(+), 7 deletions(-) diff --git a/lib/segment/analytics/logging.rb b/lib/segment/analytics/logging.rb index 20b1a7f..f7aaa63 100644 --- a/lib/segment/analytics/logging.rb +++ b/lib/segment/analytics/logging.rb @@ -2,16 +2,43 @@ module Segment class Analytics + # Wraps an existing logger and adds a prefix to all messages + class PrefixedLogger + def initialize(logger, prefix) + @logger = logger + @prefix = prefix + end + + def debug(msg) + @logger.debug("#{@prefix} #{msg}") + end + + def info(msg) + @logger.info("#{@prefix} #{msg}") + end + + def warn(msg) + @logger.warn("#{@prefix} #{msg}") + end + + def error(msg) + @logger.error("#{@prefix} #{msg}") + end + end + module Logging class << self def logger - @logger ||= if defined?(Rails) - Rails.logger - else - logger = Logger.new STDOUT - logger.progname = 'Segment::Analytics' - logger - end + return @logger if @logger + + base_logger = if defined?(Rails) + Rails.logger + else + logger = Logger.new STDOUT + logger.progname = 'Segment::Analytics' + logger + end + @logger = PrefixedLogger.new(base_logger, '[analytics-ruby]') end attr_writer :logger From 4463ca5e82482e29b1e65557847b1d4d52a70da4 Mon Sep 17 00:00:00 2001 From: Paul Kuruvilla Date: Sat, 19 May 2018 16:12:45 +0530 Subject: [PATCH 060/179] Add debug log when retrying request --- lib/segment/analytics/request.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/segment/analytics/request.rb b/lib/segment/analytics/request.rb index 502d8e1..0cdf476 100644 --- a/lib/segment/analytics/request.rb +++ b/lib/segment/analytics/request.rb @@ -90,6 +90,7 @@ def retry_with_backoff(retries_remaining, &block) end if should_retry && (retries_remaining > 1) + logger.debug("Retrying request, #{retries_remaining} retries left") sleep(@backoff_policy.next_interval.to_f / 1000) retry_with_backoff(retries_remaining - 1, &block) else From c52c386a2b7631490c3c4d4e88d2ce4d7d61c891 Mon Sep 17 00:00:00 2001 From: Paul Kuruvilla Date: Sat, 19 May 2018 16:14:24 +0530 Subject: [PATCH 061/179] Add debug log for number of items sent in each call --- lib/segment/analytics/worker.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/segment/analytics/worker.rb b/lib/segment/analytics/worker.rb index f1c7ef4..7f1a286 100644 --- a/lib/segment/analytics/worker.rb +++ b/lib/segment/analytics/worker.rb @@ -9,6 +9,7 @@ class Analytics class Worker include Segment::Analytics::Utils include Segment::Analytics::Defaults + include Segment::Analytics::Logging # public: Creates a new worker # @@ -44,6 +45,7 @@ def run end end + logger.debug("Sending request for #{@batch.length} items") res = @request.post(@write_key, @batch) @on_error.call(res.status, res.error) unless res.status == 200 From 34cabffc0e80fb59b668df766736c27f69e5e3d4 Mon Sep 17 00:00:00 2001 From: Paul Kuruvilla Date: Wed, 23 May 2018 17:50:38 +0530 Subject: [PATCH 062/179] Add HTTP status code to debug logs --- lib/segment/analytics/request.rb | 4 ++++ lib/segment/analytics/worker.rb | 1 - 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/segment/analytics/request.rb b/lib/segment/analytics/request.rb index 0cdf476..f1a319b 100644 --- a/lib/segment/analytics/request.rb +++ b/lib/segment/analytics/request.rb @@ -38,10 +38,14 @@ def initialize(options = {}) # # returns - Response of the status and error if it exists def post(write_key, batch) + logger.debug("Sending request for #{batch.length} items") + last_response, exception = retry_with_backoff(@retries) do status_code, body = send_request(write_key, batch) error = JSON.parse(body)['error'] should_retry = should_retry_request?(status_code, body) + logger.debug("Response status code: #{status_code}") + logger.debug("Response error: #{error}") if error [Response.new(status_code, error), should_retry] end diff --git a/lib/segment/analytics/worker.rb b/lib/segment/analytics/worker.rb index 7f1a286..fd47703 100644 --- a/lib/segment/analytics/worker.rb +++ b/lib/segment/analytics/worker.rb @@ -45,7 +45,6 @@ def run end end - logger.debug("Sending request for #{@batch.length} items") res = @request.post(@write_key, @batch) @on_error.call(res.status, res.error) unless res.status == 200 From aa7aa8803ef358ee9a5579e1abf2984c39db8aca Mon Sep 17 00:00:00 2001 From: Paul Kuruvilla Date: Tue, 12 Jun 2018 17:20:06 +0530 Subject: [PATCH 063/179] Add tests for oj/rails conflict --- Rakefile | 12 +++++++++++- analytics-ruby.gemspec | 1 + spec/isolated/json_example.rb | 9 +++++++++ spec/isolated/with_active_support.rb | 11 +++++++++++ spec/isolated/with_active_support_and_oj.rb | 14 ++++++++++++++ spec/isolated/with_oj.rb | 11 +++++++++++ 6 files changed, 57 insertions(+), 1 deletion(-) create mode 100644 spec/isolated/json_example.rb create mode 100644 spec/isolated/with_active_support.rb create mode 100644 spec/isolated/with_active_support_and_oj.rb create mode 100644 spec/isolated/with_oj.rb diff --git a/Rakefile b/Rakefile index 626fcbe..838c39f 100644 --- a/Rakefile +++ b/Rakefile @@ -3,12 +3,22 @@ require 'rspec/core/rake_task' default_tasks = [] RSpec::Core::RakeTask.new(:spec) do |spec| - spec.pattern = 'spec/**/*_spec.rb' + spec.pattern = 'spec/segment/**/*_spec.rb' spec.rspec_opts = "--tag ~e2e" if ENV["RUN_E2E_TESTS"] != "true" end default_tasks << :spec +# Isolated tests are run as separate rake tasks so that gem conflicts can be +# tests in different processes +Dir.glob('spec/isolated/**/*.rb').each do |isolated_test_path| + RSpec::Core::RakeTask.new(isolated_test_path) do |spec| + spec.pattern = isolated_test_path + end + + default_tasks << isolated_test_path +end + # Rubocop doesn't support < 2.1 if RUBY_VERSION >= "2.1" require 'rubocop/rake_task' diff --git a/analytics-ruby.gemspec b/analytics-ruby.gemspec index 1670cf2..581f552 100644 --- a/analytics-ruby.gemspec +++ b/analytics-ruby.gemspec @@ -24,6 +24,7 @@ Gem::Specification.new do |spec| spec.add_development_dependency 'activesupport', '~> 4.1.11' spec.add_development_dependency 'faraday', '~> 0.13' spec.add_development_dependency 'pmap', '~> 1.1' + spec.add_development_dependency 'oj', '~> 3.6.2' if RUBY_VERSION >= "2.1" spec.add_development_dependency 'rubocop', '~> 0.51.0' diff --git a/spec/isolated/json_example.rb b/spec/isolated/json_example.rb new file mode 100644 index 0000000..9392e9a --- /dev/null +++ b/spec/isolated/json_example.rb @@ -0,0 +1,9 @@ +RSpec.shared_examples 'message_batch_json' do + it 'MessageBatch generates proper JSON' do + batch = Segment::Analytics::MessageBatch.new(100) + batch << Segment::Analytics::Message.new('a' => 'b') + batch << Segment::Analytics::Message.new('c' => 'd') + + expect(JSON.generate(batch)).to eq('[{"a":"b"},{"c":"d"}]') + end +end diff --git a/spec/isolated/with_active_support.rb b/spec/isolated/with_active_support.rb new file mode 100644 index 0000000..86178ae --- /dev/null +++ b/spec/isolated/with_active_support.rb @@ -0,0 +1,11 @@ +require 'spec_helper' +require 'isolated/json_example' + +describe 'with active_support' do + before do + require 'active_support' + require 'active_support/json' + end + + include_examples 'message_batch_json' +end diff --git a/spec/isolated/with_active_support_and_oj.rb b/spec/isolated/with_active_support_and_oj.rb new file mode 100644 index 0000000..92d9a74 --- /dev/null +++ b/spec/isolated/with_active_support_and_oj.rb @@ -0,0 +1,14 @@ +require 'spec_helper' +require 'isolated/json_example' + +describe 'with active_support and oj' do + before do + require 'active_support' + require 'active_support/json' + + require 'oj' + Oj.mimic_JSON + end + + include_examples 'message_batch_json' +end diff --git a/spec/isolated/with_oj.rb b/spec/isolated/with_oj.rb new file mode 100644 index 0000000..56b79ae --- /dev/null +++ b/spec/isolated/with_oj.rb @@ -0,0 +1,11 @@ +require 'spec_helper' +require 'isolated/json_example' + +describe 'with oj' do + before do + require 'oj' + Oj.mimic_JSON + end + + include_examples 'message_batch_json' +end From f860f27730de878c227dda9669a5bbca7c3ce372 Mon Sep 17 00:00:00 2001 From: Paul Kuruvilla Date: Wed, 13 Jun 2018 18:11:58 +0530 Subject: [PATCH 064/179] Remove intermediary message class --- lib/segment/analytics/message.rb | 26 --------------- lib/segment/analytics/message_batch.rb | 10 ++++-- lib/segment/analytics/worker.rb | 5 +-- spec/isolated/json_example.rb | 4 +-- spec/segment/analytics/message_batch_spec.rb | 17 +++++----- spec/segment/analytics/message_spec.rb | 35 -------------------- 6 files changed, 20 insertions(+), 77 deletions(-) delete mode 100644 lib/segment/analytics/message.rb delete mode 100644 spec/segment/analytics/message_spec.rb diff --git a/lib/segment/analytics/message.rb b/lib/segment/analytics/message.rb deleted file mode 100644 index e00a74d..0000000 --- a/lib/segment/analytics/message.rb +++ /dev/null @@ -1,26 +0,0 @@ -require 'segment/analytics/defaults' - -module Segment - class Analytics - # Represents a message to be sent to the API - class Message - def initialize(hash) - @hash = hash - end - - def too_big? - json_size > Defaults::Message::MAX_BYTES - end - - def json_size - to_json.bytesize - end - - # Since the hash is expected to not be modified (set at initialization), - # the JSON version can be cached after the first computation. - def to_json(*args) - @json ||= @hash.to_json(*args) - end - end - end -end diff --git a/lib/segment/analytics/message_batch.rb b/lib/segment/analytics/message_batch.rb index 1864f16..2e9385d 100644 --- a/lib/segment/analytics/message_batch.rb +++ b/lib/segment/analytics/message_batch.rb @@ -16,11 +16,13 @@ def initialize(max_message_count) end def <<(message) - if message.too_big? + message_json_size = message.to_json.bytesize + + if message_too_big?(message_json_size) logger.error('a message exceeded the maximum allowed size') else @messages << message - @json_size += message.json_size + 1 # One byte for the comma + @json_size += message_json_size + 1 # One byte for the comma end end @@ -43,6 +45,10 @@ def item_count_exhausted? @messages.length >= @max_message_count end + def message_too_big?(message_json_size) + message_json_size > Defaults::Message::MAX_BYTES + end + # We consider the max size here as just enough to leave room for one more # message of the largest size possible. This is a shortcut that allows us # to use a native Ruby `Queue` that doesn't allow peeking. The tradeoff diff --git a/lib/segment/analytics/worker.rb b/lib/segment/analytics/worker.rb index fd47703..8652a53 100644 --- a/lib/segment/analytics/worker.rb +++ b/lib/segment/analytics/worker.rb @@ -1,5 +1,4 @@ require 'segment/analytics/defaults' -require 'segment/analytics/message' require 'segment/analytics/message_batch' require 'segment/analytics/request' require 'segment/analytics/utils' @@ -40,9 +39,7 @@ def run return if @queue.empty? @lock.synchronize do - until @batch.full? || @queue.empty? - @batch << Message.new(@queue.pop) - end + @batch << @queue.pop until @batch.full? || @queue.empty? end res = @request.post(@write_key, @batch) diff --git a/spec/isolated/json_example.rb b/spec/isolated/json_example.rb index 9392e9a..c693b55 100644 --- a/spec/isolated/json_example.rb +++ b/spec/isolated/json_example.rb @@ -1,8 +1,8 @@ RSpec.shared_examples 'message_batch_json' do it 'MessageBatch generates proper JSON' do batch = Segment::Analytics::MessageBatch.new(100) - batch << Segment::Analytics::Message.new('a' => 'b') - batch << Segment::Analytics::Message.new('c' => 'd') + batch << { 'a' => 'b' } + batch << { 'c' => 'd' } expect(JSON.generate(batch)).to eq('[{"a":"b"},{"c":"d"}]') end diff --git a/spec/segment/analytics/message_batch_spec.rb b/spec/segment/analytics/message_batch_spec.rb index 6e014c8..4b7b66f 100644 --- a/spec/segment/analytics/message_batch_spec.rb +++ b/spec/segment/analytics/message_batch_spec.rb @@ -7,14 +7,13 @@ class Analytics describe '#<<' do it 'appends messages' do - subject << Message.new('a' => 'b') + subject << { 'a' => 'b' } expect(subject.length).to eq(1) end it 'rejects messages that exceed the maximum allowed size' do max_bytes = Defaults::Message::MAX_BYTES - hash = { 'a' => 'b' * max_bytes } - message = Message.new(hash) + message = { 'a' => 'b' * max_bytes } subject << message expect(subject.length).to eq(0) @@ -23,21 +22,23 @@ class Analytics describe '#full?' do it 'returns true once item count is exceeded' do - 99.times { subject << Message.new(a: 'b') } + 99.times { subject << { a: 'b' } } expect(subject.full?).to be(false) - subject << Message.new(a: 'b') + subject << { a: 'b' } expect(subject.full?).to be(true) end it 'returns true once max size is almost exceeded' do - message = Message.new(a: 'b' * (Defaults::Message::MAX_BYTES - 10)) + message = { a: 'b' * (Defaults::Message::MAX_BYTES - 10) } + + message_size = message.to_json.bytesize # Each message is under the individual limit - expect(message.json_size).to be < Defaults::Message::MAX_BYTES + expect(message_size).to be < Defaults::Message::MAX_BYTES # Size of the batch is over the limit - expect(50 * message.json_size).to be > Defaults::MessageBatch::MAX_BYTES + expect(50 * message_size).to be > Defaults::MessageBatch::MAX_BYTES expect(subject.full?).to be(false) 50.times { subject << message } diff --git a/spec/segment/analytics/message_spec.rb b/spec/segment/analytics/message_spec.rb deleted file mode 100644 index b156183..0000000 --- a/spec/segment/analytics/message_spec.rb +++ /dev/null @@ -1,35 +0,0 @@ -require 'spec_helper' - -module Segment - class Analytics - describe Message do - describe '#to_json' do - it 'caches JSON conversions' do - # Keeps track of the number of times to_json was called - nested_obj = Class.new do - attr_reader :to_json_call_count - - def initialize - @to_json_call_count = 0 - end - - def to_json(*_) - @to_json_call_count += 1 - '{}' - end - end.new - - message = Message.new('some_key' => nested_obj) - expect(nested_obj.to_json_call_count).to eq(0) - - message.to_json - expect(nested_obj.to_json_call_count).to eq(1) - - # When called a second time, the call count shouldn't increase - message.to_json - expect(nested_obj.to_json_call_count).to eq(1) - end - end - end - end -end From 220011d8f6daf4781bc75a2546c61481a6d182d3 Mon Sep 17 00:00:00 2001 From: Paul Kuruvilla Date: Wed, 13 Jun 2018 18:32:30 +0530 Subject: [PATCH 065/179] Only install oj if ruby is > 2.0 and not jruby --- analytics-ruby.gemspec | 5 ++++- spec/isolated/with_active_support_and_oj.rb | 18 ++++++++++-------- spec/isolated/with_oj.rb | 14 ++++++++------ 3 files changed, 22 insertions(+), 15 deletions(-) diff --git a/analytics-ruby.gemspec b/analytics-ruby.gemspec index 581f552..4edbfe9 100644 --- a/analytics-ruby.gemspec +++ b/analytics-ruby.gemspec @@ -24,7 +24,10 @@ Gem::Specification.new do |spec| spec.add_development_dependency 'activesupport', '~> 4.1.11' spec.add_development_dependency 'faraday', '~> 0.13' spec.add_development_dependency 'pmap', '~> 1.1' - spec.add_development_dependency 'oj', '~> 3.6.2' + + if RUBY_VERSION >= '2.0' && RUBY_PLATFORM != 'java' + spec.add_development_dependency 'oj', '~> 3.6.2' + end if RUBY_VERSION >= "2.1" spec.add_development_dependency 'rubocop', '~> 0.51.0' diff --git a/spec/isolated/with_active_support_and_oj.rb b/spec/isolated/with_active_support_and_oj.rb index 92d9a74..18724e3 100644 --- a/spec/isolated/with_active_support_and_oj.rb +++ b/spec/isolated/with_active_support_and_oj.rb @@ -1,14 +1,16 @@ require 'spec_helper' require 'isolated/json_example' -describe 'with active_support and oj' do - before do - require 'active_support' - require 'active_support/json' +if RUBY_VERSION >= '2.0' && RUBY_PLATFORM != 'java' + describe 'with active_support and oj' do + before do + require 'active_support' + require 'active_support/json' - require 'oj' - Oj.mimic_JSON - end + require 'oj' + Oj.mimic_JSON + end - include_examples 'message_batch_json' + include_examples 'message_batch_json' + end end diff --git a/spec/isolated/with_oj.rb b/spec/isolated/with_oj.rb index 56b79ae..ad3f376 100644 --- a/spec/isolated/with_oj.rb +++ b/spec/isolated/with_oj.rb @@ -1,11 +1,13 @@ require 'spec_helper' require 'isolated/json_example' -describe 'with oj' do - before do - require 'oj' - Oj.mimic_JSON - end +if RUBY_VERSION >= '2.0' && RUBY_PLATFORM != 'java' + describe 'with oj' do + before do + require 'oj' + Oj.mimic_JSON + end - include_examples 'message_batch_json' + include_examples 'message_batch_json' + end end From 494b3ff8ddf9b54df32bb75d08a948d79ab33de0 Mon Sep 17 00:00:00 2001 From: Paul Kuruvilla Date: Mon, 25 Jun 2018 13:48:34 +0530 Subject: [PATCH 066/179] Revert "Reuse TCP connections" Ref: https://github.com/segmentio/analytics-ruby/issues/167 This reverts commit 94179b8ae9b7686de462418b6a4f8c590d8902cf. --- lib/segment/analytics/request.rb | 5 ----- lib/segment/analytics/worker.rb | 4 ++-- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/lib/segment/analytics/request.rb b/lib/segment/analytics/request.rb index f1a319b..d65e86e 100644 --- a/lib/segment/analytics/request.rb +++ b/lib/segment/analytics/request.rb @@ -117,11 +117,6 @@ def send_request(write_key, batch) [200, '{}'] else - # If `start` is not called, Ruby adds a 'Connection: close' header to - # all requests, preventing us from reusing a connection for multiple - # HTTP requests - @http.start unless @http.started? - response = @http.request(request, payload) [response.code.to_i, response.body] end diff --git a/lib/segment/analytics/worker.rb b/lib/segment/analytics/worker.rb index fd47703..eed6beb 100644 --- a/lib/segment/analytics/worker.rb +++ b/lib/segment/analytics/worker.rb @@ -30,7 +30,6 @@ def initialize(queue, write_key, options = {}) batch_size = options[:batch_size] || Defaults::MessageBatch::MAX_SIZE @batch = MessageBatch.new(batch_size) @lock = Mutex.new - @request = Request.new end # public: Continuously runs the loop to check for new events @@ -45,7 +44,8 @@ def run end end - res = @request.post(@write_key, @batch) + res = Request.new.post @write_key, @batch + @on_error.call(res.status, res.error) unless res.status == 200 @lock.synchronize { @batch.clear } From 4b9d328fc8f3989401a3df60ae0e068ea39fd4e5 Mon Sep 17 00:00:00 2001 From: Paul Kuruvilla Date: Wed, 27 Jun 2018 20:09:53 +0530 Subject: [PATCH 067/179] Prep for 2.2.6 release --- History.md | 12 ++++++++++++ lib/segment/analytics/version.rb | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/History.md b/History.md index 2c38a4b..c7d3e91 100644 --- a/History.md +++ b/History.md @@ -1,3 +1,15 @@ +2.2.6.pre / 2018-06-27 +================== + + * [Fix](https://github.com/segmentio/analytics-ruby/pull/168): Revert 'reuse + TCP connections' to fix EMFILE errors + * [Fix](https://github.com/segmentio/analytics-ruby/pull/166): Fix oj/rails + conflict + * [Fix](https://github.com/segmentio/analytics-ruby/pull/162): Add missing + 'Forwardable' requirement + * [Improvement](https://github.com/segmentio/analytics-ruby/pull/163): Better + logging + 2.2.5 / 2018-05-01 ================== diff --git a/lib/segment/analytics/version.rb b/lib/segment/analytics/version.rb index 258a714..7d4028f 100644 --- a/lib/segment/analytics/version.rb +++ b/lib/segment/analytics/version.rb @@ -1,5 +1,5 @@ module Segment class Analytics - VERSION = '2.2.5' + VERSION = '2.2.6.pre' end end From b5c709daed449aef6a5a8dcecd851dd4e0707e3e Mon Sep 17 00:00:00 2001 From: Paul Kuruvilla Date: Wed, 27 Jun 2018 20:13:58 +0530 Subject: [PATCH 068/179] Disable e2e tests for a month They're failing because the runscope inspector shut down. --- spec/segment/analytics/e2e_spec.rb | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/spec/segment/analytics/e2e_spec.rb b/spec/segment/analytics/e2e_spec.rb index e528d59..66a5b79 100644 --- a/spec/segment/analytics/e2e_spec.rb +++ b/spec/segment/analytics/e2e_spec.rb @@ -1,3 +1,5 @@ +require 'date' + require 'spec_helper' module Segment @@ -19,6 +21,10 @@ module Segment let(:runscope_client) { RunscopeClient.new(ENV.fetch('RUNSCOPE_TOKEN')) } it 'tracks events' do + # Runscope inspector has shut down, disable for a while until we've + # found a replacement. + skip if Date.today < Date.new(2018, 7, 27) + id = SecureRandom.uuid client.track( user_id: 'dummy_user_id', From f1d8b68ccd26b02ba382da8981caa1e28e9ebf8f Mon Sep 17 00:00:00 2001 From: Paul Kuruvilla Date: Wed, 11 Jul 2018 00:13:25 +0530 Subject: [PATCH 069/179] Add note about oj/rails conflict to changelog --- History.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/History.md b/History.md index c7d3e91..bed1b48 100644 --- a/History.md +++ b/History.md @@ -1,3 +1,7 @@ +Note: There is a known issue when using `2.2.5` with both `oj` and `rails` gems +installed. Please test out `2.2.6.pre` and hold off on using `2.2.5`. +[Details](https://github.com/segmentio/analytics-ruby/pull/166) + 2.2.6.pre / 2018-06-27 ================== From 60a1dfe425fbe2776cb3e498843bbf02b15bc1c0 Mon Sep 17 00:00:00 2001 From: Prateek Srivastava Date: Mon, 27 Aug 2018 14:06:36 -0700 Subject: [PATCH 070/179] Update end to end tests Use the testing harness that uses schema webhook instead of runscope. Other relevant changes: - Moved e2e tests from Travis.yml to Makefile - Refined list of files to include in gems, so that other files in directory don't cause installation problems (like .gem files) - Remove old e2e only code. --- .buildscript/e2e.sh | 13 +++++++ .travis.yml | 3 +- Makefile | 7 +++- Rakefile | 1 - analytics-ruby.gemspec | 4 +-- spec/helpers/runscope_client.rb | 38 --------------------- spec/segment/analytics/e2e_spec.rb | 54 ------------------------------ spec/spec_helper.rb | 1 - 8 files changed, 22 insertions(+), 99 deletions(-) create mode 100755 .buildscript/e2e.sh delete mode 100644 spec/helpers/runscope_client.rb delete mode 100644 spec/segment/analytics/e2e_spec.rb diff --git a/.buildscript/e2e.sh b/.buildscript/e2e.sh new file mode 100755 index 0000000..ed25afe --- /dev/null +++ b/.buildscript/e2e.sh @@ -0,0 +1,13 @@ +#!/bin/sh + +set -ex + +if [ "$RUN_E2E_TESTS" != "true" ]; then + echo "Skipping end to end tests." +else + echo "Running end to end tests..." + wget https://github.com/segmentio/library-e2e-tester/releases/download/0.2.1/tester_linux_amd64 -O tester + chmod +x tester + ./tester -path='./bin/analytics' + echo "End to end tests completed!" +fi diff --git a/.travis.yml b/.travis.yml index d6fc9ca..5cc960c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,7 +10,8 @@ rvm: - 2.4.3 - 2.5.0 # Performs deploys. Change condition below when changing this. -script: make check +script: + - make check # Deploy tagged commits to Rubygems # See https://docs.travis-ci.com/user/deployment/rubygems/ for more details diff --git a/Makefile b/Makefile index a027075..44b6c43 100644 --- a/Makefile +++ b/Makefile @@ -9,9 +9,14 @@ dependencies: bundle install --verbose # Run all tests and checks (including linters). -check: +check: install # Installation required for testing binary bundle exec rake + sh .buildscript/e2e.sh # Compile the code and produce any binaries where applicable. build: + rm -f analytics-ruby-*.gem gem build ./analytics-ruby.gemspec + +install: build + gem install analytics-ruby-*.gem diff --git a/Rakefile b/Rakefile index 838c39f..0d3f11a 100644 --- a/Rakefile +++ b/Rakefile @@ -4,7 +4,6 @@ default_tasks = [] RSpec::Core::RakeTask.new(:spec) do |spec| spec.pattern = 'spec/segment/**/*_spec.rb' - spec.rspec_opts = "--tag ~e2e" if ENV["RUN_E2E_TESTS"] != "true" end default_tasks << :spec diff --git a/analytics-ruby.gemspec b/analytics-ruby.gemspec index 4edbfe9..e1934c6 100644 --- a/analytics-ruby.gemspec +++ b/analytics-ruby.gemspec @@ -3,7 +3,7 @@ require File.expand_path('../lib/segment/analytics/version', __FILE__) Gem::Specification.new do |spec| spec.name = 'analytics-ruby' spec.version = Segment::Analytics::VERSION - spec.files = Dir.glob('**/*') + spec.files = Dir.glob("{lib,bin}/**/*") spec.require_paths = ['lib'] spec.bindir = 'bin' spec.executables = ['analytics'] @@ -22,8 +22,6 @@ Gem::Specification.new do |spec| spec.add_development_dependency 'rspec', '~> 3.0' spec.add_development_dependency 'tzinfo', '1.2.1' spec.add_development_dependency 'activesupport', '~> 4.1.11' - spec.add_development_dependency 'faraday', '~> 0.13' - spec.add_development_dependency 'pmap', '~> 1.1' if RUBY_VERSION >= '2.0' && RUBY_PLATFORM != 'java' spec.add_development_dependency 'oj', '~> 3.6.2' diff --git a/spec/helpers/runscope_client.rb b/spec/helpers/runscope_client.rb deleted file mode 100644 index a68be26..0000000 --- a/spec/helpers/runscope_client.rb +++ /dev/null @@ -1,38 +0,0 @@ -require 'faraday' -require 'pmap' - -class RunscopeClient - def initialize(api_token) - headers = { 'Authorization' => "Bearer #{api_token}" } - @conn = Faraday.new('https://api.runscope.com', headers: headers) - end - - def requests(bucket_key) - with_retries(3) do - response = @conn.get("/buckets/#{bucket_key}/messages", count: 20) - - raise "Runscope error. #{response.body}" unless response.status == 200 - - message_uuids = JSON.parse(response.body)['data'].map { |message| - message.fetch('uuid') - } - - message_uuids.pmap { |uuid| - response = @conn.get("/buckets/#{bucket_key}/messages/#{uuid}") - raise "Runscope error. #{response.body}" unless response.status == 200 - JSON.parse(response.body).fetch('data').fetch('request') - } - end - end - - private - - def with_retries(max_retries) - retries ||= 0 - yield - rescue StandardError => e - retries += 1 - retry if retries < max_retries - raise e - end -end diff --git a/spec/segment/analytics/e2e_spec.rb b/spec/segment/analytics/e2e_spec.rb deleted file mode 100644 index 66a5b79..0000000 --- a/spec/segment/analytics/e2e_spec.rb +++ /dev/null @@ -1,54 +0,0 @@ -require 'date' - -require 'spec_helper' - -module Segment - # End-to-end tests that send events to a segment source and verifies that a - # webhook connected to the source (configured manually via the app) is able - # to receive the data sent by this library. - describe 'End-to-end tests', e2e: true do - # Segment write key for - # https://app.segment.com/segment-libraries/sources/analytics_ruby_e2e_test/overview. - # - # This source is configured to send events to the Runscope bucket used by - # this test. - WRITE_KEY = 'qhdMksLsQTi9MES3CHyzsWRRt4ub5VM6' - - # Runscope bucket key for https://www.runscope.com/stream/umkvkgv7ndby - RUNSCOPE_BUCKET_KEY = 'umkvkgv7ndby' - - let(:client) { Segment::Analytics.new(write_key: WRITE_KEY) } - let(:runscope_client) { RunscopeClient.new(ENV.fetch('RUNSCOPE_TOKEN')) } - - it 'tracks events' do - # Runscope inspector has shut down, disable for a while until we've - # found a replacement. - skip if Date.today < Date.new(2018, 7, 27) - - id = SecureRandom.uuid - client.track( - user_id: 'dummy_user_id', - event: 'E2E Test', - properties: { id: id } - ) - client.flush - - # Allow events to propagate to runscope - eventually(timeout: 30) { - expect(has_matching_request?(id)).to eq(true) - } - end - - def has_matching_request?(id) - captured_requests = runscope_client.requests(RUNSCOPE_BUCKET_KEY) - captured_requests.any? do |request| - begin - body = JSON.parse(request['body']) - body['properties'] && body['properties']['id'] == id - rescue JSON::ParserError - false - end - end - end - end -end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 7cd6c2f..8bc41b0 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -6,7 +6,6 @@ require 'segment/analytics' require 'active_support/time' -require './spec/helpers/runscope_client' # Setting timezone for ActiveSupport::TimeWithZone to UTC Time.zone = 'UTC' From 8837903a3397db9404db4a28cfd6909aaf7e0282 Mon Sep 17 00:00:00 2001 From: Paul Kuruvilla Date: Sat, 20 Oct 2018 12:08:03 +0530 Subject: [PATCH 071/179] Move commander to development_dependencies, fixes #176 --- analytics-ruby.gemspec | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/analytics-ruby.gemspec b/analytics-ruby.gemspec index e1934c6..ba0c3fe 100644 --- a/analytics-ruby.gemspec +++ b/analytics-ruby.gemspec @@ -16,20 +16,20 @@ Gem::Specification.new do |spec| # Ruby 1.8 requires json spec.add_dependency 'json', ['~> 1.7'] if RUBY_VERSION < "1.9" - spec.add_dependency 'commander', '~> 4.4' + # Used in the executable testing script + spec.add_development_dependency 'commander', '~> 4.4' + + # Used in specs spec.add_development_dependency 'rake', '~> 10.3' spec.add_development_dependency 'rspec', '~> 3.0' spec.add_development_dependency 'tzinfo', '1.2.1' spec.add_development_dependency 'activesupport', '~> 4.1.11' - if RUBY_VERSION >= '2.0' && RUBY_PLATFORM != 'java' spec.add_development_dependency 'oj', '~> 3.6.2' end - - if RUBY_VERSION >= "2.1" + if RUBY_VERSION >= '2.1' spec.add_development_dependency 'rubocop', '~> 0.51.0' end - spec.add_development_dependency 'codecov', '~> 0.1.4' end From ddf7a20b6b793b1f632a3610af6b42d066b0376f Mon Sep 17 00:00:00 2001 From: William Grosset Date: Thu, 28 Feb 2019 20:47:16 -0800 Subject: [PATCH 072/179] Update README --- README.md | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/README.md b/README.md index e611131..abfd092 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,48 @@ analytics-ruby analytics-ruby is a ruby client for [Segment](https://segment.com) +
+ +

You can't fix what you can't measure

+
+ +Analytics helps you measure your users, product, and business. It unlocks insights into your app's funnel, core business metrics, and whether you have product-market fit. + +## How to get started +1. **Collect analytics data** from your app(s). + - The top 200 Segment companies collect data from 5+ source types (web, mobile, server, CRM, etc.). +2. **Send the data to analytics tools** (for example, Google Analytics, Amplitude, Mixpanel). + - Over 250+ Segment companies send data to eight categories of destinations such as analytics tools, warehouses, email marketing and remarketing systems, session recording, and more. +3. **Explore your data** by creating metrics (for example, new signups, retention cohorts, and revenue generation). + - The best Segment companies use retention cohorts to measure product market fit. Netflix has 70% paid retention after 12 months, 30% after 7 years. + +[Segment](https://segment.com) collects analytics data and allows you to send it to more than 250 apps (such as Google Analytics, Mixpanel, Optimizely, Facebook Ads, Slack, Sentry) just by flipping a switch. You only need one Segment code snippet, and you can turn integrations on and off at will, with no additional code. [Sign up with Segment today](https://app.segment.com/signup). + +### Why? +1. **Power all your analytics apps with the same data**. Instead of writing code to integrate all of your tools individually, send data to Segment, once. + +2. **Install tracking for the last time**. We're the last integration you'll ever need to write. You only need to instrument Segment once. Reduce all of your tracking code and advertising tags into a single set of API calls. + +3. **Send data from anywhere**. Send Segment data from any device, and we'll transform and send it on to any tool. + +4. **Query your data in SQL**. Slice, dice, and analyze your data in detail with Segment SQL. We'll transform and load your customer behavioral data directly from your apps into Amazon Redshift, Google BigQuery, or Postgres. Save weeks of engineering time by not having to invent your own data warehouse and ETL pipeline. + + For example, you can capture data on any app: + ```js + analytics.track('Order Completed', { price: 99.84 }) + ``` + Then, query the resulting data in SQL: + ```sql + select * from app.order_completed + order by price desc + ``` + +### 🚀 Startup Program +
+ +
+If you are part of a new startup (<$5M raised, <2 years since founding), we just launched a new startup program for you. You can get a Segment Team plan (up to $25,000 value in Segment credits) for free up to 2 years — apply here! + ## Install Into Gemfile from rubygems.org: From 3b34da1af7430ae703aa1b687c7e30b0b8e1ffcf Mon Sep 17 00:00:00 2001 From: William Grosset Date: Sun, 3 Mar 2019 19:24:31 -0800 Subject: [PATCH 073/179] Remove build status from README --- README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.md b/README.md index abfd092..2c44245 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,6 @@ analytics-ruby ============== -[![Build Status](https://travis-ci.org/segmentio/analytics-ruby.png?branch=master)](https://travis-ci.org/segmentio/analytics-ruby) - analytics-ruby is a ruby client for [Segment](https://segment.com)
From 7d42b0dc048db5955b5e33ddb0afe1cebb7c4a73 Mon Sep 17 00:00:00 2001 From: Paul Kuruvilla Date: Sat, 30 Mar 2019 14:27:46 +0530 Subject: [PATCH 074/179] Refine DateTime tests to use iso8601 helpers --- spec/segment/analytics/client_spec.rb | 47 +++++++++++++++++---------- 1 file changed, 30 insertions(+), 17 deletions(-) diff --git a/spec/segment/analytics/client_spec.rb b/spec/segment/analytics/client_spec.rb index f8a52e3..178eb13 100644 --- a/spec/segment/analytics/client_spec.rb +++ b/spec/segment/analytics/client_spec.rb @@ -76,25 +76,30 @@ class Analytics end.to_not raise_error end - it 'converts time and date traits into iso8601 format' do + it 'converts time and date properties into iso8601 format' do client.track({ :user_id => 'user', :event => 'Event', :properties => { :time => Time.utc(2013), - :time_with_zone => Time.zone.parse('2013-01-01'), + :time_with_zone => Time.zone.parse('2013-01-01'), :date_time => DateTime.new(2013, 1, 1), :date => Date.new(2013, 1, 1), :nottime => 'x' } }) - message = queue.pop + message = queue.pop properties = message[:properties] - expect(properties[:time]).to eq('2013-01-01T00:00:00.000Z') - expect(properties[:time_with_zone]).to eq('2013-01-01T00:00:00.000Z') - expect(properties[:date_time]).to eq('2013-01-01T00:00:00.000+00:00') - expect(properties[:date]).to eq('2013-01-01') + + date_time = DateTime.new(2013, 1, 1) + expect(Time.iso8601(properties[:time])).to eq(date_time) + expect(Time.iso8601(properties[:time_with_zone])).to eq(date_time) + expect(Time.iso8601(properties[:date_time])).to eq(date_time) + + date = Date.new(2013, 1, 1) + expect(Date.iso8601(properties[:date])).to eq(date) + expect(properties[:nottime]).to eq('x') end end @@ -131,12 +136,16 @@ class Analytics }) message = queue.pop - traits = message[:traits] - expect(traits[:time]).to eq('2013-01-01T00:00:00.000Z') - expect(traits[:time_with_zone]).to eq('2013-01-01T00:00:00.000Z') - expect(traits[:date_time]).to eq('2013-01-01T00:00:00.000+00:00') - expect(traits[:date]).to eq('2013-01-01') + + date_time = DateTime.new(2013, 1, 1) + expect(Time.iso8601(traits[:time])).to eq(date_time) + expect(Time.iso8601(traits[:time_with_zone])).to eq(date_time) + expect(Time.iso8601(traits[:date_time])).to eq(date_time) + + date = Date.new(2013, 1, 1) + expect(Date.iso8601(traits[:date])).to eq(date) + expect(traits[:nottime]).to eq('x') end end @@ -192,12 +201,16 @@ class Analytics }) message = queue.pop - traits = message[:traits] - expect(traits[:time]).to eq('2013-01-01T00:00:00.000Z') - expect(traits[:time_with_zone]).to eq('2013-01-01T00:00:00.000Z') - expect(traits[:date_time]).to eq('2013-01-01T00:00:00.000+00:00') - expect(traits[:date]).to eq('2013-01-01') + + date_time = DateTime.new(2013, 1, 1) + expect(Time.iso8601(traits[:time])).to eq(date_time) + expect(Time.iso8601(traits[:time_with_zone])).to eq(date_time) + expect(Time.iso8601(traits[:date_time])).to eq(date_time) + + date = Date.new(2013, 1, 1) + expect(Date.iso8601(traits[:date])).to eq(date) + expect(traits[:nottime]).to eq('x') end end From 070fefcf7be6f32a4ed086db98488c97bb750092 Mon Sep 17 00:00:00 2001 From: Paul Kuruvilla Date: Sat, 30 Mar 2019 14:57:46 +0530 Subject: [PATCH 075/179] Remove #sleep calls in tests --- spec/segment/analytics_spec.rb | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/spec/segment/analytics_spec.rb b/spec/segment/analytics_spec.rb index bc06703..fcf5f51 100644 --- a/spec/segment/analytics_spec.rb +++ b/spec/segment/analytics_spec.rb @@ -17,7 +17,7 @@ class Analytics it 'does not error with the required options' do expect do analytics.track Queued::TRACK - sleep(1) + analytics.flush end.to_not raise_error end end @@ -29,7 +29,7 @@ class Analytics it 'does not error with the required options' do analytics.identify Queued::IDENTIFY - sleep(1) + analytics.flush end end @@ -45,7 +45,7 @@ class Analytics it 'does not error with the required options' do expect do analytics.alias ALIAS - sleep(1) + analytics.flush end.to_not raise_error end end @@ -62,7 +62,7 @@ class Analytics it 'does not error with the required options' do expect do analytics.group Queued::GROUP - sleep(1) + analytics.flush end.to_not raise_error end end @@ -75,7 +75,7 @@ class Analytics it 'does not error with the required options' do expect do analytics.page Queued::PAGE - sleep(1) + analytics.flush end.to_not raise_error end end @@ -88,7 +88,7 @@ class Analytics it 'does not error with the required options' do expect do analytics.screen Queued::SCREEN - sleep(1) + analytics.flush end.to_not raise_error end end From e55cd8713b95df99a440f2efc20385881a8398e0 Mon Sep 17 00:00:00 2001 From: Paul Kuruvilla Date: Sat, 30 Mar 2019 15:12:45 +0530 Subject: [PATCH 076/179] Don't assume that all errors are ConnectionErrors --- lib/segment/analytics/request.rb | 2 +- spec/segment/analytics/request_spec.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/segment/analytics/request.rb b/lib/segment/analytics/request.rb index d65e86e..5fcb1c2 100644 --- a/lib/segment/analytics/request.rb +++ b/lib/segment/analytics/request.rb @@ -53,7 +53,7 @@ def post(write_key, batch) if exception logger.error(exception.message) exception.backtrace.each { |line| logger.error(line) } - Response.new(-1, "Connection error: #{exception}") + Response.new(-1, exception.to_s) else last_response end diff --git a/spec/segment/analytics/request_spec.rb b/spec/segment/analytics/request_spec.rb index 721d7e9..b4d0f63 100644 --- a/spec/segment/analytics/request_spec.rb +++ b/spec/segment/analytics/request_spec.rb @@ -232,7 +232,7 @@ class Analytics it 'has a connection error' do error = subject.post(write_key, batch).error - expect(error).to match(/Connection error/) + expect(error).to match(/Malformed JSON/) end it_behaves_like('retried request', 200, 'Malformed JSON ---') From d07c42b0a4e0ea66e2bd1adbc665f92fdc386588 Mon Sep 17 00:00:00 2001 From: Paul Kuruvilla Date: Wed, 11 Jul 2018 12:21:12 +0530 Subject: [PATCH 077/179] Promote 2.2.6.pre to 2.2.6 --- History.md | 9 ++++++--- lib/segment/analytics/version.rb | 2 +- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/History.md b/History.md index bed1b48..c776a65 100644 --- a/History.md +++ b/History.md @@ -1,6 +1,9 @@ -Note: There is a known issue when using `2.2.5` with both `oj` and `rails` gems -installed. Please test out `2.2.6.pre` and hold off on using `2.2.5`. -[Details](https://github.com/segmentio/analytics-ruby/pull/166) +2.2.6 / 2018-06-11 +================== + + * Promote pre-release version to stable. + * [Fix](https://github.com/segmentio/analytics-ruby/pull/187): Don't assume + all errors are 'ConnectionError's 2.2.6.pre / 2018-06-27 ================== diff --git a/lib/segment/analytics/version.rb b/lib/segment/analytics/version.rb index 7d4028f..70aa0a5 100644 --- a/lib/segment/analytics/version.rb +++ b/lib/segment/analytics/version.rb @@ -1,5 +1,5 @@ module Segment class Analytics - VERSION = '2.2.6.pre' + VERSION = '2.2.6' end end From 7681bdcae983b4347e1a0efff1288f3a93793ec6 Mon Sep 17 00:00:00 2001 From: Paul Kuruvilla Date: Sat, 30 Mar 2019 15:36:50 +0530 Subject: [PATCH 078/179] Remove unused method --- lib/segment/analytics/client.rb | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/lib/segment/analytics/client.rb b/lib/segment/analytics/client.rb index ce830a3..d41a357 100644 --- a/lib/segment/analytics/client.rb +++ b/lib/segment/analytics/client.rb @@ -388,19 +388,6 @@ def check_timestamp!(timestamp) raise ArgumentError, 'Timestamp must be a Time' unless timestamp.is_a? Time end - def event(attrs) - symbolize_keys! attrs - - { - :userId => user_id, - :name => name, - :properties => properties, - :context => context, - :timestamp => datetime_in_iso8601(timestamp), - :type => 'screen' - } - end - def check_user_id!(attrs) unless attrs[:user_id] || attrs[:anonymous_id] raise ArgumentError, 'Must supply either user_id or anonymous_id' From cb00e85efcf27af378b0902dfe03a448adc3a8df Mon Sep 17 00:00:00 2001 From: Paul Kuruvilla Date: Sat, 30 Mar 2019 15:39:55 +0530 Subject: [PATCH 079/179] Remove unused instance variable --- lib/segment/analytics/client.rb | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/segment/analytics/client.rb b/lib/segment/analytics/client.rb index d41a357..5451bd6 100644 --- a/lib/segment/analytics/client.rb +++ b/lib/segment/analytics/client.rb @@ -23,9 +23,8 @@ def initialize(opts = {}) @queue = Queue.new @write_key = opts[:write_key] @max_queue_size = opts[:max_queue_size] || Defaults::Queue::MAX_SIZE - @options = opts @worker_mutex = Mutex.new - @worker = Worker.new(@queue, @write_key, @options) + @worker = Worker.new(@queue, @write_key, opts) check_write_key! From 66d941a82e7a7e84b36285a3e2e0f84f1e290d7c Mon Sep 17 00:00:00 2001 From: Paul Kuruvilla Date: Sat, 30 Mar 2019 16:18:46 +0530 Subject: [PATCH 080/179] Add FieldParser, use in #track --- lib/segment/analytics.rb | 1 + lib/segment/analytics/client.rb | 44 +++----------- lib/segment/analytics/field_parser.rb | 85 +++++++++++++++++++++++++++ 3 files changed, 94 insertions(+), 36 deletions(-) create mode 100644 lib/segment/analytics/field_parser.rb diff --git a/lib/segment/analytics.rb b/lib/segment/analytics.rb index c03f8cc..d6f1df8 100644 --- a/lib/segment/analytics.rb +++ b/lib/segment/analytics.rb @@ -1,6 +1,7 @@ require 'segment/analytics/version' require 'segment/analytics/defaults' require 'segment/analytics/utils' +require 'segment/analytics/field_parser' require 'segment/analytics/client' require 'segment/analytics/worker' require 'segment/analytics/request' diff --git a/lib/segment/analytics/client.rb b/lib/segment/analytics/client.rb index 5451bd6..6547b3c 100644 --- a/lib/segment/analytics/client.rb +++ b/lib/segment/analytics/client.rb @@ -47,53 +47,25 @@ def flush # @see https://segment.com/docs/sources/server/ruby/#track # # @param [Hash] attrs + # + # @option attrs [String] :event Event name + # @option attrs [Hash] :properties Event properties (optional) + # # @option attrs [String] :anonymous_id ID for a user when you don't know # who they are yet. (optional but you must provide either an # `anonymous_id` or `user_id`) # @option attrs [Hash] :context ({}) - # @option attrs [String] :event Event name # @option attrs [Hash] :integrations What integrations this event # goes to (optional) - # @option attrs [Hash] :options Options such as user traits (optional) - # @option attrs [Hash] :properties Event properties (optional) + # @option attrs [String] :message_id ID that uniquely + # identifies a message across the API. (optional) # @option attrs [Time] :timestamp When the event occurred (optional) # @option attrs [String] :user_id The ID for this user in your database # (optional but you must provide either an `anonymous_id` or `user_id`) - # @option attrs [String] :message_id ID that uniquely - # identifies a message across the API. (optional) + # @option attrs [Hash] :options Options such as user traits (optional) def track(attrs) symbolize_keys! attrs - check_user_id! attrs - - event = attrs[:event] - properties = attrs[:properties] || {} - timestamp = attrs[:timestamp] || Time.new - context = attrs[:context] || {} - message_id = attrs[:message_id].to_s if attrs[:message_id] - - check_timestamp! timestamp - - if event.nil? || event.empty? - raise ArgumentError, 'Must supply event as a non-empty string' - end - - raise ArgumentError, 'Properties must be a Hash' unless properties.is_a? Hash - isoify_dates! properties - - add_context context - - enqueue({ - :event => event, - :userId => attrs[:user_id], - :anonymousId => attrs[:anonymous_id], - :context => context, - :options => attrs[:options], - :integrations => attrs[:integrations], - :properties => properties, - :messageId => message_id, - :timestamp => datetime_in_iso8601(timestamp), - :type => 'track' - }) + enqueue(FieldParser.parse_for_track(attrs)) end # Identifies a user diff --git a/lib/segment/analytics/field_parser.rb b/lib/segment/analytics/field_parser.rb new file mode 100644 index 0000000..157f34a --- /dev/null +++ b/lib/segment/analytics/field_parser.rb @@ -0,0 +1,85 @@ +module Segment + class Analytics + # Handles parsing fields according to the Segment Spec + # + # @see https://segment.com/docs/spec/ + class FieldParser + class << self + include Segment::Analytics::Utils + + # In addition to the common fields, track accepts: + # + # - "event" + # - "properties" + def parse_for_track(fields) + common = parse_common_fields(fields) + + event = fields[:event] + properties = fields[:properties] || {} + + check_presence!(event, 'event') + check_is_hash!(properties, 'properties') + + isoify_dates! properties + + common.merge({ + :type => 'track', + :event => event.to_s, + :properties => properties + }) + end + + private + + def parse_common_fields(fields) + timestamp = fields[:timestamp] || Time.new + message_id = fields[:message_id].to_s if fields[:message_id] + context = fields[:context] || {} + + check_user_id! fields + check_timestamp! timestamp + + add_context! context + + { + :anonymousId => fields[:anonymous_id], + :context => context, + :integrations => fields[:integrations], + :messageId => message_id, + :timestamp => datetime_in_iso8601(timestamp), + :userId => fields[:user_id], + :options => fields[:options] # Not in spec, retained for backward compatibility + } + end + + def check_user_id!(fields) + unless fields[:user_id] || fields[:anonymous_id] + raise ArgumentError, 'Must supply either user_id or anonymous_id' + end + end + + def check_timestamp!(timestamp) + raise ArgumentError, 'Timestamp must be a Time' unless timestamp.is_a? Time + end + + def add_context!(context) + context[:library] = { :name => 'analytics-ruby', :version => Segment::Analytics::VERSION.to_s } + end + + # private: Ensures that a string is non-empty + # + # obj - String|Number that must be non-blank + # name - Name of the validated value + def check_presence!(obj, name) + if obj.nil? || (obj.is_a?(String) && obj.empty?) + raise ArgumentError, "#{name} must be given" + end + end + + def check_is_hash!(obj, name) + raise ArgumentError, "#{name} must be a Hash" unless obj.is_a? Hash + end + end + end + end +end From 62f4a24669e312817769599dfb5ca08405a93305 Mon Sep 17 00:00:00 2001 From: Paul Kuruvilla Date: Sat, 30 Mar 2019 16:36:55 +0530 Subject: [PATCH 081/179] Use common fields parser for #identify --- lib/segment/analytics/client.rb | 36 ++++++--------------------- lib/segment/analytics/field_parser.rb | 16 ++++++++++++ 2 files changed, 23 insertions(+), 29 deletions(-) diff --git a/lib/segment/analytics/client.rb b/lib/segment/analytics/client.rb index 6547b3c..6aea723 100644 --- a/lib/segment/analytics/client.rb +++ b/lib/segment/analytics/client.rb @@ -73,46 +73,24 @@ def track(attrs) # @see https://segment.com/docs/sources/server/ruby/#identify # # @param [Hash] attrs + # + # @option attrs [Hash] :traits User traits (optional) + # # @option attrs [String] :anonymous_id ID for a user when you don't know # who they are yet. (optional but you must provide either an # `anonymous_id` or `user_id`) # @option attrs [Hash] :context ({}) # @option attrs [Hash] :integrations What integrations this event # goes to (optional) - # @option attrs [Hash] :options Options such as user traits (optional) + # @option attrs [String] :message_id ID that uniquely + # identifies a message across the API. (optional) # @option attrs [Time] :timestamp When the event occurred (optional) - # @option attrs [Hash] :traits User traits (optional) # @option attrs [String] :user_id The ID for this user in your database # (optional but you must provide either an `anonymous_id` or `user_id`) - # @option attrs [String] :message_id ID that uniquely identifies a - # message across the API. (optional) + # @option attrs [Hash] :options Options such as user traits (optional) def identify(attrs) symbolize_keys! attrs - check_user_id! attrs - - traits = attrs[:traits] || {} - timestamp = attrs[:timestamp] || Time.new - context = attrs[:context] || {} - message_id = attrs[:message_id].to_s if attrs[:message_id] - - check_timestamp! timestamp - - raise ArgumentError, 'Must supply traits as a hash' unless traits.is_a? Hash - isoify_dates! traits - - add_context context - - enqueue({ - :userId => attrs[:user_id], - :anonymousId => attrs[:anonymous_id], - :integrations => attrs[:integrations], - :context => context, - :traits => traits, - :options => attrs[:options], - :messageId => message_id, - :timestamp => datetime_in_iso8601(timestamp), - :type => 'identify' - }) + enqueue(FieldParser.parse_for_identify(attrs)) end # Aliases a user from one id to another diff --git a/lib/segment/analytics/field_parser.rb b/lib/segment/analytics/field_parser.rb index 157f34a..c7a8532 100644 --- a/lib/segment/analytics/field_parser.rb +++ b/lib/segment/analytics/field_parser.rb @@ -29,6 +29,22 @@ def parse_for_track(fields) }) end + # In addition to the common fields, identify accepts: + # + # - "traits" + def parse_for_identify(fields) + common = parse_common_fields(fields) + + traits = fields[:traits] || {} + check_is_hash!(traits, 'traits') + isoify_dates! traits + + common.merge({ + :type => 'identify', + :traits => traits + }) + end + private def parse_common_fields(fields) From 15369fe25cdf74fceee465bd758ca53799e283ea Mon Sep 17 00:00:00 2001 From: Paul Kuruvilla Date: Sat, 30 Mar 2019 16:47:34 +0530 Subject: [PATCH 082/179] Use common fields parser for #alias --- lib/segment/analytics/client.rb | 43 +++++++++------------------ lib/segment/analytics/field_parser.rb | 15 ++++++++++ 2 files changed, 29 insertions(+), 29 deletions(-) diff --git a/lib/segment/analytics/client.rb b/lib/segment/analytics/client.rb index 6aea723..8937fd1 100644 --- a/lib/segment/analytics/client.rb +++ b/lib/segment/analytics/client.rb @@ -98,39 +98,24 @@ def identify(attrs) # @see https://segment.com/docs/sources/server/ruby/#alias # # @param [Hash] attrs + # + # @option attrs [String] :previous_id The ID to alias from + # + # @option attrs [String] :anonymous_id ID for a user when you don't know + # who they are yet. (optional but you must provide either an + # `anonymous_id` or `user_id`) # @option attrs [Hash] :context ({}) - # @option attrs [Hash] :integrations What integrations this must be - # sent to (optional) + # @option attrs [Hash] :integrations What integrations this event + # goes to (optional) + # @option attrs [String] :message_id ID that uniquely + # identifies a message across the API. (optional) + # @option attrs [Time] :timestamp When the event occurred (optional) + # @option attrs [String] :user_id The ID for this user in your database + # (optional but you must provide either an `anonymous_id` or `user_id`) # @option attrs [Hash] :options Options such as user traits (optional) - # @option attrs [String] :previous_id The ID to alias from - # @option attrs [Time] :timestamp When the alias occurred (optional) - # @option attrs [String] :user_id The ID to alias to - # @option attrs [String] :message_id ID that uniquely identifies a - # message across the API. (optional) def alias(attrs) symbolize_keys! attrs - - from = attrs[:previous_id] - to = attrs[:user_id] - timestamp = attrs[:timestamp] || Time.new - context = attrs[:context] || {} - message_id = attrs[:message_id].to_s if attrs[:message_id] - - check_presence! from, 'previous_id' - check_presence! to, 'user_id' - check_timestamp! timestamp - add_context context - - enqueue({ - :previousId => from, - :userId => to, - :integrations => attrs[:integrations], - :context => context, - :options => attrs[:options], - :messageId => message_id, - :timestamp => datetime_in_iso8601(timestamp), - :type => 'alias' - }) + enqueue(FieldParser.parse_for_alias(attrs)) end # Associates a user identity with a group. diff --git a/lib/segment/analytics/field_parser.rb b/lib/segment/analytics/field_parser.rb index c7a8532..ef77f79 100644 --- a/lib/segment/analytics/field_parser.rb +++ b/lib/segment/analytics/field_parser.rb @@ -45,6 +45,21 @@ def parse_for_identify(fields) }) end + # In addition to the common fields, alias accepts: + # + # - "previous_id" + def parse_for_alias(fields) + common = parse_common_fields(fields) + + previous_id = fields[:previous_id] + check_presence!(previous_id, 'previous_id') + + common.merge({ + :type => 'alias', + :previousId => previous_id + }) + end + private def parse_common_fields(fields) From fd8faa9ad3ee2c01503323a94f8e47124d139ba5 Mon Sep 17 00:00:00 2001 From: Paul Kuruvilla Date: Sat, 30 Mar 2019 16:55:04 +0530 Subject: [PATCH 083/179] Use common fields parser for #group --- lib/segment/analytics/client.rb | 54 +++++---------------------- lib/segment/analytics/field_parser.rb | 22 +++++++++++ 2 files changed, 32 insertions(+), 44 deletions(-) diff --git a/lib/segment/analytics/client.rb b/lib/segment/analytics/client.rb index 8937fd1..8357ccf 100644 --- a/lib/segment/analytics/client.rb +++ b/lib/segment/analytics/client.rb @@ -123,48 +123,25 @@ def alias(attrs) # @see https://segment.com/docs/sources/server/ruby/#group # # @param [Hash] attrs + # + # @option attrs [String] :group_id The ID of the group + # @option attrs [Hash] :traits User traits (optional) + # # @option attrs [String] :anonymous_id ID for a user when you don't know # who they are yet. (optional but you must provide either an # `anonymous_id` or `user_id`) # @option attrs [Hash] :context ({}) - # @option attrs [String] :group_id The ID of the group # @option attrs [Hash] :integrations What integrations this event # goes to (optional) - # @option attrs [Hash] :options Options such as user traits (optional) + # @option attrs [String] :message_id ID that uniquely + # identifies a message across the API. (optional) # @option attrs [Time] :timestamp When the event occurred (optional) - # @option attrs [String] :user_id The ID for the user that is part of - # the group - # @option attrs [String] :message_id ID that uniquely identifies a - # message across the API. (optional) + # @option attrs [String] :user_id The ID for this user in your database + # (optional but you must provide either an `anonymous_id` or `user_id`) + # @option attrs [Hash] :options Options such as user traits (optional) def group(attrs) symbolize_keys! attrs - check_user_id! attrs - - group_id = attrs[:group_id] - user_id = attrs[:user_id] - traits = attrs[:traits] || {} - timestamp = attrs[:timestamp] || Time.new - context = attrs[:context] || {} - message_id = attrs[:message_id].to_s if attrs[:message_id] - - raise ArgumentError, '.traits must be a hash' unless traits.is_a? Hash - isoify_dates! traits - - check_presence! group_id, 'group_id' - check_timestamp! timestamp - add_context context - - enqueue({ - :groupId => group_id, - :userId => user_id, - :traits => traits, - :integrations => attrs[:integrations], - :options => attrs[:options], - :context => context, - :messageId => message_id, - :timestamp => datetime_in_iso8601(timestamp), - :type => 'group' - }) + enqueue(FieldParser.parse_for_group(attrs)) end # Records a page view @@ -294,17 +271,6 @@ def enqueue(action) end end - # private: Ensures that a string is non-empty - # - # obj - String|Number that must be non-blank - # name - Name of the validated value - # - def check_presence!(obj, name) - if obj.nil? || (obj.is_a?(String) && obj.empty?) - raise ArgumentError, "#{name} must be given" - end - end - # private: Adds contextual information to the call # # context - Hash of call context diff --git a/lib/segment/analytics/field_parser.rb b/lib/segment/analytics/field_parser.rb index ef77f79..f3ce094 100644 --- a/lib/segment/analytics/field_parser.rb +++ b/lib/segment/analytics/field_parser.rb @@ -60,6 +60,28 @@ def parse_for_alias(fields) }) end + # In addition to the common fields, group accepts: + # + # - "group_id" + # - "traits" + def parse_for_group(fields) + common = parse_common_fields(fields) + + group_id = fields[:group_id] + traits = fields[:traits] || {} + + check_presence!(group_id, 'group_id') + check_is_hash!(traits, 'traits') + + isoify_dates! traits + + common.merge({ + :type => 'group', + :groupId => group_id, + :traits => traits + }) + end + private def parse_common_fields(fields) From ecc5576c361665ab1d2c59e641c7ee832772eab5 Mon Sep 17 00:00:00 2001 From: Paul Kuruvilla Date: Sat, 30 Mar 2019 17:08:13 +0530 Subject: [PATCH 084/179] Use common field parser for #page --- lib/segment/analytics/client.rb | 44 ++++++--------------------- lib/segment/analytics/field_parser.rb | 22 ++++++++++++++ 2 files changed, 32 insertions(+), 34 deletions(-) diff --git a/lib/segment/analytics/client.rb b/lib/segment/analytics/client.rb index 8357ccf..07c66f6 100644 --- a/lib/segment/analytics/client.rb +++ b/lib/segment/analytics/client.rb @@ -149,49 +149,25 @@ def group(attrs) # @see https://segment.com/docs/sources/server/ruby/#page # # @param [Hash] attrs + # + # @option attrs [String] :name Name of the page + # @option attrs [Hash] :properties Page properties (optional) + # # @option attrs [String] :anonymous_id ID for a user when you don't know # who they are yet. (optional but you must provide either an # `anonymous_id` or `user_id`) - # @option attrs [String] :category The page category (optional) # @option attrs [Hash] :context ({}) # @option attrs [Hash] :integrations What integrations this event # goes to (optional) - # @option attrs [String] :name Name of the page + # @option attrs [String] :message_id ID that uniquely + # identifies a message across the API. (optional) + # @option attrs [Time] :timestamp When the event occurred (optional) + # @option attrs [String] :user_id The ID for this user in your database + # (optional but you must provide either an `anonymous_id` or `user_id`) # @option attrs [Hash] :options Options such as user traits (optional) - # @option attrs [Hash] :properties Page properties (optional) - # @option attrs [Time] :timestamp When the pageview occurred (optional) - # @option attrs [String] :user_id The ID of the user viewing the page - # @option attrs [String] :message_id ID that uniquely identifies a - # message across the API. (optional) def page(attrs) symbolize_keys! attrs - check_user_id! attrs - - name = attrs[:name].to_s - properties = attrs[:properties] || {} - timestamp = attrs[:timestamp] || Time.new - context = attrs[:context] || {} - message_id = attrs[:message_id].to_s if attrs[:message_id] - - raise ArgumentError, '.properties must be a hash' unless properties.is_a? Hash - isoify_dates! properties - - check_timestamp! timestamp - add_context context - - enqueue({ - :userId => attrs[:user_id], - :anonymousId => attrs[:anonymous_id], - :name => name, - :category => attrs[:category], - :properties => properties, - :integrations => attrs[:integrations], - :options => attrs[:options], - :context => context, - :messageId => message_id, - :timestamp => datetime_in_iso8601(timestamp), - :type => 'page' - }) + enqueue(FieldParser.parse_for_page(attrs)) end # Records a screen view (for a mobile app) diff --git a/lib/segment/analytics/field_parser.rb b/lib/segment/analytics/field_parser.rb index f3ce094..a45526d 100644 --- a/lib/segment/analytics/field_parser.rb +++ b/lib/segment/analytics/field_parser.rb @@ -82,6 +82,28 @@ def parse_for_group(fields) }) end + # In addition to the common fields, page accepts: + # + # - "name" + # - "properties" + def parse_for_page(fields) + common = parse_common_fields(fields) + + name = fields[:name] + properties = fields[:properties] || {} + + check_presence!(name, 'name') + check_is_hash!(properties, 'properties') + + isoify_dates! properties + + common.merge({ + :type => 'page', + :name => name.to_s, + :properties => properties + }) + end + private def parse_common_fields(fields) From 84e5eb7c586d6b5cfb89686bb6324e1240a4d7b4 Mon Sep 17 00:00:00 2001 From: Paul Kuruvilla Date: Sat, 30 Mar 2019 17:13:41 +0530 Subject: [PATCH 085/179] Use common field parser for #screen --- lib/segment/analytics/client.rb | 45 +++++++-------------------- lib/segment/analytics/field_parser.rb | 25 +++++++++++++++ 2 files changed, 36 insertions(+), 34 deletions(-) diff --git a/lib/segment/analytics/client.rb b/lib/segment/analytics/client.rb index 07c66f6..2a8f509 100644 --- a/lib/segment/analytics/client.rb +++ b/lib/segment/analytics/client.rb @@ -173,49 +173,26 @@ def page(attrs) # Records a screen view (for a mobile app) # # @param [Hash] attrs + # + # @option attrs [String] :name Name of the screen + # @option attrs [Hash] :properties Screen properties (optional) + # @option attrs [String] :category The screen category (optional) + # # @option attrs [String] :anonymous_id ID for a user when you don't know # who they are yet. (optional but you must provide either an # `anonymous_id` or `user_id`) - # @option attrs [String] :category The screen category (optional) # @option attrs [Hash] :context ({}) # @option attrs [Hash] :integrations What integrations this event # goes to (optional) - # @option attrs [String] :name Name of the screen + # @option attrs [String] :message_id ID that uniquely + # identifies a message across the API. (optional) + # @option attrs [Time] :timestamp When the event occurred (optional) + # @option attrs [String] :user_id The ID for this user in your database + # (optional but you must provide either an `anonymous_id` or `user_id`) # @option attrs [Hash] :options Options such as user traits (optional) - # @option attrs [Hash] :properties Page properties (optional) - # @option attrs [Time] :timestamp When the pageview occurred (optional) - # @option attrs [String] :user_id The ID of the user viewing the screen - # @option attrs [String] :message_id ID that uniquely identifies a - # message across the API. (optional) def screen(attrs) symbolize_keys! attrs - check_user_id! attrs - - name = attrs[:name].to_s - properties = attrs[:properties] || {} - timestamp = attrs[:timestamp] || Time.new - context = attrs[:context] || {} - message_id = attrs[:message_id].to_s if attrs[:message_id] - - raise ArgumentError, '.properties must be a hash' unless properties.is_a? Hash - isoify_dates! properties - - check_timestamp! timestamp - add_context context - - enqueue({ - :userId => attrs[:user_id], - :anonymousId => attrs[:anonymous_id], - :name => name, - :properties => properties, - :category => attrs[:category], - :options => attrs[:options], - :integrations => attrs[:integrations], - :context => context, - :messageId => message_id, - :timestamp => timestamp.iso8601, - :type => 'screen' - }) + enqueue(FieldParser.parse_for_screen(attrs)) end # @return [Fixnum] number of messages in the queue diff --git a/lib/segment/analytics/field_parser.rb b/lib/segment/analytics/field_parser.rb index a45526d..4f142f7 100644 --- a/lib/segment/analytics/field_parser.rb +++ b/lib/segment/analytics/field_parser.rb @@ -104,6 +104,31 @@ def parse_for_page(fields) }) end + # In addition to the common fields, screen accepts: + # + # - "name" + # - "properties" + # - "category" (Not in spec, retained for backward compatibility" + def parse_for_screen(fields) + common = parse_common_fields(fields) + + name = fields[:name] + properties = fields[:properties] || {} + category = fields[:category] + + check_presence!(name, 'name') + check_is_hash!(properties, 'properties') + + isoify_dates! properties + + common.merge({ + :type => 'screen', + :name => name, + :properties => properties, + :category => category + }) + end + private def parse_common_fields(fields) From 15d957ae84fbae9409bf7ce3dd805efd37aa087c Mon Sep 17 00:00:00 2001 From: Paul Kuruvilla Date: Sat, 30 Mar 2019 17:17:49 +0530 Subject: [PATCH 086/179] Remove unused private methods --- lib/segment/analytics/client.rb | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/lib/segment/analytics/client.rb b/lib/segment/analytics/client.rb index 2a8f509..e75e0fb 100644 --- a/lib/segment/analytics/client.rb +++ b/lib/segment/analytics/client.rb @@ -224,29 +224,11 @@ def enqueue(action) end end - # private: Adds contextual information to the call - # - # context - Hash of call context - def add_context(context) - context[:library] = { :name => 'analytics-ruby', :version => Segment::Analytics::VERSION.to_s } - end - # private: Checks that the write_key is properly initialized def check_write_key! raise ArgumentError, 'Write key must be initialized' if @write_key.nil? end - # private: Checks the timstamp option to make sure it is a Time. - def check_timestamp!(timestamp) - raise ArgumentError, 'Timestamp must be a Time' unless timestamp.is_a? Time - end - - def check_user_id!(attrs) - unless attrs[:user_id] || attrs[:anonymous_id] - raise ArgumentError, 'Must supply either user_id or anonymous_id' - end - end - def ensure_worker_running return if worker_running? @worker_mutex.synchronize do From 153bc4665d498ed122c2a4ed5bfdcf8045c34efc Mon Sep 17 00:00:00 2001 From: Paul Kuruvilla Date: Sat, 6 Apr 2019 14:15:39 +0530 Subject: [PATCH 087/179] Simplify repetitive documentation using macros --- lib/segment/analytics/client.rb | 98 +++++++-------------------------- 1 file changed, 20 insertions(+), 78 deletions(-) diff --git a/lib/segment/analytics/client.rb b/lib/segment/analytics/client.rb index e75e0fb..e83c8d8 100644 --- a/lib/segment/analytics/client.rb +++ b/lib/segment/analytics/client.rb @@ -42,6 +42,20 @@ def flush end end + # @!macro common_attrs + # @option attrs [String] :anonymous_id ID for a user when you don't know + # who they are yet. (optional but you must provide either an + # `anonymous_id` or `user_id`) + # @option attrs [Hash] :context ({}) + # @option attrs [Hash] :integrations What integrations this event + # goes to (optional) + # @option attrs [String] :message_id ID that uniquely + # identifies a message across the API. (optional) + # @option attrs [Time] :timestamp When the event occurred (optional) + # @option attrs [String] :user_id The ID for this user in your database + # (optional but you must provide either an `anonymous_id` or `user_id`) + # @option attrs [Hash] :options Options such as user traits (optional) + # Tracks an event # # @see https://segment.com/docs/sources/server/ruby/#track @@ -50,19 +64,7 @@ def flush # # @option attrs [String] :event Event name # @option attrs [Hash] :properties Event properties (optional) - # - # @option attrs [String] :anonymous_id ID for a user when you don't know - # who they are yet. (optional but you must provide either an - # `anonymous_id` or `user_id`) - # @option attrs [Hash] :context ({}) - # @option attrs [Hash] :integrations What integrations this event - # goes to (optional) - # @option attrs [String] :message_id ID that uniquely - # identifies a message across the API. (optional) - # @option attrs [Time] :timestamp When the event occurred (optional) - # @option attrs [String] :user_id The ID for this user in your database - # (optional but you must provide either an `anonymous_id` or `user_id`) - # @option attrs [Hash] :options Options such as user traits (optional) + # @macro common_attrs def track(attrs) symbolize_keys! attrs enqueue(FieldParser.parse_for_track(attrs)) @@ -75,19 +77,7 @@ def track(attrs) # @param [Hash] attrs # # @option attrs [Hash] :traits User traits (optional) - # - # @option attrs [String] :anonymous_id ID for a user when you don't know - # who they are yet. (optional but you must provide either an - # `anonymous_id` or `user_id`) - # @option attrs [Hash] :context ({}) - # @option attrs [Hash] :integrations What integrations this event - # goes to (optional) - # @option attrs [String] :message_id ID that uniquely - # identifies a message across the API. (optional) - # @option attrs [Time] :timestamp When the event occurred (optional) - # @option attrs [String] :user_id The ID for this user in your database - # (optional but you must provide either an `anonymous_id` or `user_id`) - # @option attrs [Hash] :options Options such as user traits (optional) + # @macro common_attrs def identify(attrs) symbolize_keys! attrs enqueue(FieldParser.parse_for_identify(attrs)) @@ -100,19 +90,7 @@ def identify(attrs) # @param [Hash] attrs # # @option attrs [String] :previous_id The ID to alias from - # - # @option attrs [String] :anonymous_id ID for a user when you don't know - # who they are yet. (optional but you must provide either an - # `anonymous_id` or `user_id`) - # @option attrs [Hash] :context ({}) - # @option attrs [Hash] :integrations What integrations this event - # goes to (optional) - # @option attrs [String] :message_id ID that uniquely - # identifies a message across the API. (optional) - # @option attrs [Time] :timestamp When the event occurred (optional) - # @option attrs [String] :user_id The ID for this user in your database - # (optional but you must provide either an `anonymous_id` or `user_id`) - # @option attrs [Hash] :options Options such as user traits (optional) + # @macro common_attrs def alias(attrs) symbolize_keys! attrs enqueue(FieldParser.parse_for_alias(attrs)) @@ -126,19 +104,7 @@ def alias(attrs) # # @option attrs [String] :group_id The ID of the group # @option attrs [Hash] :traits User traits (optional) - # - # @option attrs [String] :anonymous_id ID for a user when you don't know - # who they are yet. (optional but you must provide either an - # `anonymous_id` or `user_id`) - # @option attrs [Hash] :context ({}) - # @option attrs [Hash] :integrations What integrations this event - # goes to (optional) - # @option attrs [String] :message_id ID that uniquely - # identifies a message across the API. (optional) - # @option attrs [Time] :timestamp When the event occurred (optional) - # @option attrs [String] :user_id The ID for this user in your database - # (optional but you must provide either an `anonymous_id` or `user_id`) - # @option attrs [Hash] :options Options such as user traits (optional) + # @macro common_attrs def group(attrs) symbolize_keys! attrs enqueue(FieldParser.parse_for_group(attrs)) @@ -152,19 +118,7 @@ def group(attrs) # # @option attrs [String] :name Name of the page # @option attrs [Hash] :properties Page properties (optional) - # - # @option attrs [String] :anonymous_id ID for a user when you don't know - # who they are yet. (optional but you must provide either an - # `anonymous_id` or `user_id`) - # @option attrs [Hash] :context ({}) - # @option attrs [Hash] :integrations What integrations this event - # goes to (optional) - # @option attrs [String] :message_id ID that uniquely - # identifies a message across the API. (optional) - # @option attrs [Time] :timestamp When the event occurred (optional) - # @option attrs [String] :user_id The ID for this user in your database - # (optional but you must provide either an `anonymous_id` or `user_id`) - # @option attrs [Hash] :options Options such as user traits (optional) + # @macro common_attrs def page(attrs) symbolize_keys! attrs enqueue(FieldParser.parse_for_page(attrs)) @@ -177,19 +131,7 @@ def page(attrs) # @option attrs [String] :name Name of the screen # @option attrs [Hash] :properties Screen properties (optional) # @option attrs [String] :category The screen category (optional) - # - # @option attrs [String] :anonymous_id ID for a user when you don't know - # who they are yet. (optional but you must provide either an - # `anonymous_id` or `user_id`) - # @option attrs [Hash] :context ({}) - # @option attrs [Hash] :integrations What integrations this event - # goes to (optional) - # @option attrs [String] :message_id ID that uniquely - # identifies a message across the API. (optional) - # @option attrs [Time] :timestamp When the event occurred (optional) - # @option attrs [String] :user_id The ID for this user in your database - # (optional but you must provide either an `anonymous_id` or `user_id`) - # @option attrs [Hash] :options Options such as user traits (optional) + # @macro common_attrs def screen(attrs) symbolize_keys! attrs enqueue(FieldParser.parse_for_screen(attrs)) From e2336c24391f92102d3481bf43db12137fd40d0e Mon Sep 17 00:00:00 2001 From: Paul Kuruvilla Date: Sat, 6 Apr 2019 14:36:33 +0530 Subject: [PATCH 088/179] Add extra tests for user_id/anonymous_id --- spec/segment/analytics_spec.rb | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/spec/segment/analytics_spec.rb b/spec/segment/analytics_spec.rb index fcf5f51..f4444dd 100644 --- a/spec/segment/analytics_spec.rb +++ b/spec/segment/analytics_spec.rb @@ -10,8 +10,10 @@ class Analytics expect { analytics.track(:user_id => 'user') }.to raise_error(ArgumentError) end - it 'errors without a user_id' do - expect { analytics.track(:event => 'Event') }.to raise_error(ArgumentError) + it 'errors without user_id or anonymous_id' do + expect { analytics.track :event => 'event' }.to raise_error(ArgumentError) + expect { analytics.track :event => 'event', user_id: '1234' }.to_not raise_error(ArgumentError) + expect { analytics.track :event => 'event', anonymous_id: '2345' }.to_not raise_error(ArgumentError) end it 'does not error with the required options' do @@ -23,8 +25,10 @@ class Analytics end describe '#identify' do - it 'errors without a user_id' do + it 'errors without user_id or anonymous_id' do expect { analytics.identify :traits => {} }.to raise_error(ArgumentError) + expect { analytics.identify :traits => {}, user_id: '1234' }.to_not raise_error(ArgumentError) + expect { analytics.identify :traits => {}, anonymous_id: '2345' }.to_not raise_error(ArgumentError) end it 'does not error with the required options' do @@ -34,12 +38,14 @@ class Analytics end describe '#alias' do - it 'errors without from' do + it 'errors without previous_id' do expect { analytics.alias :user_id => 1234 }.to raise_error(ArgumentError) end - it 'errors without to' do - expect { analytics.alias :previous_id => 1234 }.to raise_error(ArgumentError) + it 'errors without user_id or anonymous_id' do + expect { analytics.alias :previous_id => 'foo' }.to raise_error(ArgumentError) + expect { analytics.alias :previous_id => 'foo', user_id: '1234' }.to_not raise_error(ArgumentError) + expect { analytics.alias :previous_id => 'foo', anonymous_id: '2345' }.to_not raise_error(ArgumentError) end it 'does not error with the required options' do @@ -57,6 +63,8 @@ class Analytics it 'errors without user_id or anonymous_id' do expect { analytics.group :group_id => 'foo' }.to raise_error(ArgumentError) + expect { analytics.group :group_id => 'foo', user_id: '1234' }.to_not raise_error(ArgumentError) + expect { analytics.group :group_id => 'foo', anonymous_id: '2345' }.to_not raise_error(ArgumentError) end it 'does not error with the required options' do @@ -70,6 +78,8 @@ class Analytics describe '#page' do it 'errors without user_id or anonymous_id' do expect { analytics.page :name => 'foo' }.to raise_error(ArgumentError) + expect { analytics.page :name => 'foo', user_id: '1234' }.to_not raise_error(ArgumentError) + expect { analytics.page :name => 'foo', anonymous_id: '2345' }.to_not raise_error(ArgumentError) end it 'does not error with the required options' do @@ -83,6 +93,8 @@ class Analytics describe '#screen' do it 'errors without user_id or anonymous_id' do expect { analytics.screen :name => 'foo' }.to raise_error(ArgumentError) + expect { analytics.screen :name => 'foo', user_id: '1234' }.to_not raise_error(ArgumentError) + expect { analytics.screen :name => 'foo', anonymous_id: '2345' }.to_not raise_error(ArgumentError) end it 'does not error with the required options' do From 5da40adbbc7c8eb9a07bea3b88c93dee8cb5b7bc Mon Sep 17 00:00:00 2001 From: Paul Kuruvilla Date: Fri, 19 Apr 2019 12:58:32 +0530 Subject: [PATCH 089/179] Add failing test for optional :name in #page --- spec/segment/analytics/client_spec.rb | 10 ++++++---- spec/spec_helper.rb | 4 +--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/spec/segment/analytics/client_spec.rb b/spec/segment/analytics/client_spec.rb index 178eb13..d6d8225 100644 --- a/spec/segment/analytics/client_spec.rb +++ b/spec/segment/analytics/client_spec.rb @@ -224,10 +224,12 @@ class Analytics expect { client.page Queued::PAGE }.to_not raise_error end - it 'does not error with the required options as strings' do - expect do - client.page Utils.stringify_keys(Queued::PAGE) - end.to_not raise_error + it 'accepts name' do + client.page :name => 'foo', :user_id => 1234 + + message = queue.pop + expect(message[:userId]).to eq(1234) + expect(message[:name]).to eq('foo') end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 8bc41b0..847acea 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -39,9 +39,7 @@ class Analytics GROUP = {} - PAGE = { - :name => 'home' - } + PAGE = {} SCREEN = { :name => 'main' From b59720a377296db719e9f1d4ddf7557f9a23531c Mon Sep 17 00:00:00 2001 From: Paul Kuruvilla Date: Fri, 19 Apr 2019 13:01:30 +0530 Subject: [PATCH 090/179] Make name non-mandatory in #page This was a bug, introduced in #188 --- lib/segment/analytics/field_parser.rb | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/segment/analytics/field_parser.rb b/lib/segment/analytics/field_parser.rb index 4f142f7..378c730 100644 --- a/lib/segment/analytics/field_parser.rb +++ b/lib/segment/analytics/field_parser.rb @@ -89,10 +89,9 @@ def parse_for_group(fields) def parse_for_page(fields) common = parse_common_fields(fields) - name = fields[:name] + name = fields[:name] || '' properties = fields[:properties] || {} - check_presence!(name, 'name') check_is_hash!(properties, 'properties') isoify_dates! properties From 343e3d186804735500269b094a880bce481f0059 Mon Sep 17 00:00:00 2001 From: Paul Kuruvilla Date: Thu, 9 May 2019 18:53:03 +0530 Subject: [PATCH 091/179] Add new release instructions This ensures that pre-releases and their promoted versions contain the exact same code. --- RELEASING.md | 29 ++++++++++++++++++++++------- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/RELEASING.md b/RELEASING.md index 2a60d9c..a13353d 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -1,9 +1,24 @@ -Releasing -========= +Pre-Releases +============ 1. Verify everything works with `make check build`. - 2. Bump version in [`version.rb`](https://github.com/segmentio/analytics-ruby/blob/master/lib/segment/analytics/version.rb). - 3. Update [`History.md`](https://github.com/segmentio/analytics-ruby/blob/master/History.md). - 4. Commit and tag `git commit -am "Release {version}" && git tag -a {version} -m "Version {version}"`. - 5. Upload to Github with `git push -u origin master && git push --tags`. -The tagged commit will be pushed to RubyGems via Travis. + 2. Bump version in + [`version.rb`](https://github.com/segmentio/analytics-ruby/blob/master/lib/segment/analytics/version.rb). + This version string should not include a `.pre` suffix, as the same commit will + be re-tagged when the pre-release is promoted. + 3. Update + [`History.md`](https://github.com/segmentio/analytics-ruby/blob/master/History.md). + 4. Commit and tag the pre-release. `git commit -am "Release {version.pre}" && + git tag -a {version.pre} -m "Version {version.pre}"`. + 5. Upload to Github with `git push -u origin master && git push --tags`. The + tagged commit will be pushed to RubyGems via Travis. + +Promoting Pre-releases +====================== + +- Find the tag for the pre-release you want to promote. `git tag --list + '*.pre'` +- Re-tag this commit without the `.pre` prefix. `git tag -a -m "Version + {version}" {version} {pre_version}` +- Upload to Github with `git push --tags`. The tagged commit will be pushed to + RubyGems via Travis. From 95ba917df0500b509084e45d76e81ae90c646f49 Mon Sep 17 00:00:00 2001 From: Paul Kuruvilla Date: Thu, 9 May 2019 18:58:03 +0530 Subject: [PATCH 092/179] Bump version and update history for 2.2.7.pre --- History.md | 6 ++++++ lib/segment/analytics/version.rb | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/History.md b/History.md index c776a65..34be271 100644 --- a/History.md +++ b/History.md @@ -1,3 +1,9 @@ +2.2.7.pre / 2019-05-09 +================== + + * [Fix](https://github.com/segmentio/analytics-ruby/pull/188): Allow `anonymous_id` + in `#alias` and `#group`. + 2.2.6 / 2018-06-11 ================== diff --git a/lib/segment/analytics/version.rb b/lib/segment/analytics/version.rb index 70aa0a5..9a4addf 100644 --- a/lib/segment/analytics/version.rb +++ b/lib/segment/analytics/version.rb @@ -1,5 +1,5 @@ module Segment class Analytics - VERSION = '2.2.6' + VERSION = '2.2.7' end end From 3381f7f49cd1f71bd7bf09099f625a093360bddc Mon Sep 17 00:00:00 2001 From: Paul Kuruvilla Date: Fri, 24 May 2019 19:55:52 +0530 Subject: [PATCH 093/179] Update history to reflect 2.2.7 release Due to a bug in the release script, we accidentally released 2.2.7 directly instead of going through a pre-release. --- History.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/History.md b/History.md index 34be271..7f400e7 100644 --- a/History.md +++ b/History.md @@ -1,4 +1,4 @@ -2.2.7.pre / 2019-05-09 +2.2.7 / 2019-05-09 ================== * [Fix](https://github.com/segmentio/analytics-ruby/pull/188): Allow `anonymous_id` From 0e62d149e9e02855613666b82992365339eec581 Mon Sep 17 00:00:00 2001 From: Paul Kuruvilla Date: Mon, 27 May 2019 21:17:15 +0530 Subject: [PATCH 094/179] Upgrade tester version --- .buildscript/e2e.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.buildscript/e2e.sh b/.buildscript/e2e.sh index ed25afe..adc5c66 100755 --- a/.buildscript/e2e.sh +++ b/.buildscript/e2e.sh @@ -6,7 +6,7 @@ if [ "$RUN_E2E_TESTS" != "true" ]; then echo "Skipping end to end tests." else echo "Running end to end tests..." - wget https://github.com/segmentio/library-e2e-tester/releases/download/0.2.1/tester_linux_amd64 -O tester + wget https://github.com/segmentio/library-e2e-tester/releases/download/0.4.0/tester_linux_amd64 -O tester chmod +x tester ./tester -path='./bin/analytics' echo "End to end tests completed!" From 8b4dca9b81fd9366fdd3119d370d1fc03c42f2d3 Mon Sep 17 00:00:00 2001 From: Paul Kuruvilla Date: Mon, 27 May 2019 21:28:32 +0530 Subject: [PATCH 095/179] Support --integrations in CLI for e2e tests --- bin/analytics | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/bin/analytics b/bin/analytics index e9e107e..1ba79db 100755 --- a/bin/analytics +++ b/bin/analytics @@ -29,6 +29,7 @@ command :send do |c| c.option '--userId=', String, 'the user id to send the event as' c.option '--anonymousId=', String, 'the anonymous user id to send the event as' c.option '--context=', 'additional context for the event (JSON-encoded)' + c.option '--integrations=', 'additional integrations for the event (JSON-encoded)' c.option '--event=', String, 'the event name to send with the event' c.option '--properties=', 'the event properties to send (JSON-encoded)' @@ -52,7 +53,8 @@ command :send do |c| event: options.event, anonymous_id: options.anonymousId, properties: json_hash(options.properties), - context: json_hash(options.context) + context: json_hash(options.context), + integrations: json_hash(options.integrations) }) when "page" Analytics.page({ @@ -60,22 +62,25 @@ command :send do |c| anonymous_id: options.anonymousId, name: options.name, properties: json_hash(options.properties), - context: json_hash(options.context) + context: json_hash(options.context), + integrations: json_hash(options.integrations) }) when "screen" Analytics.screen({ user_id: options.userId, anonymous_id: options.anonymousId, - name: option.name, - traits: json_hash(options.traits), - properties: json_hash(option.properties) + name: options.name, + properties: json_hash(options.properties), + context: json_hash(options.context), + integrations: json_hash(options.integrations) }) when "identify" Analytics.identify({ user_id: options.userId, anonymous_id: options.anonymousId, traits: json_hash(options.traits), - context: json_hash(options.context) + context: json_hash(options.context), + integrations: json_hash(options.integrations) }) when "group" Analytics.group({ @@ -83,7 +88,8 @@ command :send do |c| anonymous_id: options.anonymousId, group_id: options.groupId, traits: json_hash(options.traits), - context: json_hash(options.context) + context: json_hash(options.context), + integrations: json_hash(options.integrations) }) else raise "Invalid Message Type #{options.type}" From c13ff43a3ef11895de728faf492cb817d808e2b4 Mon Sep 17 00:00:00 2001 From: Paul Kuruvilla Date: Mon, 27 May 2019 21:38:51 +0530 Subject: [PATCH 096/179] Support 'alias' calls in e2e tests --- bin/analytics | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/bin/analytics b/bin/analytics index 1ba79db..86f3511 100755 --- a/bin/analytics +++ b/bin/analytics @@ -39,6 +39,7 @@ command :send do |c| c.option '--traits=', 'the identify/group traits to send (JSON-encoded)' c.option '--groupId=', String, 'the group id' + c.option '--previousId=', String, 'the previous id' c.action do |args, options| Analytics = Segment::Analytics.new({ @@ -91,6 +92,14 @@ command :send do |c| context: json_hash(options.context), integrations: json_hash(options.integrations) }) + when "alias" + Analytics.alias({ + previous_id: options.previousId, + user_id: options.userId, + anonymous_id: options.anonymousId, + context: json_hash(options.context), + integrations: json_hash(options.integrations) + }) else raise "Invalid Message Type #{options.type}" end From bc71efc5e8663d9620415eabe89551213de20c1c Mon Sep 17 00:00:00 2001 From: Paul Kuruvilla Date: Mon, 27 May 2019 22:04:28 +0530 Subject: [PATCH 097/179] Don't send fields if they're nil --- lib/segment/analytics/field_parser.rb | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/lib/segment/analytics/field_parser.rb b/lib/segment/analytics/field_parser.rb index 378c730..7918710 100644 --- a/lib/segment/analytics/field_parser.rb +++ b/lib/segment/analytics/field_parser.rb @@ -120,12 +120,15 @@ def parse_for_screen(fields) isoify_dates! properties - common.merge({ + parsed = common.merge({ :type => 'screen', :name => name, - :properties => properties, - :category => category + :properties => properties }) + + parsed[:category] = category if category + + parsed end private @@ -140,15 +143,20 @@ def parse_common_fields(fields) add_context! context - { - :anonymousId => fields[:anonymous_id], + parsed = { :context => context, - :integrations => fields[:integrations], :messageId => message_id, - :timestamp => datetime_in_iso8601(timestamp), - :userId => fields[:user_id], - :options => fields[:options] # Not in spec, retained for backward compatibility + :timestamp => datetime_in_iso8601(timestamp) } + + parsed[:userId] = fields[:user_id] if fields[:user_id] + parsed[:anonymousId] = fields[:anonymous_id] if fields[:anonymous_id] + parsed[:integrations] = fields[:integrations] if fields[:integrations] + + # Not in spec, retained for backward compatibility + parsed[:options] = fields[:options] if fields[:options] + + parsed end def check_user_id!(fields) From 39aad129f292525bd63ea48f2b1a8713c45c76a1 Mon Sep 17 00:00:00 2001 From: Paul Kuruvilla Date: Mon, 27 May 2019 22:29:53 +0530 Subject: [PATCH 098/179] Use wget's quiet mode when downloading tester --- .buildscript/e2e.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.buildscript/e2e.sh b/.buildscript/e2e.sh index adc5c66..a634eda 100755 --- a/.buildscript/e2e.sh +++ b/.buildscript/e2e.sh @@ -6,7 +6,7 @@ if [ "$RUN_E2E_TESTS" != "true" ]; then echo "Skipping end to end tests." else echo "Running end to end tests..." - wget https://github.com/segmentio/library-e2e-tester/releases/download/0.4.0/tester_linux_amd64 -O tester + wget -q https://github.com/segmentio/library-e2e-tester/releases/download/0.4.0/tester_linux_amd64 -O tester chmod +x tester ./tester -path='./bin/analytics' echo "End to end tests completed!" From 540af902db80d94742267608481f44b70b096476 Mon Sep 17 00:00:00 2001 From: Paul Kuruvilla Date: Wed, 5 Jun 2019 17:21:11 +0530 Subject: [PATCH 099/179] Fix steps in RELEASING.md for pre-releases --- RELEASING.md | 42 +++++++++++++++++++++++------------------- 1 file changed, 23 insertions(+), 19 deletions(-) diff --git a/RELEASING.md b/RELEASING.md index a13353d..b6041eb 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -1,24 +1,28 @@ -Pre-Releases +We automatically push tags to Rubygems via CI. + +Pre-releases ============ - 1. Verify everything works with `make check build`. - 2. Bump version in - [`version.rb`](https://github.com/segmentio/analytics-ruby/blob/master/lib/segment/analytics/version.rb). - This version string should not include a `.pre` suffix, as the same commit will - be re-tagged when the pre-release is promoted. - 3. Update - [`History.md`](https://github.com/segmentio/analytics-ruby/blob/master/History.md). - 4. Commit and tag the pre-release. `git commit -am "Release {version.pre}" && - git tag -a {version.pre} -m "Version {version.pre}"`. - 5. Upload to Github with `git push -u origin master && git push --tags`. The - tagged commit will be pushed to RubyGems via Travis. +- Make sure you're on the latest `master` +- Bump the version in [`version.rb`](lib/segment/analytics/version.rb) +- Update [`History.md`](History.md) +- Commit these changes. `git commit -am "Release x.y.z.pre"` +- Tag the pre-release. `git tag -a -m "Version x.y.z.pre" x.y.z.pre` +- `git push -u origin master && git push --tags`. The tagged commit will be + pushed to RubyGems via Travis + -Promoting Pre-releases +Promoting pre-releases ====================== -- Find the tag for the pre-release you want to promote. `git tag --list - '*.pre'` -- Re-tag this commit without the `.pre` prefix. `git tag -a -m "Version - {version}" {version} {pre_version}` -- Upload to Github with `git push --tags`. The tagged commit will be pushed to - RubyGems via Travis. +- Find the tag for the pre-release you want to promote. Use `git tag --list + '*.pre'` to list all pre-release tags +- Checkout that tag. `git checkout tags/x.y.z.pre` +- Update the version in [`version.rb`](lib/segment/analytics/version.rb) to not + include the `.pre` suffix +- Commit these changes. `git commit -am "Promote x.y.z.pre"` +- Tag the release. `git tag -a -m "Version x.y.z" x.y.z` +- `git push -u origin master && git push --tags`. The tagged commit will be + pushed to RubyGems via Travis +- On `master`, add an entry to [`History.md`](History.md) under `x.y.z` that + says 'Promoted pre-release to stable' From 92a3594edc39ad93ebcda3841dc67389e66b6913 Mon Sep 17 00:00:00 2001 From: Paul Kuruvilla Date: Thu, 18 Jul 2019 11:15:15 +0530 Subject: [PATCH 100/179] Remove 1.9.3 from test matrix Travis doesn't support 1.9.3 anymore, as it looks for RVM binaries and they aren't available for 14.04. Ref: https://rvm.io/binaries/ubuntu/16.04/x86_64/ --- .travis.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 5cc960c..6750bc0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,7 +2,6 @@ language: ruby rvm: - jruby-19mode - - 1.9.3 - 2.0.0 - 2.1.10 - 2.2.9 From 4a135fbddc42c4dd2c37be57758eb18b4fd5c7f4 Mon Sep 17 00:00:00 2001 From: Paul Kuruvilla Date: Mon, 29 Jul 2019 21:31:37 +0530 Subject: [PATCH 101/179] Add coverage/ to .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 83f5868..ab09e0c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ *.gem Gemfile.lock .ruby-version +coverage/ From 81b825e4d751f1fff37e0df83229aa92713cb18f Mon Sep 17 00:00:00 2001 From: Paul Kuruvilla Date: Mon, 29 Jul 2019 22:08:42 +0530 Subject: [PATCH 102/179] Handle serialization errors, isolate bad events This commit adds error handling for JSON generation, and ensures that one bad event doesn't affect other good events in a batch. Fixes https://github.com/segmentio/analytics-ruby/issues/180 --- lib/segment/analytics/message_batch.rb | 9 ++++++++- lib/segment/analytics/worker.rb | 11 +++++++++-- spec/segment/analytics/worker_spec.rb | 23 +++++++++++++++++++++++ 3 files changed, 40 insertions(+), 3 deletions(-) diff --git a/lib/segment/analytics/message_batch.rb b/lib/segment/analytics/message_batch.rb index 2e9385d..1522463 100644 --- a/lib/segment/analytics/message_batch.rb +++ b/lib/segment/analytics/message_batch.rb @@ -5,6 +5,8 @@ module Segment class Analytics # A batch of `Message`s to be sent to the API class MessageBatch + class JSONGenerationError < StandardError; end + extend Forwardable include Segment::Analytics::Logging include Segment::Analytics::Defaults::MessageBatch @@ -16,8 +18,13 @@ def initialize(max_message_count) end def <<(message) - message_json_size = message.to_json.bytesize + begin + message_json = message.to_json + rescue StandardError => e + raise JSONGenerationError, "Serialization error: #{e}" + end + message_json_size = message_json.bytesize if message_too_big?(message_json_size) logger.error('a message exceeded the maximum allowed size') else diff --git a/lib/segment/analytics/worker.rb b/lib/segment/analytics/worker.rb index 2f86dd4..ced52e9 100644 --- a/lib/segment/analytics/worker.rb +++ b/lib/segment/analytics/worker.rb @@ -38,11 +38,10 @@ def run return if @queue.empty? @lock.synchronize do - @batch << @queue.pop until @batch.full? || @queue.empty? + consume_message_from_queue! until @batch.full? || @queue.empty? end res = Request.new.post @write_key, @batch - @on_error.call(res.status, res.error) unless res.status == 200 @lock.synchronize { @batch.clear } @@ -54,6 +53,14 @@ def run def is_requesting? @lock.synchronize { !@batch.empty? } end + + private + + def consume_message_from_queue! + @batch << @queue.pop + rescue MessageBatch::JSONGenerationError => e + @on_error.call(-1, e.to_s) + end end end end diff --git a/spec/segment/analytics/worker_spec.rb b/spec/segment/analytics/worker_spec.rb index e66db64..eb5de20 100644 --- a/spec/segment/analytics/worker_spec.rb +++ b/spec/segment/analytics/worker_spec.rb @@ -83,6 +83,29 @@ class Analytics expect(queue).to be_empty end + + it 'calls on_error for bad json' do + bad_obj = Object.new + def bad_obj.to_json(*_args) + raise "can't serialize to json" + end + + on_error = proc {} + expect(on_error).to receive(:call).once.with(-1, /serialize to json/) + + good_message = Requested::TRACK + bad_message = Requested::TRACK.merge({ 'bad_obj' => bad_obj }) + + queue = Queue.new + queue << good_message + queue << bad_message + + worker = described_class.new(queue, + 'testsecret', + :on_error => on_error) + worker.run + expect(queue).to be_empty + end end describe '#is_requesting?' do From 5da703e7d64afe4943d49098368b37ebee09cbc0 Mon Sep 17 00:00:00 2001 From: Kyle VanderBeek Date: Sat, 7 Sep 2019 13:27:50 -0700 Subject: [PATCH 103/179] Explicitly initialize @worker_thread Without this, some modern rubies will emit this warning: /usr/local/bundle/gems/analytics-ruby-2.2.7/lib/segment/analytics/client.rb:31: warning: instance variable @worker_thread not initialized --- lib/segment/analytics/client.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/segment/analytics/client.rb b/lib/segment/analytics/client.rb index e83c8d8..61ee386 100644 --- a/lib/segment/analytics/client.rb +++ b/lib/segment/analytics/client.rb @@ -25,6 +25,7 @@ def initialize(opts = {}) @max_queue_size = opts[:max_queue_size] || Defaults::Queue::MAX_SIZE @worker_mutex = Mutex.new @worker = Worker.new(@queue, @write_key, opts) + @worker_thread = nil check_write_key! From e84b6fc72a0a72d39f328878464a98c82c4b2a22 Mon Sep 17 00:00:00 2001 From: Paul Kuruvilla Date: Sun, 6 Oct 2019 02:34:42 +0530 Subject: [PATCH 104/179] Set minimum ruby version to 2.0 + minor code/docs cleanup in areas that were 1.9-specific --- .rubocop.yml | 6 +----- .travis.yml | 1 - analytics-ruby.gemspec | 4 +--- spec/segment/analytics/client_spec.rb | 8 ++++---- 4 files changed, 6 insertions(+), 13 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index d9751fd..a043e4f 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1,7 +1,7 @@ inherit_from: .rubocop_todo.yml AllCops: - # Rubocop doesn't support 1.9, so we'll use the minimum available + # Rubocop doesn't support 2.0, so we'll use the minimum available TargetRubyVersion: 2.1 Layout/IndentHash: @@ -84,7 +84,3 @@ Style/ParallelAssignment: Style/PreferredHashMethods: EnforcedStyle: verbose - -# Ruby 1.9 doesn't support percent-styled symbol arrays -Style/SymbolArray: - EnforcedStyle: brackets diff --git a/.travis.yml b/.travis.yml index 6750bc0..cadd54c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,6 @@ language: ruby rvm: - - jruby-19mode - 2.0.0 - 2.1.10 - 2.2.9 diff --git a/analytics-ruby.gemspec b/analytics-ruby.gemspec index ba0c3fe..60d9155 100644 --- a/analytics-ruby.gemspec +++ b/analytics-ruby.gemspec @@ -13,9 +13,7 @@ Gem::Specification.new do |spec| spec.email = 'friends@segment.io' spec.homepage = 'https://github.com/segmentio/analytics-ruby' spec.license = 'MIT' - - # Ruby 1.8 requires json - spec.add_dependency 'json', ['~> 1.7'] if RUBY_VERSION < "1.9" + spec.required_ruby_version = '>= 2.0' # Used in the executable testing script spec.add_development_dependency 'commander', '~> 4.4' diff --git a/spec/segment/analytics/client_spec.rb b/spec/segment/analytics/client_spec.rb index d6d8225..e4ffd60 100644 --- a/spec/segment/analytics/client_spec.rb +++ b/spec/segment/analytics/client_spec.rb @@ -281,7 +281,7 @@ class Analytics let(:data) { { :user_id => 1, :group_id => 2, :previous_id => 3, :anonymous_id => 4, :message_id => 5, :event => 'coco barked', :name => 'coco' } } it 'does not convert ids given as fixnums to strings' do - [:track, :screen, :page, :identify].each do |s| + %i[track screen page identify].each do |s| client.send(s, data) message = queue.pop(true) @@ -293,7 +293,7 @@ class Analytics it 'returns false if queue is full' do client.instance_variable_set(:@max_queue_size, 1) - [:track, :screen, :page, :group, :identify, :alias].each do |s| + %i[track screen page group identify alias].each do |s| expect(client.send(s, data)).to eq(true) expect(client.send(s, data)).to eq(false) # Queue is full queue.pop(true) @@ -301,7 +301,7 @@ class Analytics end it 'converts message id to string' do - [:track, :screen, :page, :group, :identify, :alias].each do |s| + %i[track screen page group identify alias].each do |s| client.send(s, data) message = queue.pop(true) @@ -330,7 +330,7 @@ class Analytics end it 'sends integrations' do - [:track, :screen, :page, :group, :identify, :alias].each do |s| + %i[track screen page group identify alias].each do |s| client.send s, :integrations => { :All => true, :Salesforce => false }, :user_id => 1, :group_id => 2, :previous_id => 3, :anonymous_id => 4, :event => 'coco barked', :name => 'coco' message = queue.pop(true) expect(message[:integrations][:All]).to eq(true) From d3e831ec31a4744b373a861bf7e72d6a472ce499 Mon Sep 17 00:00:00 2001 From: Paul Kuruvilla Date: Sun, 6 Oct 2019 02:56:50 +0530 Subject: [PATCH 105/179] Refresh .rubocop_todo.yml --- .rubocop_todo.yml | 21 ++++++++------------- 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 17dd033..11a76cc 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -1,39 +1,34 @@ # This configuration was generated by # `rubocop --auto-gen-config` -# on 2017-11-23 15:34:00 +0530 using RuboCop version 0.51.0. +# on 2019-10-06 02:55:44 +0530 using RuboCop version 0.51.0. # The point is for the user to remove these configuration records # one by one as the offenses are removed from the code base. # Note that changes in the inspected code, or installation of new # versions of RuboCop, may require this file to be generated again. -# Offense count: 1 -Lint/RescueException: - Exclude: - - 'lib/segment/analytics/request.rb' - -# Offense count: 9 +# Offense count: 4 Metrics/AbcSize: - Max: 32 + Max: 24 # Offense count: 1 # Configuration parameters: CountComments. Metrics/ClassLength: - Max: 231 + Max: 114 # Offense count: 1 Metrics/CyclomaticComplexity: Max: 8 -# Offense count: 36 +# Offense count: 8 # Configuration parameters: AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, IgnoredPatterns. # URISchemes: http, https Metrics/LineLength: - Max: 223 + Max: 147 -# Offense count: 9 +# Offense count: 10 # Configuration parameters: CountComments. Metrics/MethodLength: - Max: 29 + Max: 16 # Offense count: 1 Metrics/PerceivedComplexity: From 7a2439cedb1d7632561327f314bb9e146632ca53 Mon Sep 17 00:00:00 2001 From: Paul Kuruvilla Date: Sun, 6 Oct 2019 03:30:34 +0530 Subject: [PATCH 106/179] Avoid network calls in tests These tests were sending HTTP requests to Segment. Added mocks/stubs at the right places to prevent this from happening. --- spec/segment/analytics/client_spec.rb | 7 ++++++- spec/segment/analytics/worker_spec.rb | 22 +++++++++++++++++++--- spec/spec_helper.rb | 15 +++++++++++++++ 3 files changed, 40 insertions(+), 4 deletions(-) diff --git a/spec/segment/analytics/client_spec.rb b/spec/segment/analytics/client_spec.rb index d6d8225..496545b 100644 --- a/spec/segment/analytics/client_spec.rb +++ b/spec/segment/analytics/client_spec.rb @@ -250,7 +250,12 @@ class Analytics end describe '#flush' do - let(:client_with_worker) { Client.new(:write_key => WRITE_KEY) } + let(:client_with_worker) { + Client.new(:write_key => WRITE_KEY).tap { |client| + queue = client.instance_variable_get(:@queue) + client.instance_variable_set(:@worker, DummyWorker.new(queue)) + } + } it 'waits for the queue to finish on a flush' do client_with_worker.identify Queued::IDENTIFY diff --git a/spec/segment/analytics/worker_spec.rb b/spec/segment/analytics/worker_spec.rb index eb5de20..ca6dcd0 100644 --- a/spec/segment/analytics/worker_spec.rb +++ b/spec/segment/analytics/worker_spec.rb @@ -3,6 +3,10 @@ module Segment class Analytics describe Worker do + before do + Segment::Analytics::Request.stub = true + end + describe '#init' do it 'accepts string keys' do queue = Queue.new @@ -23,9 +27,12 @@ class Analytics Segment::Analytics::Defaults::Request::BACKOFF = 30.0 end - it 'does not error if the endpoint is unreachable' do + it 'does not error if the request fails' do expect do - Net::HTTP.any_instance.stub(:post).and_raise(Exception) + Segment::Analytics::Request + .any_instance + .stub(:post) + .and_return(Segment::Analytics::Response.new(-1, 'Unknown error')) queue = Queue.new queue << {} @@ -34,7 +41,7 @@ class Analytics expect(queue).to be_empty - Net::HTTP.any_instance.unstub(:post) + Segment::Analytics::Request.any_instance.unstub(:post) end.to_not raise_error end @@ -117,6 +124,13 @@ def bad_obj.to_json(*_args) end it 'returns true if there is a current batch' do + Segment::Analytics::Request + .any_instance + .stub(:post) { + sleep(0.2) + Segment::Analytics::Response.new(200, 'Success') + } + queue = Queue.new queue << Requested::TRACK worker = Segment::Analytics::Worker.new(queue, 'testsecret') @@ -126,6 +140,8 @@ def bad_obj.to_json(*_args) worker_thread.join expect(worker.is_requesting?).to eq(false) + + Segment::Analytics::Request.any_instance.unstub(:post) end end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 847acea..a0f9247 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -88,6 +88,21 @@ def run end end +# A worker that consumes all jobs +class DummyWorker + def initialize(queue) + @queue = queue + end + + def run + @queue.pop until @queue.empty? + end + + def is_requesting? + false + end +end + # A backoff policy that returns a fixed list of values class FakeBackoffPolicy def initialize(interval_values) From 6bba4b756229b01bfa6b6cac4b9283f874bf2822 Mon Sep 17 00:00:00 2001 From: Mark Date: Tue, 26 Nov 2019 18:51:54 +0400 Subject: [PATCH 107/179] Fix stubbed request message --- lib/segment/analytics/request.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/segment/analytics/request.rb b/lib/segment/analytics/request.rb index 5fcb1c2..344ab75 100644 --- a/lib/segment/analytics/request.rb +++ b/lib/segment/analytics/request.rb @@ -113,7 +113,7 @@ def send_request(write_key, batch) if self.class.stub logger.debug "stubbed request to #{@path}: " \ - "write key = #{write_key}, batch = JSON.generate(#{batch})" + "write key = #{write_key}, batch = #{JSON.generate(batch)}" [200, '{}'] else From 25edc53447f6bf404251318e999d8cb9ce60789e Mon Sep 17 00:00:00 2001 From: Paul Kuruvilla Date: Fri, 29 Nov 2019 23:02:46 +0530 Subject: [PATCH 108/179] Release 2.2.8.pre --- History.md | 8 ++++++++ lib/segment/analytics/version.rb | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/History.md b/History.md index 7f400e7..103a005 100644 --- a/History.md +++ b/History.md @@ -1,3 +1,11 @@ +2.2.8.pre / 2019-11-29 +================== + + * [Fix](https://github.com/segmentio/analytics-ruby/pull/212): Fix log message + for stubbed requests + * [Deprecate](https://github.com/segmentio/analytics-ruby/pull/209): Deprecate + Ruby <2.0 support + 2.2.7 / 2019-05-09 ================== diff --git a/lib/segment/analytics/version.rb b/lib/segment/analytics/version.rb index 9a4addf..5f3772e 100644 --- a/lib/segment/analytics/version.rb +++ b/lib/segment/analytics/version.rb @@ -1,5 +1,5 @@ module Segment class Analytics - VERSION = '2.2.7' + VERSION = '2.2.8.pre' end end From 6205ce3678123717be68e55b140ddcd113de0f89 Mon Sep 17 00:00:00 2001 From: Paul Kuruvilla Date: Fri, 6 Dec 2019 12:28:43 +0530 Subject: [PATCH 109/179] Re-use TCP connections across API calls - Rename Analytics::Request -> Analytics::Transport - Rename Transport#post -> Transport#send - Add Tranport#shutdown to close persistent connections - Re-use Transport object in Analytics::Worker --- lib/segment/analytics.rb | 4 +-- .../analytics/{request.rb => transport.rb} | 16 ++++++---- lib/segment/analytics/worker.rb | 7 +++-- .../{request_spec.rb => transport_spec.rb} | 29 ++++++++++--------- spec/segment/analytics/worker_spec.rb | 20 ++++++------- 5 files changed, 42 insertions(+), 34 deletions(-) rename lib/segment/analytics/{request.rb => transport.rb} (91%) rename spec/segment/analytics/{request_spec.rb => transport_spec.rb} (90%) diff --git a/lib/segment/analytics.rb b/lib/segment/analytics.rb index d6f1df8..1e47c62 100644 --- a/lib/segment/analytics.rb +++ b/lib/segment/analytics.rb @@ -4,7 +4,7 @@ require 'segment/analytics/field_parser' require 'segment/analytics/client' require 'segment/analytics/worker' -require 'segment/analytics/request' +require 'segment/analytics/transport' require 'segment/analytics/response' require 'segment/analytics/logging' @@ -18,7 +18,7 @@ class Analytics # @option options [Boolean] :stub (false) If true, requests don't hit the # server and are stubbed to be successful. def initialize(options = {}) - Request.stub = options[:stub] if options.has_key?(:stub) + Transport.stub = options[:stub] if options.has_key?(:stub) @client = Segment::Analytics::Client.new options end diff --git a/lib/segment/analytics/request.rb b/lib/segment/analytics/transport.rb similarity index 91% rename from lib/segment/analytics/request.rb rename to lib/segment/analytics/transport.rb index 344ab75..59697be 100644 --- a/lib/segment/analytics/request.rb +++ b/lib/segment/analytics/transport.rb @@ -9,13 +9,11 @@ module Segment class Analytics - class Request + class Transport include Segment::Analytics::Defaults::Request include Segment::Analytics::Utils include Segment::Analytics::Logging - # public: Creates a new request object to send analytics batch - # def initialize(options = {}) options[:host] ||= HOST options[:port] ||= PORT @@ -34,10 +32,10 @@ def initialize(options = {}) @http = http end - # public: Posts the write key and batch of messages to the API. + # Sends a batch of messages to the API # - # returns - Response of the status and error if it exists - def post(write_key, batch) + # @return [Response] API response + def send(write_key, batch) logger.debug("Sending request for #{batch.length} items") last_response, exception = retry_with_backoff(@retries) do @@ -59,6 +57,11 @@ def post(write_key, batch) end end + # Closes a persistent connection if it exists + def shutdown + @http.finish if @http.started? + end + private def should_retry_request?(status_code, body) @@ -117,6 +120,7 @@ def send_request(write_key, batch) [200, '{}'] else + @http.start unless @http.started? # Maintain a persistent connection response = @http.request(request, payload) [response.code.to_i, response.body] end diff --git a/lib/segment/analytics/worker.rb b/lib/segment/analytics/worker.rb index ced52e9..a313eed 100644 --- a/lib/segment/analytics/worker.rb +++ b/lib/segment/analytics/worker.rb @@ -1,6 +1,6 @@ require 'segment/analytics/defaults' require 'segment/analytics/message_batch' -require 'segment/analytics/request' +require 'segment/analytics/transport' require 'segment/analytics/utils' module Segment @@ -29,6 +29,7 @@ def initialize(queue, write_key, options = {}) batch_size = options[:batch_size] || Defaults::MessageBatch::MAX_SIZE @batch = MessageBatch.new(batch_size) @lock = Mutex.new + @transport = Transport.new end # public: Continuously runs the loop to check for new events @@ -41,11 +42,13 @@ def run consume_message_from_queue! until @batch.full? || @queue.empty? end - res = Request.new.post @write_key, @batch + res = @transport.send @write_key, @batch @on_error.call(res.status, res.error) unless res.status == 200 @lock.synchronize { @batch.clear } end + ensure + @transport.shutdown end # public: Check whether we have outstanding requests. diff --git a/spec/segment/analytics/request_spec.rb b/spec/segment/analytics/transport_spec.rb similarity index 90% rename from spec/segment/analytics/request_spec.rb rename to spec/segment/analytics/transport_spec.rb index b4d0f63..56b551f 100644 --- a/spec/segment/analytics/request_spec.rb +++ b/spec/segment/analytics/transport_spec.rb @@ -2,7 +2,7 @@ module Segment class Analytics - describe Request do + describe Transport do before do # Try and keep debug statements out of tests allow(subject.logger).to receive(:error) @@ -98,7 +98,7 @@ class Analytics end end - describe '#post' do + describe '#send' do let(:response) { Net::HTTPResponse.new(http_version, status_code, response_body) } @@ -110,6 +110,7 @@ class Analytics before do http = subject.instance_variable_get(:@http) + allow(http).to receive(:start) allow(http).to receive(:request) { response } allow(response).to receive(:body) { response_body } end @@ -125,14 +126,14 @@ class Analytics path, default_headers ).and_call_original - subject.post(write_key, batch) + subject.send(write_key, batch) end it 'adds basic auth to the Net::HTTP::Post' do expect_any_instance_of(Net::HTTP::Post).to receive(:basic_auth) .with(write_key, nil) - subject.post(write_key, batch) + subject.send(write_key, batch) end context 'with a stub' do @@ -141,16 +142,16 @@ class Analytics end it 'returns a 200 response' do - expect(subject.post(write_key, batch).status).to eq(200) + expect(subject.send(write_key, batch).status).to eq(200) end it 'has a nil error' do - expect(subject.post(write_key, batch).error).to be_nil + expect(subject.send(write_key, batch).error).to be_nil end it 'logs a debug statement' do expect(subject.logger).to receive(:debug).with(/stubbed request to/) - subject.post(write_key, batch) + subject.send(write_key, batch) end end @@ -171,7 +172,7 @@ class Analytics .exactly(retries - 1).times .with(1) .and_return(nil) - subject.post(write_key, batch) + subject.send(write_key, batch) end end @@ -186,18 +187,18 @@ class Analytics expect(subject) .to receive(:sleep) .never - subject.post(write_key, batch) + subject.send(write_key, batch) end end context 'request is successful' do let(:status_code) { 201 } it 'returns a response code' do - expect(subject.post(write_key, batch).status).to eq(status_code) + expect(subject.send(write_key, batch).status).to eq(status_code) end it 'returns a nil error' do - expect(subject.post(write_key, batch).error).to be_nil + expect(subject.send(write_key, batch).error).to be_nil end end @@ -206,7 +207,7 @@ class Analytics let(:response_body) { { error: error }.to_json } it 'returns the parsed error' do - expect(subject.post(write_key, batch).error).to eq(error) + expect(subject.send(write_key, batch).error).to eq(error) end end @@ -227,11 +228,11 @@ class Analytics subject { described_class.new(retries: 0) } it 'returns a -1 for status' do - expect(subject.post(write_key, batch).status).to eq(-1) + expect(subject.send(write_key, batch).status).to eq(-1) end it 'has a connection error' do - error = subject.post(write_key, batch).error + error = subject.send(write_key, batch).error expect(error).to match(/Malformed JSON/) end diff --git a/spec/segment/analytics/worker_spec.rb b/spec/segment/analytics/worker_spec.rb index ca6dcd0..0ca4d49 100644 --- a/spec/segment/analytics/worker_spec.rb +++ b/spec/segment/analytics/worker_spec.rb @@ -4,7 +4,7 @@ module Segment class Analytics describe Worker do before do - Segment::Analytics::Request.stub = true + Segment::Analytics::Transport.stub = true end describe '#init' do @@ -29,9 +29,9 @@ class Analytics it 'does not error if the request fails' do expect do - Segment::Analytics::Request + Segment::Analytics::Transport .any_instance - .stub(:post) + .stub(:send) .and_return(Segment::Analytics::Response.new(-1, 'Unknown error')) queue = Queue.new @@ -41,14 +41,14 @@ class Analytics expect(queue).to be_empty - Segment::Analytics::Request.any_instance.unstub(:post) + Segment::Analytics::Transport.any_instance.unstub(:send) end.to_not raise_error end it 'executes the error handler if the request is invalid' do - Segment::Analytics::Request + Segment::Analytics::Transport .any_instance - .stub(:post) + .stub(:send) .and_return(Segment::Analytics::Response.new(400, 'Some error')) status = error = nil @@ -67,7 +67,7 @@ class Analytics sleep 0.1 # First give thread time to spin-up. sleep 0.01 while worker.is_requesting? - Segment::Analytics::Request.any_instance.unstub(:post) + Segment::Analytics::Transport.any_instance.unstub(:send) expect(queue).to be_empty expect(status).to eq(400) @@ -124,9 +124,9 @@ def bad_obj.to_json(*_args) end it 'returns true if there is a current batch' do - Segment::Analytics::Request + Segment::Analytics::Transport .any_instance - .stub(:post) { + .stub(:send) { sleep(0.2) Segment::Analytics::Response.new(200, 'Success') } @@ -141,7 +141,7 @@ def bad_obj.to_json(*_args) worker_thread.join expect(worker.is_requesting?).to eq(false) - Segment::Analytics::Request.any_instance.unstub(:post) + Segment::Analytics::Transport.any_instance.unstub(:send) end end end From b48dfa9ae3124a372beb99e1135e40cbade6a18c Mon Sep 17 00:00:00 2001 From: Paul Kuruvilla Date: Mon, 10 Feb 2020 16:50:21 +0530 Subject: [PATCH 110/179] Update History.md to reflect 2.2.8 release --- History.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/History.md b/History.md index 103a005..f703a72 100644 --- a/History.md +++ b/History.md @@ -1,3 +1,8 @@ +2.2.8 / 2020-02-10 +================== + + * Promoted pre-release version to stable. + 2.2.8.pre / 2019-11-29 ================== From bde3d65d5483afc7add58bcbecd90615702b4635 Mon Sep 17 00:00:00 2001 From: Kyle VanderBeek Date: Wed, 19 Feb 2020 17:07:38 -0800 Subject: [PATCH 111/179] BitDeli is gone, remove badge from README. --- README.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/README.md b/README.md index 2c44245..b0b551f 100644 --- a/README.md +++ b/README.md @@ -118,7 +118,3 @@ Permission is hereby granted, free of charge, to any person obtaining a copy of The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - - -[![Bitdeli Badge](https://d2weczhvl823v0.cloudfront.net/segmentio/analytics-ruby/trend.png)](https://bitdeli.com/free "Bitdeli Badge") - From 0fcb338938e59c7cbe9581ef3f81642182a550cb Mon Sep 17 00:00:00 2001 From: Shane Duvall Date: Wed, 24 Mar 2021 16:53:02 -0500 Subject: [PATCH 112/179] Changed the fractional digits to display 6 places instead of the original 3 --- lib/segment/analytics/utils.rb | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/lib/segment/analytics/utils.rb b/lib/segment/analytics/utils.rb index 60bfa52..745a5e4 100644 --- a/lib/segment/analytics/utils.rb +++ b/lib/segment/analytics/utils.rb @@ -64,12 +64,8 @@ def datetime_in_iso8601(datetime) end end - def time_in_iso8601(time, fraction_digits = 3) - fraction = if fraction_digits > 0 - ('.%06i' % time.usec)[0, fraction_digits + 1] - end - - "#{time.strftime('%Y-%m-%dT%H:%M:%S')}#{fraction}#{formatted_offset(time, true, 'Z')}" + def time_in_iso8601(time) + "#{time.strftime('%Y-%m-%dT%H:%M:%S.%6N')}#{formatted_offset(time, true, 'Z')}" end def date_in_iso8601(date) From 7aa3f1df29f26be1cc1d9f8d0ef36143cbbdae4e Mon Sep 17 00:00:00 2001 From: Shane Duvall Date: Fri, 26 Mar 2021 11:16:31 -0500 Subject: [PATCH 113/179] Issue #226 Update supported Ruby Versions --- .travis.yml | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/.travis.yml b/.travis.yml index cadd54c..d015dd2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,12 +1,10 @@ language: ruby rvm: - - 2.0.0 - - 2.1.10 - - 2.2.9 - - 2.3.6 - 2.4.3 - - 2.5.0 # Performs deploys. Change condition below when changing this. + - 2.5.0 + - 2.6.0 + - 2.7.0 # Performs deploys. Change condition below when changing this. script: - make check @@ -17,6 +15,6 @@ deploy: provider: rubygems on: tags: true - condition: "$TRAVIS_RUBY_VERSION == 2.5.0" + condition: "$TRAVIS_RUBY_VERSION == 2.7.0" api_key: secure: Ceq6J4aBpsoqRfSiC7z+/J4moOXNjcPMFb2Bfm5qE51cIZzeyuOIOc6zhrad9tUgoX6uTRRxLxkybyu4wNYSluMA3IXW20CJyXZeJEHIaTYIDTWFAIYyerBJyMujJycSo7XueWb0faKBENrBQKx1K1tS0EiXpA2rMhdA6RM3DOY= From 6c998346ca4d0f8f050674444e62474ba64dfe75 Mon Sep 17 00:00:00 2001 From: Shane Duvall Date: Fri, 26 Mar 2021 11:24:15 -0500 Subject: [PATCH 114/179] Update version file --- lib/segment/analytics/version.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/segment/analytics/version.rb b/lib/segment/analytics/version.rb index 5f3772e..767c2cd 100644 --- a/lib/segment/analytics/version.rb +++ b/lib/segment/analytics/version.rb @@ -1,5 +1,5 @@ module Segment class Analytics - VERSION = '2.2.8.pre' + VERSION = '2.2.8' end end From 91799d8a28b31ced1d25cd2a8953156d35499dc3 Mon Sep 17 00:00:00 2001 From: Shane Duvall Date: Wed, 24 Mar 2021 16:53:02 -0500 Subject: [PATCH 115/179] Changed the fractional digits to display 6 places instead of the original 3 --- lib/segment/analytics/utils.rb | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/lib/segment/analytics/utils.rb b/lib/segment/analytics/utils.rb index 60bfa52..745a5e4 100644 --- a/lib/segment/analytics/utils.rb +++ b/lib/segment/analytics/utils.rb @@ -64,12 +64,8 @@ def datetime_in_iso8601(datetime) end end - def time_in_iso8601(time, fraction_digits = 3) - fraction = if fraction_digits > 0 - ('.%06i' % time.usec)[0, fraction_digits + 1] - end - - "#{time.strftime('%Y-%m-%dT%H:%M:%S')}#{fraction}#{formatted_offset(time, true, 'Z')}" + def time_in_iso8601(time) + "#{time.strftime('%Y-%m-%dT%H:%M:%S.%6N')}#{formatted_offset(time, true, 'Z')}" end def date_in_iso8601(date) From a8558ea73792d52d01f55d34f8e6fa6cf688a5f2 Mon Sep 17 00:00:00 2001 From: Shane Duvall Date: Fri, 26 Mar 2021 12:07:35 -0500 Subject: [PATCH 116/179] Minor version bump with history --- History.md | 6 ++++++ lib/segment/analytics/version.rb | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/History.md b/History.md index f703a72..3d888f2 100644 --- a/History.md +++ b/History.md @@ -1,3 +1,9 @@ +2.3.0 / 2020-03-26 +================== + + * [Improvement](https://github.com/segmentio/analytics-ruby/pull/225): Update timestamp for sub-millisecond reporting + * Update supported Ruby versions (2.4, 2.5, 2.6, 2.7), remove unsupported Ruby versions (2.0, 2.1, 2.2, 2.3) + 2.2.8 / 2020-02-10 ================== diff --git a/lib/segment/analytics/version.rb b/lib/segment/analytics/version.rb index 767c2cd..62d2fa0 100644 --- a/lib/segment/analytics/version.rb +++ b/lib/segment/analytics/version.rb @@ -1,5 +1,5 @@ module Segment class Analytics - VERSION = '2.2.8' + VERSION = '2.3.0' end end From 7305134b9231099e152bdc0c7de4c63024113ea7 Mon Sep 17 00:00:00 2001 From: Pooya Jaferian Date: Fri, 26 Mar 2021 13:36:41 -0700 Subject: [PATCH 117/179] release 2.3.0 From 46df274056adf9f226412ea5dabf58959a8c7959 Mon Sep 17 00:00:00 2001 From: Ryan Jackson Date: Fri, 19 Feb 2021 13:40:59 +0100 Subject: [PATCH 118/179] Add test option and queue for easier testing --- README.md | 73 +++++++- lib/segment/analytics.rb | 1 + lib/segment/analytics/client.rb | 11 ++ lib/segment/analytics/test_queue.rb | 56 ++++++ spec/segment/analytics/test_queue_spec.rb | 213 ++++++++++++++++++++++ spec/segment/analytics_spec.rb | 27 +++ 6 files changed, 380 insertions(+), 1 deletion(-) create mode 100644 lib/segment/analytics/test_queue.rb create mode 100644 spec/segment/analytics/test_queue_spec.rb diff --git a/README.md b/README.md index b0b551f..f3b3d5a 100644 --- a/README.md +++ b/README.md @@ -92,7 +92,78 @@ Documentation is available at [segment.com/docs/sources/server/ruby](https://seg ## Testing -You can use the `stub` option to Segment::Analytics.new to cause all requests to be stubbed, making it easier to test with this library. +You can use the `stub: true` option to Segment::Analytics.new to cause all requests to be stubbed, making it easier to test with this library. + +### Test Queue + +You can use the `test: true` option to Segment::Analytics.new to cause all requests to be saved to a test queue until manually reset. All events will process as specified by the configuration, and they will also be stored in a separate queue for inspection during testing. + +A test queue can be used as follows: + +```ruby +client = Segment::Analytics.new(test: true) + +client.test_queue # => # + +client.track(user_id: "foo", event: "bar") + +client.test_queue.all +# [ +# { +# :context => { +# :library => { +# :name => "analytics-ruby", +# :version => "2.2.8.pre" +# } +# }, +# :messageId => "e9754cc0-1c5e-47e4-832a-203589d279e4", +# :timestamp => "2021-02-19T13:32:39.547+01:00", +# :userId => "foo", +# :type => "track", +# :event => "bar", +# :properties => {} +# } +# ] + +client.test_queue.track +# [ +# { +# :context => { +# :library => { +# :name => "analytics-ruby", +# :version => "2.2.8.pre" +# } +# }, +# :messageId => "e9754cc0-1c5e-47e4-832a-203589d279e4", +# :timestamp => "2021-02-19T13:32:39.547+01:00", +# :userId => "foo", +# :type => "track", +# :event => "bar", +# :properties => {} +# } +# ] + +# Other available methods +client.test_queue.alias # => [] +client.test_queue.group # => [] +client.test_queue.identify # => [] +client.test_queue.page # => [] +client.test_queue.screen # => [] + +client.reset! + +client.test_queue.all # => [] +``` + +Note: It is recommended to call `reset!` before each test to ensure your test queue is empty. For example, in rspec you may have the following: + +```ruby +RSpec.configure do |config| + config.before do + Analytics.test_queue.reset! + end +end +``` ## License diff --git a/lib/segment/analytics.rb b/lib/segment/analytics.rb index 1e47c62..ca83353 100644 --- a/lib/segment/analytics.rb +++ b/lib/segment/analytics.rb @@ -7,6 +7,7 @@ require 'segment/analytics/transport' require 'segment/analytics/response' require 'segment/analytics/logging' +require 'segment/analytics/test_queue' module Segment class Analytics diff --git a/lib/segment/analytics/client.rb b/lib/segment/analytics/client.rb index 61ee386..fc81cce 100644 --- a/lib/segment/analytics/client.rb +++ b/lib/segment/analytics/client.rb @@ -21,6 +21,7 @@ def initialize(opts = {}) symbolize_keys!(opts) @queue = Queue.new + @test = opts[:test] @write_key = opts[:write_key] @max_queue_size = opts[:max_queue_size] || Defaults::Queue::MAX_SIZE @worker_mutex = Mutex.new @@ -143,6 +144,14 @@ def queued_messages @queue.length end + def test_queue + unless @test + raise 'Test queue only available when setting :test to true.' + end + + @test_queue ||= TestQueue.new + end + private # private: Enqueues the action. @@ -152,6 +161,8 @@ def enqueue(action) # add our request id for tracing purposes action[:messageId] ||= uid + test_queue << action if @test + if @queue.length < @max_queue_size @queue << action ensure_worker_running diff --git a/lib/segment/analytics/test_queue.rb b/lib/segment/analytics/test_queue.rb new file mode 100644 index 0000000..af31558 --- /dev/null +++ b/lib/segment/analytics/test_queue.rb @@ -0,0 +1,56 @@ +module Segment + class Analytics + class TestQueue + attr_reader :messages + + def initialize + reset! + end + + def [](key) + all[key] + end + + def count + all.count + end + + def <<(message) + all << message + send(message[:type]) << message + end + + def alias + messages[:alias] ||= [] + end + + def all + messages[:all] ||= [] + end + + def group + messages[:group] ||= [] + end + + def identify + messages[:identify] ||= [] + end + + def page + messages[:page] ||= [] + end + + def screen + messages[:screen] ||= [] + end + + def track + messages[:track] ||= [] + end + + def reset! + @messages = {} + end + end + end +end diff --git a/spec/segment/analytics/test_queue_spec.rb b/spec/segment/analytics/test_queue_spec.rb new file mode 100644 index 0000000..810a49e --- /dev/null +++ b/spec/segment/analytics/test_queue_spec.rb @@ -0,0 +1,213 @@ +require 'spec_helper' + +module Segment + class Analytics + describe TestQueue do + let(:test_queue) { described_class.new } + + describe '#initialize' do + it 'starts empty' do + expect(test_queue.messages).to eq({}) + end + end + + describe '#<<' do + let(:message) do + { + type: type, + foo: "bar" + } + end + + let(:expected_messages) do + { + type.to_sym => [message], + all: [message] + } + end + + context 'when unsupported type' do + let(:type) { :foo } + + it 'raises error' do + expect { test_queue << message }.to raise_error(NoMethodError) + end + end + + context 'when supported type' do + before do + test_queue << message + end + + context 'when type is alias' do + let(:type) { :alias } + + it 'adds messages' do + expect(test_queue.messages).to eq(expected_messages) + end + + it 'adds type to all' do + expect(test_queue.all).to eq([message]) + end + + it 'adds type to alias' do + expect(test_queue.alias).to eq([message]) + end + end + + context 'when type is group' do + let(:type) { :group } + + it 'adds messages' do + expect(test_queue.messages).to eq(expected_messages) + end + + it 'adds type to all' do + expect(test_queue.all).to eq([message]) + end + + it 'adds type to group' do + expect(test_queue.group).to eq([message]) + end + end + + context 'when type is identify' do + let(:type) { :identify } + + it 'adds messages' do + expect(test_queue.messages).to eq(expected_messages) + end + + it 'adds type to all' do + expect(test_queue.all).to eq([message]) + end + + it 'adds type to identify' do + expect(test_queue.identify).to eq([message]) + end + end + + context 'when type is page' do + let(:type) { :page } + + it 'adds messages' do + expect(test_queue.messages).to eq(expected_messages) + end + + it 'adds type to all' do + expect(test_queue.all).to eq([message]) + end + + it 'adds type to page' do + expect(test_queue.page).to eq([message]) + end + end + + context 'when type is screen' do + let(:type) { :screen } + + it 'adds messages' do + expect(test_queue.messages).to eq(expected_messages) + end + + it 'adds type to all' do + expect(test_queue.all).to eq([message]) + end + + it 'adds type to screen' do + expect(test_queue.screen).to eq([message]) + end + end + + context 'when type is track' do + let(:type) { :track } + + it 'adds messages' do + expect(test_queue.messages).to eq(expected_messages) + end + + it 'adds type to all' do + expect(test_queue.all).to eq([message]) + end + + it 'adds type to track' do + expect(test_queue.track).to eq([message]) + end + end + end + end + + describe '#count' do + let(:message) do + { + type: "alias", + foo: "bar" + } + end + + it "returns 0" do + expect(test_queue.count).to eq(0) + end + + it "returns 1" do + test_queue << message + expect(test_queue.count).to eq(1) + end + + it "returns 2" do + test_queue << message + test_queue << message + expect(test_queue.count).to eq(2) + end + end + + describe '#[]' do + let(:message1) do + { + type: "alias", + foo: "bar" + } + end + + let(:message2) do + { + type: "identify", + foo: "baz" + } + end + + it "returns message1" do + test_queue << message1 + expect(test_queue[0]).to eq(message1) + end + + it "returns message2" do + test_queue << message2 + expect(test_queue[0]).to eq(message2) + end + + it "returns message2" do + test_queue << message1 + test_queue << message2 + expect(test_queue[1]).to eq(message2) + end + end + + describe '#reset!' do + let(:message) do + { + type: "alias", + foo: "bar" + } + end + + it "returns message" do + test_queue << message + expect(test_queue.count).to eq(1) + test_queue.reset! + expect(test_queue.messages).to eq({}) + end + end + end + end +end diff --git a/spec/segment/analytics_spec.rb b/spec/segment/analytics_spec.rb index f4444dd..17961b2 100644 --- a/spec/segment/analytics_spec.rb +++ b/spec/segment/analytics_spec.rb @@ -127,6 +127,33 @@ class Analytics end end end + + describe '#test_queue' do + context 'when not in mode' do + let(:analytics) { Segment::Analytics.new :write_key => WRITE_KEY, :stub => true, :test => true } + + it 'returns TestQueue' do + expect(analytics.test_queue).to be_a(TestQueue) + end + + it 'returns event' do + analytics.track Queued::TRACK + expect(analytics.test_queue[0]).to include(Requested::TRACK) + expect(analytics.test_queue.track[0]).to include(Requested::TRACK) + end + end + + context 'when not in test mode' do + let(:analytics) { Segment::Analytics.new :write_key => WRITE_KEY, :stub => true, :test => false } + + it 'errors when not in test mode' do + expect(analytics.instance_variable_get(:@test)).to be_falsey + expect { analytics.test_queue }.to raise_error( + RuntimeError, 'Test queue only available when setting :test to true.' + ) + end + end + end end end end From 337072c16aab3b2055bdfd75475c8fc254a60050 Mon Sep 17 00:00:00 2001 From: Ryan Jackson Date: Fri, 9 Apr 2021 15:49:17 +0200 Subject: [PATCH 119/179] Update quotes for rubocop --- README.md | 2 +- spec/segment/analytics/test_queue_spec.rb | 32 +++++++++++------------ 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index f3b3d5a..f19f941 100644 --- a/README.md +++ b/README.md @@ -105,7 +105,7 @@ client = Segment::Analytics.new(test: true) client.test_queue # => # -client.track(user_id: "foo", event: "bar") +client.track(user_id: 'foo', event: 'bar') client.test_queue.all # [ diff --git a/spec/segment/analytics/test_queue_spec.rb b/spec/segment/analytics/test_queue_spec.rb index 810a49e..c97431d 100644 --- a/spec/segment/analytics/test_queue_spec.rb +++ b/spec/segment/analytics/test_queue_spec.rb @@ -15,7 +15,7 @@ class Analytics let(:message) do { type: type, - foo: "bar" + foo: 'bar' } end @@ -140,21 +140,21 @@ class Analytics describe '#count' do let(:message) do { - type: "alias", - foo: "bar" + type: 'alias', + foo: 'bar' } end - it "returns 0" do + it 'returns 0' do expect(test_queue.count).to eq(0) end - it "returns 1" do + it 'returns 1' do test_queue << message expect(test_queue.count).to eq(1) end - it "returns 2" do + it 'returns 2' do test_queue << message test_queue << message expect(test_queue.count).to eq(2) @@ -164,29 +164,29 @@ class Analytics describe '#[]' do let(:message1) do { - type: "alias", - foo: "bar" + type: 'alias', + foo: 'bar' } end let(:message2) do { - type: "identify", - foo: "baz" + type: 'identify', + foo: 'baz' } end - it "returns message1" do + it 'returns message1' do test_queue << message1 expect(test_queue[0]).to eq(message1) end - it "returns message2" do + it 'returns message2' do test_queue << message2 expect(test_queue[0]).to eq(message2) end - it "returns message2" do + it 'returns message2' do test_queue << message1 test_queue << message2 expect(test_queue[1]).to eq(message2) @@ -196,12 +196,12 @@ class Analytics describe '#reset!' do let(:message) do { - type: "alias", - foo: "bar" + type: 'alias', + foo: 'bar' } end - it "returns message" do + it 'returns message' do test_queue << message expect(test_queue.count).to eq(1) test_queue.reset! From 3ff9d0186dffbed1933ef30acbff792df5a4e804 Mon Sep 17 00:00:00 2001 From: Shane Duvall Date: Tue, 13 Apr 2021 14:37:54 -0500 Subject: [PATCH 120/179] Update version for gem --- History.md | 6 ++++++ lib/segment/analytics/version.rb | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/History.md b/History.md index 3d888f2..6732570 100644 --- a/History.md +++ b/History.md @@ -1,3 +1,9 @@ +2.3.1 / 2020-04-13 +================== + + * Add test option for easier testing (https://github.com/segmentio/analytics-ruby/pull/222) + + 2.3.0 / 2020-03-26 ================== diff --git a/lib/segment/analytics/version.rb b/lib/segment/analytics/version.rb index 62d2fa0..1191fc4 100644 --- a/lib/segment/analytics/version.rb +++ b/lib/segment/analytics/version.rb @@ -1,5 +1,5 @@ module Segment class Analytics - VERSION = '2.3.0' + VERSION = '2.3.1' end end From 98adba6ba2c1bc7e87f446c6b0d08d99e52f6bd7 Mon Sep 17 00:00:00 2001 From: annc128 Date: Wed, 28 Apr 2021 17:39:11 -0700 Subject: [PATCH 121/179] enable-overriding-transport-options Pass options when initializing Transport --- lib/segment/analytics/worker.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/segment/analytics/worker.rb b/lib/segment/analytics/worker.rb index a313eed..7e73fae 100644 --- a/lib/segment/analytics/worker.rb +++ b/lib/segment/analytics/worker.rb @@ -29,7 +29,7 @@ def initialize(queue, write_key, options = {}) batch_size = options[:batch_size] || Defaults::MessageBatch::MAX_SIZE @batch = MessageBatch.new(batch_size) @lock = Mutex.new - @transport = Transport.new + @transport = Transport.new(options) end # public: Continuously runs the loop to check for new events From 156f7896e40ef76874b06d106b27093a01c32076 Mon Sep 17 00:00:00 2001 From: annc128 Date: Wed, 28 Apr 2021 18:18:17 -0700 Subject: [PATCH 122/179] enable-overriding-transport-options Bump version --- History.md | 5 +++++ lib/segment/analytics/version.rb | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/History.md b/History.md index 6732570..774346f 100644 --- a/History.md +++ b/History.md @@ -1,3 +1,8 @@ +2.3.2 / 2020-04-28 +================== + +* Enable overriding options in Transport () + 2.3.1 / 2020-04-13 ================== diff --git a/lib/segment/analytics/version.rb b/lib/segment/analytics/version.rb index 1191fc4..3c375ed 100644 --- a/lib/segment/analytics/version.rb +++ b/lib/segment/analytics/version.rb @@ -1,5 +1,5 @@ module Segment class Analytics - VERSION = '2.3.1' + VERSION = '2.3.2' end end From 6bc8d8fb1037a5fec9418c1705e6d071e71bc693 Mon Sep 17 00:00:00 2001 From: Shane Duvall Date: Wed, 5 May 2021 14:46:19 -0500 Subject: [PATCH 123/179] Version update 2.3.3 --- History.md | 5 +++++ lib/segment/analytics/version.rb | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/History.md b/History.md index 774346f..736f01a 100644 --- a/History.md +++ b/History.md @@ -1,3 +1,8 @@ +2.3.3 / 2020-05-05 +================== + +* Enable overriding transport Pass options when initializing Transport () + 2.3.2 / 2020-04-28 ================== diff --git a/lib/segment/analytics/version.rb b/lib/segment/analytics/version.rb index 3c375ed..8ff9daa 100644 --- a/lib/segment/analytics/version.rb +++ b/lib/segment/analytics/version.rb @@ -1,5 +1,5 @@ module Segment class Analytics - VERSION = '2.3.2' + VERSION = '2.3.3' end end From 8fa2e1c92afc810d44da1993a85751be082667ed Mon Sep 17 00:00:00 2001 From: Pooya Jaferian Date: Wed, 5 May 2021 12:52:06 -0700 Subject: [PATCH 124/179] Update History.md --- History.md | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/History.md b/History.md index 736f01a..b3534db 100644 --- a/History.md +++ b/History.md @@ -1,12 +1,8 @@ -2.3.3 / 2020-05-05 +2.4.0 / 2020-05-05 ================== * Enable overriding transport Pass options when initializing Transport () -2.3.2 / 2020-04-28 -================== - -* Enable overriding options in Transport () 2.3.1 / 2020-04-13 ================== From 9b1dc2ff4c176b526d7fbe17328db501104cb2a3 Mon Sep 17 00:00:00 2001 From: Pooya Jaferian Date: Wed, 5 May 2021 12:53:36 -0700 Subject: [PATCH 125/179] Update version.rb --- lib/segment/analytics/version.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/segment/analytics/version.rb b/lib/segment/analytics/version.rb index 8ff9daa..199992a 100644 --- a/lib/segment/analytics/version.rb +++ b/lib/segment/analytics/version.rb @@ -1,5 +1,5 @@ module Segment class Analytics - VERSION = '2.3.3' + VERSION = '2.4.0' end end From a3c83a245aed6c661ecf8f4c3e4cafd02814a072 Mon Sep 17 00:00:00 2001 From: David Hughes Date: Wed, 9 Jun 2021 12:59:48 -0700 Subject: [PATCH 126/179] Fix test queue reset! documentation I ran into this with version 2.4.0, not sure if this is a just a typo or if the interface changed between versions. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f19f941..1cfebc0 100644 --- a/README.md +++ b/README.md @@ -150,7 +150,7 @@ client.test_queue.identify # => [] client.test_queue.page # => [] client.test_queue.screen # => [] -client.reset! +client.test_queue.reset! client.test_queue.all # => [] ``` From 9d230d97a11dfb58c04cce4cd40376e910b94545 Mon Sep 17 00:00:00 2001 From: Pooya Jaferian Date: Wed, 16 Jun 2021 11:09:56 -0700 Subject: [PATCH 127/179] Update README.md --- README.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/README.md b/README.md index 1cfebc0..7d83122 100644 --- a/README.md +++ b/README.md @@ -90,10 +90,6 @@ There are a few calls available, please check the documentation section. Documentation is available at [segment.com/docs/sources/server/ruby](https://segment.com/docs/sources/server/ruby/) -## Testing - -You can use the `stub: true` option to Segment::Analytics.new to cause all requests to be stubbed, making it easier to test with this library. - ### Test Queue You can use the `test: true` option to Segment::Analytics.new to cause all requests to be saved to a test queue until manually reset. All events will process as specified by the configuration, and they will also be stored in a separate queue for inspection during testing. From 675b296393d304aa3903737613f5dbb4e643591c Mon Sep 17 00:00:00 2001 From: MaxenceFlatlooker Date: Mon, 21 Jun 2021 16:38:31 +0200 Subject: [PATCH 128/179] not enqueuing test action in real queue --- lib/segment/analytics/client.rb | 9 +++++---- spec/segment/analytics/client_spec.rb | 15 +++++++++++++++ 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/lib/segment/analytics/client.rb b/lib/segment/analytics/client.rb index fc81cce..9e626d9 100644 --- a/lib/segment/analytics/client.rb +++ b/lib/segment/analytics/client.rb @@ -161,7 +161,10 @@ def enqueue(action) # add our request id for tracing purposes action[:messageId] ||= uid - test_queue << action if @test + if @test + test_queue << action + return true + end if @queue.length < @max_queue_size @queue << action @@ -170,9 +173,7 @@ def enqueue(action) true else logger.warn( - 'Queue is full, dropping events. The :max_queue_size ' \ - 'configuration parameter can be increased to prevent this from ' \ - 'happening.' + 'Queue is full, dropping events. The :max_queue_size configuration parameter can be increased to prevent this from happening.' ) false end diff --git a/spec/segment/analytics/client_spec.rb b/spec/segment/analytics/client_spec.rb index c88e7ab..0358bb2 100644 --- a/spec/segment/analytics/client_spec.rb +++ b/spec/segment/analytics/client_spec.rb @@ -342,6 +342,21 @@ class Analytics expect(message[:integrations][:Salesforce]).to eq(false) end end + + it 'does not enqueue the action in test mode' do + client.instance_variable_set(:@test, true) + client.test_queue + test_queue = client.instance_variable_get(:@test_queue) + + %i[track screen page group identify alias].each do |s| + old_test_queue_size = test_queue.count + queue_size = queue.length + client.send(s, data) + + expect(queue.length).to eq(queue_size) # The "real" queue size should not change in test mode + expect(test_queue.count).to_not eq(old_test_queue_size) # The "test" queue size should change in test mode + end + end end end end From 18a8408097319d01889de45d6a159317ce961936 Mon Sep 17 00:00:00 2001 From: Diego Silva <8826716+diego-silva@users.noreply.github.com> Date: Wed, 25 Aug 2021 17:54:04 -0300 Subject: [PATCH 129/179] Correct dates on History.md Versions 2.3.0 up to 2.4.0 were released in 2021, not in 2020. --- History.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/History.md b/History.md index b3534db..6417a98 100644 --- a/History.md +++ b/History.md @@ -1,16 +1,16 @@ -2.4.0 / 2020-05-05 +2.4.0 / 2021-05-05 ================== * Enable overriding transport Pass options when initializing Transport () -2.3.1 / 2020-04-13 +2.3.1 / 2021-04-13 ================== * Add test option for easier testing (https://github.com/segmentio/analytics-ruby/pull/222) -2.3.0 / 2020-03-26 +2.3.0 / 2021-03-26 ================== * [Improvement](https://github.com/segmentio/analytics-ruby/pull/225): Update timestamp for sub-millisecond reporting From c908070c9da16f67747a74d5f762352c59a3616d Mon Sep 17 00:00:00 2001 From: Tyler Goodwin Date: Thu, 14 Oct 2021 09:18:57 +1100 Subject: [PATCH 130/179] Fix for empty user_id or anonymous_id not throwing errors Events were being swallowed if the user id was an empty string. --- lib/segment/analytics/field_parser.rb | 13 +++++++++---- spec/segment/analytics_spec.rb | 2 ++ 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/lib/segment/analytics/field_parser.rb b/lib/segment/analytics/field_parser.rb index 7918710..56e2f12 100644 --- a/lib/segment/analytics/field_parser.rb +++ b/lib/segment/analytics/field_parser.rb @@ -160,9 +160,10 @@ def parse_common_fields(fields) end def check_user_id!(fields) - unless fields[:user_id] || fields[:anonymous_id] - raise ArgumentError, 'Must supply either user_id or anonymous_id' - end + return unless blank?(fields[:user_id]) + return unless blank?(fields[:anonymous_id]) + + raise ArgumentError, 'Must supply either user_id or anonymous_id' end def check_timestamp!(timestamp) @@ -178,11 +179,15 @@ def add_context!(context) # obj - String|Number that must be non-blank # name - Name of the validated value def check_presence!(obj, name) - if obj.nil? || (obj.is_a?(String) && obj.empty?) + if blank?(obj) raise ArgumentError, "#{name} must be given" end end + def blank?(obj) + obj.nil? || (obj.is_a?(String) && obj.empty?) + end + def check_is_hash!(obj, name) raise ArgumentError, "#{name} must be a Hash" unless obj.is_a? Hash end diff --git a/spec/segment/analytics_spec.rb b/spec/segment/analytics_spec.rb index 17961b2..7d20611 100644 --- a/spec/segment/analytics_spec.rb +++ b/spec/segment/analytics_spec.rb @@ -12,6 +12,8 @@ class Analytics it 'errors without user_id or anonymous_id' do expect { analytics.track :event => 'event' }.to raise_error(ArgumentError) + expect { analytics.track :event => 'event', user_id: ''}.to raise_error(ArgumentError) + expect { analytics.track :event => 'event', anonymous_id:''}.to raise_error(ArgumentError) expect { analytics.track :event => 'event', user_id: '1234' }.to_not raise_error(ArgumentError) expect { analytics.track :event => 'event', anonymous_id: '2345' }.to_not raise_error(ArgumentError) end From 36f82d1618f4b0c6e202674bbcf4478ebe9c23ca Mon Sep 17 00:00:00 2001 From: "Shane L. Duvall" Date: Mon, 17 Jan 2022 14:43:17 -0600 Subject: [PATCH 131/179] Create ruby.yml --- .github/workflows/ruby.yml | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 .github/workflows/ruby.yml diff --git a/.github/workflows/ruby.yml b/.github/workflows/ruby.yml new file mode 100644 index 0000000..2006e3f --- /dev/null +++ b/.github/workflows/ruby.yml @@ -0,0 +1,35 @@ +# This workflow uses actions that are not certified by GitHub. +# They are provided by a third-party and are governed by +# separate terms of service, privacy policy, and support +# documentation. +# This workflow will download a prebuilt Ruby version, install dependencies and run tests with Rake +# For more information see: https://github.com/marketplace/actions/setup-ruby-jruby-and-truffleruby + +name: Ruby + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +jobs: + test: + + runs-on: ubuntu-latest + strategy: + matrix: + ruby-version: ['2.4.3', '2.5', '2.6', '2.7.3'] + + steps: + - uses: actions/checkout@v2 + - name: Set up Ruby + # To automatically get bug fixes and new Ruby versions for ruby/setup-ruby, + # change this to (see https://github.com/ruby/setup-ruby#versioning): + # uses: ruby/setup-ruby@v1 + uses: ruby/setup-ruby@473e4d8fe5dd94ee328fdfca9f8c9c7afc9dae5e + with: + ruby-version: ${{ matrix.ruby-version }} + bundler-cache: true # runs 'bundle install' and caches installed gems automatically + - name: Run tests + run: bundle exec rake From 5f4328e6b2b0d844b53c64ace53d1fe7de6282c4 Mon Sep 17 00:00:00 2001 From: Shane Duvall Date: Mon, 17 Jan 2022 14:59:08 -0600 Subject: [PATCH 132/179] Update test version --- .github/workflows/ruby.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ruby.yml b/.github/workflows/ruby.yml index 2006e3f..905aa5c 100644 --- a/.github/workflows/ruby.yml +++ b/.github/workflows/ruby.yml @@ -19,7 +19,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - ruby-version: ['2.4.3', '2.5', '2.6', '2.7.3'] + ruby-version: ['2.4.3', '2.5', '2.6', '2.7.2'] steps: - uses: actions/checkout@v2 From 34a5be7950d0f6bd31939bb873f4360dcb063e18 Mon Sep 17 00:00:00 2001 From: Shane Duvall Date: Mon, 17 Jan 2022 16:44:21 -0600 Subject: [PATCH 133/179] rubocop update parameters; fix syntax reported by rubocop --- .rubocop_todo.yml | 2 +- spec/segment/analytics_spec.rb | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 11a76cc..8024cb6 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -13,7 +13,7 @@ Metrics/AbcSize: # Offense count: 1 # Configuration parameters: CountComments. Metrics/ClassLength: - Max: 114 + Max: 120 # Offense count: 1 Metrics/CyclomaticComplexity: diff --git a/spec/segment/analytics_spec.rb b/spec/segment/analytics_spec.rb index 7d20611..bf4dbed 100644 --- a/spec/segment/analytics_spec.rb +++ b/spec/segment/analytics_spec.rb @@ -12,8 +12,8 @@ class Analytics it 'errors without user_id or anonymous_id' do expect { analytics.track :event => 'event' }.to raise_error(ArgumentError) - expect { analytics.track :event => 'event', user_id: ''}.to raise_error(ArgumentError) - expect { analytics.track :event => 'event', anonymous_id:''}.to raise_error(ArgumentError) + expect { analytics.track :event => 'event', user_id: '' }.to raise_error(ArgumentError) + expect { analytics.track :event => 'event', anonymous_id: '' }.to raise_error(ArgumentError) expect { analytics.track :event => 'event', user_id: '1234' }.to_not raise_error(ArgumentError) expect { analytics.track :event => 'event', anonymous_id: '2345' }.to_not raise_error(ArgumentError) end From c03164ba338f3593ca5b1b3f4043cb0e28366fb8 Mon Sep 17 00:00:00 2001 From: Shane Duvall Date: Mon, 17 Jan 2022 16:51:29 -0600 Subject: [PATCH 134/179] rubocop syntax update --- lib/segment/analytics/field_parser.rb | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/segment/analytics/field_parser.rb b/lib/segment/analytics/field_parser.rb index 56e2f12..602f4bb 100644 --- a/lib/segment/analytics/field_parser.rb +++ b/lib/segment/analytics/field_parser.rb @@ -179,9 +179,7 @@ def add_context!(context) # obj - String|Number that must be non-blank # name - Name of the validated value def check_presence!(obj, name) - if blank?(obj) - raise ArgumentError, "#{name} must be given" - end + raise ArgumentError, "#{name} must be given" if blank?(obj) end def blank?(obj) From 48bac002f7d6a8638337e1c8da61fc37c3f00c1a Mon Sep 17 00:00:00 2001 From: Shane Duvall Date: Mon, 17 Jan 2022 19:53:25 -0600 Subject: [PATCH 135/179] Add ruby gems config --- .github/workflows/gem-push.yml | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 .github/workflows/gem-push.yml diff --git a/.github/workflows/gem-push.yml b/.github/workflows/gem-push.yml new file mode 100644 index 0000000..8518638 --- /dev/null +++ b/.github/workflows/gem-push.yml @@ -0,0 +1,33 @@ +name: Ruby Gem + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +jobs: + build: + name: Build + Publish + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + steps: + - uses: actions/checkout@v2 + - name: Set up Ruby 2.7 + uses: actions/setup-ruby@v1 + with: + ruby-version: 2.7.x + + - name: Publish to RubyGems + run: | + mkdir -p $HOME/.gem + touch $HOME/.gem/credentials + chmod 0600 $HOME/.gem/credentials + printf -- "---\n:rubygems_api_key: ${GEM_HOST_API_KEY}\n" > $HOME/.gem/credentials + gem build *.gemspec + gem push *.gem + env: + GEM_HOST_API_KEY: "${{secrets.RUBYGEMS_AUTH_TOKEN}}" From 5d53b6c7964ed803c45d34ff80dd801cfeda305e Mon Sep 17 00:00:00 2001 From: Shane Duvall Date: Mon, 17 Jan 2022 20:22:35 -0600 Subject: [PATCH 136/179] Add GPR into config --- .github/workflows/gem-push.yml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/.github/workflows/gem-push.yml b/.github/workflows/gem-push.yml index 8518638..ead605f 100644 --- a/.github/workflows/gem-push.yml +++ b/.github/workflows/gem-push.yml @@ -21,6 +21,18 @@ jobs: with: ruby-version: 2.7.x + - name: Publish to GPR + run: | + mkdir -p $HOME/.gem + touch $HOME/.gem/credentials + chmod 0600 $HOME/.gem/credentials + printf -- "---\n:github: ${GEM_HOST_API_KEY}\n" > $HOME/.gem/credentials + gem build *.gemspec + gem push --KEY github --host https://rubygems.pkg.github.com/${OWNER} *.gem + env: + GEM_HOST_API_KEY: "Bearer ${{secrets.GITHUB_TOKEN}}" + OWNER: ${{ github.repository_owner }} + - name: Publish to RubyGems run: | mkdir -p $HOME/.gem From 9e61564f0557850a695735847024aee8ba37b27e Mon Sep 17 00:00:00 2001 From: Shane Duvall Date: Wed, 23 Feb 2022 13:36:55 -0600 Subject: [PATCH 137/179] Resolve Issue #247, change milliseconds from reporting 6 places back to 3 --- lib/segment/analytics/utils.rb | 2 +- lib/segment/analytics/version.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/segment/analytics/utils.rb b/lib/segment/analytics/utils.rb index 745a5e4..2c2d86a 100644 --- a/lib/segment/analytics/utils.rb +++ b/lib/segment/analytics/utils.rb @@ -65,7 +65,7 @@ def datetime_in_iso8601(datetime) end def time_in_iso8601(time) - "#{time.strftime('%Y-%m-%dT%H:%M:%S.%6N')}#{formatted_offset(time, true, 'Z')}" + "#{time.strftime('%Y-%m-%dT%H:%M:%S.%3N')}#{formatted_offset(time, true, 'Z')}" end def date_in_iso8601(date) diff --git a/lib/segment/analytics/version.rb b/lib/segment/analytics/version.rb index 199992a..43460ff 100644 --- a/lib/segment/analytics/version.rb +++ b/lib/segment/analytics/version.rb @@ -1,5 +1,5 @@ module Segment class Analytics - VERSION = '2.4.0' + VERSION = '2.4.1' end end From d188a3943d05b8f0ff655f0c7eb0cdd3c1a60555 Mon Sep 17 00:00:00 2001 From: Raghu Bhupatiraju Date: Wed, 20 Apr 2022 13:59:54 -0700 Subject: [PATCH 138/179] Mentioning about stubbing during tests. --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 7d83122..e6cdb6b 100644 --- a/README.md +++ b/README.md @@ -161,6 +161,7 @@ RSpec.configure do |config| end ``` +And also to stub actions use `stub: true` along with `test: true` so that it doesn't send any real calls during specs. ## License ``` From d6d9a33e30cf396b597315372e8f2ce8adf49400 Mon Sep 17 00:00:00 2001 From: Raghu Bhupatiraju Date: Wed, 20 Apr 2022 16:23:52 -0700 Subject: [PATCH 139/179] Bumping the version --- lib/segment/analytics/version.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/segment/analytics/version.rb b/lib/segment/analytics/version.rb index 43460ff..c292749 100644 --- a/lib/segment/analytics/version.rb +++ b/lib/segment/analytics/version.rb @@ -1,5 +1,5 @@ module Segment class Analytics - VERSION = '2.4.1' + VERSION = '2.4.2' end end From 71b30778a0be495154ebd5bd5efd515f1c232fbe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=B2=20Rebughini?= Date: Tue, 10 Jan 2023 15:01:18 +0100 Subject: [PATCH 140/179] Add frozen_string_literals magic comment This low effort change greatly changes the outcome of how many strings are allocated and, most importantly, retained by the gem. --- lib/analytics-ruby.rb | 2 ++ lib/segment.rb | 2 ++ lib/segment/analytics.rb | 2 ++ lib/segment/analytics/backoff_policy.rb | 2 ++ lib/segment/analytics/client.rb | 2 ++ lib/segment/analytics/defaults.rb | 2 ++ lib/segment/analytics/field_parser.rb | 2 ++ lib/segment/analytics/logging.rb | 2 ++ lib/segment/analytics/message_batch.rb | 2 ++ lib/segment/analytics/response.rb | 2 ++ lib/segment/analytics/test_queue.rb | 2 ++ lib/segment/analytics/transport.rb | 2 ++ lib/segment/analytics/utils.rb | 2 ++ lib/segment/analytics/version.rb | 2 ++ lib/segment/analytics/worker.rb | 2 ++ spec/isolated/json_example.rb | 2 ++ spec/isolated/with_active_support.rb | 2 ++ spec/isolated/with_active_support_and_oj.rb | 2 ++ spec/isolated/with_oj.rb | 2 ++ spec/segment/analytics/backoff_policy_spec.rb | 2 ++ spec/segment/analytics/client_spec.rb | 2 ++ spec/segment/analytics/message_batch_spec.rb | 2 ++ spec/segment/analytics/response_spec.rb | 2 ++ spec/segment/analytics/test_queue_spec.rb | 2 ++ spec/segment/analytics/transport_spec.rb | 2 ++ spec/segment/analytics/worker_spec.rb | 2 ++ spec/segment/analytics_spec.rb | 2 ++ spec/spec_helper.rb | 2 ++ 28 files changed, 56 insertions(+) diff --git a/lib/analytics-ruby.rb b/lib/analytics-ruby.rb index 801ad01..1c99ea1 100644 --- a/lib/analytics-ruby.rb +++ b/lib/analytics-ruby.rb @@ -1 +1,3 @@ +# frozen_string_literal: true + require 'segment' diff --git a/lib/segment.rb b/lib/segment.rb index 1465165..0efdd70 100644 --- a/lib/segment.rb +++ b/lib/segment.rb @@ -1 +1,3 @@ +# frozen_string_literal: true + require 'segment/analytics' diff --git a/lib/segment/analytics.rb b/lib/segment/analytics.rb index ca83353..707e7c4 100644 --- a/lib/segment/analytics.rb +++ b/lib/segment/analytics.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'segment/analytics/version' require 'segment/analytics/defaults' require 'segment/analytics/utils' diff --git a/lib/segment/analytics/backoff_policy.rb b/lib/segment/analytics/backoff_policy.rb index 394a2c1..e6033b1 100644 --- a/lib/segment/analytics/backoff_policy.rb +++ b/lib/segment/analytics/backoff_policy.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'segment/analytics/defaults' module Segment diff --git a/lib/segment/analytics/client.rb b/lib/segment/analytics/client.rb index 9e626d9..9589f69 100644 --- a/lib/segment/analytics/client.rb +++ b/lib/segment/analytics/client.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'thread' require 'time' diff --git a/lib/segment/analytics/defaults.rb b/lib/segment/analytics/defaults.rb index 8562346..aa32697 100644 --- a/lib/segment/analytics/defaults.rb +++ b/lib/segment/analytics/defaults.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Segment class Analytics module Defaults diff --git a/lib/segment/analytics/field_parser.rb b/lib/segment/analytics/field_parser.rb index 602f4bb..a7364ec 100644 --- a/lib/segment/analytics/field_parser.rb +++ b/lib/segment/analytics/field_parser.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Segment class Analytics # Handles parsing fields according to the Segment Spec diff --git a/lib/segment/analytics/logging.rb b/lib/segment/analytics/logging.rb index f7aaa63..5449878 100644 --- a/lib/segment/analytics/logging.rb +++ b/lib/segment/analytics/logging.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'logger' module Segment diff --git a/lib/segment/analytics/message_batch.rb b/lib/segment/analytics/message_batch.rb index 1522463..15eef43 100644 --- a/lib/segment/analytics/message_batch.rb +++ b/lib/segment/analytics/message_batch.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'forwardable' require 'segment/analytics/logging' diff --git a/lib/segment/analytics/response.rb b/lib/segment/analytics/response.rb index 7306ac0..c31116a 100644 --- a/lib/segment/analytics/response.rb +++ b/lib/segment/analytics/response.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Segment class Analytics class Response diff --git a/lib/segment/analytics/test_queue.rb b/lib/segment/analytics/test_queue.rb index af31558..c93cad2 100644 --- a/lib/segment/analytics/test_queue.rb +++ b/lib/segment/analytics/test_queue.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Segment class Analytics class TestQueue diff --git a/lib/segment/analytics/transport.rb b/lib/segment/analytics/transport.rb index 59697be..6ee14d8 100644 --- a/lib/segment/analytics/transport.rb +++ b/lib/segment/analytics/transport.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'segment/analytics/defaults' require 'segment/analytics/utils' require 'segment/analytics/response' diff --git a/lib/segment/analytics/utils.rb b/lib/segment/analytics/utils.rb index 2c2d86a..62ee69b 100644 --- a/lib/segment/analytics/utils.rb +++ b/lib/segment/analytics/utils.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'securerandom' module Segment diff --git a/lib/segment/analytics/version.rb b/lib/segment/analytics/version.rb index c292749..eaabbdf 100644 --- a/lib/segment/analytics/version.rb +++ b/lib/segment/analytics/version.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Segment class Analytics VERSION = '2.4.2' diff --git a/lib/segment/analytics/worker.rb b/lib/segment/analytics/worker.rb index 7e73fae..6a7d68e 100644 --- a/lib/segment/analytics/worker.rb +++ b/lib/segment/analytics/worker.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'segment/analytics/defaults' require 'segment/analytics/message_batch' require 'segment/analytics/transport' diff --git a/spec/isolated/json_example.rb b/spec/isolated/json_example.rb index c693b55..5f80533 100644 --- a/spec/isolated/json_example.rb +++ b/spec/isolated/json_example.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + RSpec.shared_examples 'message_batch_json' do it 'MessageBatch generates proper JSON' do batch = Segment::Analytics::MessageBatch.new(100) diff --git a/spec/isolated/with_active_support.rb b/spec/isolated/with_active_support.rb index 86178ae..cfd9f2c 100644 --- a/spec/isolated/with_active_support.rb +++ b/spec/isolated/with_active_support.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' require 'isolated/json_example' diff --git a/spec/isolated/with_active_support_and_oj.rb b/spec/isolated/with_active_support_and_oj.rb index 18724e3..135be31 100644 --- a/spec/isolated/with_active_support_and_oj.rb +++ b/spec/isolated/with_active_support_and_oj.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' require 'isolated/json_example' diff --git a/spec/isolated/with_oj.rb b/spec/isolated/with_oj.rb index ad3f376..7519238 100644 --- a/spec/isolated/with_oj.rb +++ b/spec/isolated/with_oj.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' require 'isolated/json_example' diff --git a/spec/segment/analytics/backoff_policy_spec.rb b/spec/segment/analytics/backoff_policy_spec.rb index 75dc9fc..25ef05e 100644 --- a/spec/segment/analytics/backoff_policy_spec.rb +++ b/spec/segment/analytics/backoff_policy_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' module Segment diff --git a/spec/segment/analytics/client_spec.rb b/spec/segment/analytics/client_spec.rb index 0358bb2..99c973b 100644 --- a/spec/segment/analytics/client_spec.rb +++ b/spec/segment/analytics/client_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' module Segment diff --git a/spec/segment/analytics/message_batch_spec.rb b/spec/segment/analytics/message_batch_spec.rb index 4b7b66f..98f7be7 100644 --- a/spec/segment/analytics/message_batch_spec.rb +++ b/spec/segment/analytics/message_batch_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' module Segment diff --git a/spec/segment/analytics/response_spec.rb b/spec/segment/analytics/response_spec.rb index 7e09971..bb673db 100644 --- a/spec/segment/analytics/response_spec.rb +++ b/spec/segment/analytics/response_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' module Segment diff --git a/spec/segment/analytics/test_queue_spec.rb b/spec/segment/analytics/test_queue_spec.rb index c97431d..4aa4f77 100644 --- a/spec/segment/analytics/test_queue_spec.rb +++ b/spec/segment/analytics/test_queue_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' module Segment diff --git a/spec/segment/analytics/transport_spec.rb b/spec/segment/analytics/transport_spec.rb index 56b551f..b73ce9d 100644 --- a/spec/segment/analytics/transport_spec.rb +++ b/spec/segment/analytics/transport_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' module Segment diff --git a/spec/segment/analytics/worker_spec.rb b/spec/segment/analytics/worker_spec.rb index 0ca4d49..5111943 100644 --- a/spec/segment/analytics/worker_spec.rb +++ b/spec/segment/analytics/worker_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' module Segment diff --git a/spec/segment/analytics_spec.rb b/spec/segment/analytics_spec.rb index bf4dbed..04431de 100644 --- a/spec/segment/analytics_spec.rb +++ b/spec/segment/analytics_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' module Segment diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index a0f9247..b1d8435 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # https://github.com/codecov/codecov-ruby#usage require 'simplecov' SimpleCov.start From 0484c52f9e7bfe4966f8029e6f0ecac78fe65e3c Mon Sep 17 00:00:00 2001 From: Orien Madgwick <497874+orien@users.noreply.github.com> Date: Tue, 24 Jan 2023 16:13:37 +1100 Subject: [PATCH 141/179] CI: add Ruby 3.0, 3.1, and 3.2 to the test matrix --- .github/workflows/ruby.yml | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ruby.yml b/.github/workflows/ruby.yml index 905aa5c..02d2de1 100644 --- a/.github/workflows/ruby.yml +++ b/.github/workflows/ruby.yml @@ -19,15 +19,12 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - ruby-version: ['2.4.3', '2.5', '2.6', '2.7.2'] + ruby-version: ['2.4', '2.5', '2.6', '2.7', '3.0', '3.1', '3.2'] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Ruby - # To automatically get bug fixes and new Ruby versions for ruby/setup-ruby, - # change this to (see https://github.com/ruby/setup-ruby#versioning): - # uses: ruby/setup-ruby@v1 - uses: ruby/setup-ruby@473e4d8fe5dd94ee328fdfca9f8c9c7afc9dae5e + uses: ruby/setup-ruby@v1 with: ruby-version: ${{ matrix.ruby-version }} bundler-cache: true # runs 'bundle install' and caches installed gems automatically From f18e6491439aef0c37d3c6db637954430e6184d9 Mon Sep 17 00:00:00 2001 From: Orien Madgwick <497874+orien@users.noreply.github.com> Date: Tue, 24 Jan 2023 18:44:24 +1100 Subject: [PATCH 142/179] Update development dependencies --- analytics-ruby.gemspec | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/analytics-ruby.gemspec b/analytics-ruby.gemspec index 60d9155..aad3553 100644 --- a/analytics-ruby.gemspec +++ b/analytics-ruby.gemspec @@ -19,15 +19,13 @@ Gem::Specification.new do |spec| spec.add_development_dependency 'commander', '~> 4.4' # Used in specs - spec.add_development_dependency 'rake', '~> 10.3' + spec.add_development_dependency 'rake', '~> 13.0' spec.add_development_dependency 'rspec', '~> 3.0' - spec.add_development_dependency 'tzinfo', '1.2.1' - spec.add_development_dependency 'activesupport', '~> 4.1.11' - if RUBY_VERSION >= '2.0' && RUBY_PLATFORM != 'java' + spec.add_development_dependency 'tzinfo', '~> 1.2' + spec.add_development_dependency 'activesupport', '~> 5.2.0' + if RUBY_PLATFORM != 'java' spec.add_development_dependency 'oj', '~> 3.6.2' end - if RUBY_VERSION >= '2.1' - spec.add_development_dependency 'rubocop', '~> 0.51.0' - end - spec.add_development_dependency 'codecov', '~> 0.1.4' + spec.add_development_dependency 'rubocop', '~> 1.0' + spec.add_development_dependency 'codecov', '~> 0.6' end From d258c1cb27f9892c9d2feab82f8a443a124e5ef4 Mon Sep 17 00:00:00 2001 From: Orien Madgwick <497874+orien@users.noreply.github.com> Date: Tue, 24 Jan 2023 19:31:43 +1100 Subject: [PATCH 143/179] Rubocop: resolve Style/HashSyntax --- bin/analytics | 72 +++++++++++++++++++++++++-------------------------- 1 file changed, 36 insertions(+), 36 deletions(-) diff --git a/bin/analytics b/bin/analytics index 86f3511..1f4c19e 100755 --- a/bin/analytics +++ b/bin/analytics @@ -43,62 +43,62 @@ command :send do |c| c.action do |args, options| Analytics = Segment::Analytics.new({ - write_key: options.writeKey, - on_error: Proc.new { |status, msg| print msg } + :write_key => options.writeKey, + :on_error => Proc.new { |status, msg| print msg } }) case options.type when "track" Analytics.track({ - user_id: options.userId, - event: options.event, - anonymous_id: options.anonymousId, - properties: json_hash(options.properties), - context: json_hash(options.context), - integrations: json_hash(options.integrations) + :user_id => options.userId, + :event => options.event, + :anonymous_id => options.anonymousId, + :properties => json_hash(options.properties), + :context => json_hash(options.context), + :integrations => json_hash(options.integrations) }) when "page" Analytics.page({ - user_id: options.userId, - anonymous_id: options.anonymousId, - name: options.name, - properties: json_hash(options.properties), - context: json_hash(options.context), - integrations: json_hash(options.integrations) + :user_id => options.userId, + :anonymous_id => options.anonymousId, + :name => options.name, + :properties => json_hash(options.properties), + :context => json_hash(options.context), + :integrations => json_hash(options.integrations) }) when "screen" Analytics.screen({ - user_id: options.userId, - anonymous_id: options.anonymousId, - name: options.name, - properties: json_hash(options.properties), - context: json_hash(options.context), - integrations: json_hash(options.integrations) + :user_id => options.userId, + :anonymous_id => options.anonymousId, + :name => options.name, + :properties => json_hash(options.properties), + :context => json_hash(options.context), + :integrations => json_hash(options.integrations) }) when "identify" Analytics.identify({ - user_id: options.userId, - anonymous_id: options.anonymousId, - traits: json_hash(options.traits), - context: json_hash(options.context), - integrations: json_hash(options.integrations) + :user_id => options.userId, + :anonymous_id => options.anonymousId, + :traits => json_hash(options.traits), + :context => json_hash(options.context), + :integrations => json_hash(options.integrations) }) when "group" Analytics.group({ - user_id: options.userId, - anonymous_id: options.anonymousId, - group_id: options.groupId, - traits: json_hash(options.traits), - context: json_hash(options.context), - integrations: json_hash(options.integrations) + :user_id => options.userId, + :anonymous_id => options.anonymousId, + :group_id => options.groupId, + :traits => json_hash(options.traits), + :context => json_hash(options.context), + :integrations => json_hash(options.integrations) }) when "alias" Analytics.alias({ - previous_id: options.previousId, - user_id: options.userId, - anonymous_id: options.anonymousId, - context: json_hash(options.context), - integrations: json_hash(options.integrations) + :previous_id => options.previousId, + :user_id => options.userId, + :anonymous_id => options.anonymousId, + :context => json_hash(options.context), + :integrations => json_hash(options.integrations) }) else raise "Invalid Message Type #{options.type}" From 32e92e68b54fe3b69828c1eb9cdeabcedbcce8c8 Mon Sep 17 00:00:00 2001 From: Orien Madgwick <497874+orien@users.noreply.github.com> Date: Tue, 24 Jan 2023 20:08:10 +1100 Subject: [PATCH 144/179] Rubocop: update configuration --- .rubocop.yml | 14 ++-- .rubocop_todo.yml | 165 +++++++++++++++++++++++++++++++++++++++++----- Rakefile | 5 +- 3 files changed, 159 insertions(+), 25 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index a043e4f..ecabcc4 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1,10 +1,11 @@ inherit_from: .rubocop_todo.yml AllCops: - # Rubocop doesn't support 2.0, so we'll use the minimum available - TargetRubyVersion: 2.1 + TargetRubyVersion: '2.0' + SuggestExtensions: false + NewCops: disable -Layout/IndentHash: +Layout/FirstHashElementIndentation: EnforcedStyle: consistent Metrics/AbcSize: @@ -23,7 +24,7 @@ Metrics/CyclomaticComplexity: Exclude: - "spec/**/*.rb" -Metrics/LineLength: +Layout/LineLength: Exclude: - "spec/**/*.rb" @@ -40,16 +41,13 @@ Naming/FileName: - lib/analytics-ruby.rb # Gem name, added for easier Gemfile usage Naming/PredicateName: - NameWhitelist: + AllowedMethods: - is_requesting? # Can't be renamed, backwards compatibility Style/BlockDelimiters: Exclude: - 'spec/**/*' -Style/BracesAroundHashParameters: - Enabled: false - Style/DateTime: Exclude: - 'spec/**/*.rb' diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 8024cb6..c02e129 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -1,35 +1,170 @@ # This configuration was generated by # `rubocop --auto-gen-config` -# on 2019-10-06 02:55:44 +0530 using RuboCop version 0.51.0. +# on 2023-01-24 09:12:04 UTC using RuboCop version 1.44.0. # The point is for the user to remove these configuration records # one by one as the offenses are removed from the code base. # Note that changes in the inspected code, or installation of new # versions of RuboCop, may require this file to be generated again. +# Offense count: 2 +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: TreatCommentsAsGroupSeparators, ConsiderPunctuation, Include. +# Include: **/*.gemspec +Gemspec/OrderedDependencies: + Exclude: + - 'analytics-ruby.gemspec' + +# Offense count: 1 +# Configuration parameters: Severity, Include. +# Include: **/*.gemspec +Gemspec/RubyVersionGlobalsUsage: + Exclude: + - 'analytics-ruby.gemspec' + # Offense count: 4 +# This cop supports safe autocorrection (--autocorrect). +Layout/EmptyLineAfterGuardClause: + Exclude: + - 'lib/segment/analytics/client.rb' + - 'spec/spec_helper.rb' + +# Offense count: 3 +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: AllowMultipleStyles, EnforcedHashRocketStyle, EnforcedColonStyle, EnforcedLastArgumentHashStyle. +# SupportedHashRocketStyles: key, separator, table +# SupportedColonStyles: key, separator, table +# SupportedLastArgumentHashStyles: always_inspect, always_ignore, ignore_implicit, ignore_explicit +Layout/HashAlignment: + Exclude: + - 'spec/segment/analytics/client_spec.rb' + - 'spec/spec_helper.rb' + +# Offense count: 1 +# This cop supports safe autocorrection (--autocorrect). +Layout/SpaceAfterComma: + Exclude: + - 'Rakefile' + +# Offense count: 1 +# Configuration parameters: AllowedMethods. +# AllowedMethods: enums +Lint/ConstantDefinitionInBlock: + Exclude: + - 'bin/analytics' + +# Offense count: 2 +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: IgnoreEmptyBlocks, AllowUnusedKeywordArguments. +Lint/UnusedBlockArgument: + Exclude: + - 'bin/analytics' + +# Offense count: 3 +# Configuration parameters: AllowedMethods, AllowedPatterns, IgnoredMethods, CountRepeatedAttributes. Metrics/AbcSize: - Max: 24 + Max: 25 + +# Offense count: 3 +# Configuration parameters: CountComments, CountAsOne, ExcludedMethods, AllowedMethods, AllowedPatterns, IgnoredMethods. +# AllowedMethods: refine +Metrics/BlockLength: + Max: 76 # Offense count: 1 -# Configuration parameters: CountComments. +# Configuration parameters: CountComments, CountAsOne. Metrics/ClassLength: - Max: 120 + Max: 115 -# Offense count: 1 +# Offense count: 2 +# Configuration parameters: AllowedMethods, AllowedPatterns, IgnoredMethods. Metrics/CyclomaticComplexity: Max: 8 -# Offense count: 8 -# Configuration parameters: AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, IgnoredPatterns. -# URISchemes: http, https -Metrics/LineLength: - Max: 147 - -# Offense count: 10 -# Configuration parameters: CountComments. +# Offense count: 11 +# Configuration parameters: CountComments, CountAsOne, ExcludedMethods, AllowedMethods, AllowedPatterns, IgnoredMethods. Metrics/MethodLength: Max: 16 # Offense count: 1 -Metrics/PerceivedComplexity: - Max: 8 +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: PreferredName. +Naming/RescuedExceptionsVariableName: + Exclude: + - 'spec/spec_helper.rb' + +# Offense count: 1 +# This cop supports safe autocorrection (--autocorrect). +Style/ExpandPathArguments: + Exclude: + - 'analytics-ruby.gemspec' + +# Offense count: 6 +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: MaxUnannotatedPlaceholdersAllowed, AllowedMethods, AllowedPatterns, IgnoredMethods. +# SupportedStyles: annotated, template, unannotated +Style/FormatStringToken: + EnforcedStyle: unannotated + +# Offense count: 1 +# This cop supports unsafe autocorrection (--autocorrect-all). +Style/GlobalStdStream: + Exclude: + - 'lib/segment/analytics/logging.rb' + +# Offense count: 4 +# This cop supports safe autocorrection (--autocorrect). +Style/IfUnlessModifier: + Exclude: + - 'analytics-ruby.gemspec' + - 'bin/analytics' + - 'lib/segment/analytics/client.rb' + +# Offense count: 1 +Style/MixinUsage: + Exclude: + - 'spec/spec_helper.rb' + +# Offense count: 2 +# Configuration parameters: AllowedMethods. +# AllowedMethods: respond_to_missing? +Style/OptionalBooleanParameter: + Exclude: + - 'lib/segment/analytics/utils.rb' + +# Offense count: 1 +# This cop supports safe autocorrection (--autocorrect). +Style/Proc: + Exclude: + - 'bin/analytics' + +# Offense count: 1 +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: AllowMultipleReturnValues. +Style/RedundantReturn: + Exclude: + - 'bin/analytics' + +# Offense count: 8 +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: EnforcedStyle, ConsistentQuotesInMultiline. +# SupportedStyles: single_quotes, double_quotes +Style/StringLiterals: + Exclude: + - 'Rakefile' + - 'analytics-ruby.gemspec' + - 'bin/analytics' + +# Offense count: 1 +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: EnforcedStyleForMultiline. +# SupportedStylesForMultiline: comma, consistent_comma, no_comma +Style/TrailingCommaInArrayLiteral: + Exclude: + - 'Rakefile' + +# Offense count: 2 +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, AllowedPatterns, IgnoredPatterns. +# URISchemes: http, https +Layout/LineLength: + Max: 147 diff --git a/Rakefile b/Rakefile index 0d3f11a..bb22a9f 100644 --- a/Rakefile +++ b/Rakefile @@ -18,8 +18,9 @@ Dir.glob('spec/isolated/**/*.rb').each do |isolated_test_path| default_tasks << isolated_test_path end -# Rubocop doesn't support < 2.1 -if RUBY_VERSION >= "2.1" +# Older versions of Rubocop don't support a target Ruby version of 2.1 +require 'rubocop/version' +if RuboCop::Version::STRING >= '1.30.0' require 'rubocop/rake_task' RuboCop::RakeTask.new(:rubocop) do |task| From 58334031e9bc6556c81569857148e590ad09b275 Mon Sep 17 00:00:00 2001 From: Shane Duvall Date: Thu, 2 Mar 2023 16:04:29 -0600 Subject: [PATCH 145/179] Updating sub-version --- .github/workflows/ruby.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ruby.yml b/.github/workflows/ruby.yml index 02d2de1..91f608a 100644 --- a/.github/workflows/ruby.yml +++ b/.github/workflows/ruby.yml @@ -19,7 +19,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - ruby-version: ['2.4', '2.5', '2.6', '2.7', '3.0', '3.1', '3.2'] + ruby-version: ['2.4', '2.5', '2.6', '2.7.2', '3.0', '3.1', '3.2'] steps: - uses: actions/checkout@v3 From c7f9e2dc27832ec14067b29d14103e8afcede0a1 Mon Sep 17 00:00:00 2001 From: Shane Duvall Date: Thu, 2 Mar 2023 16:40:43 -0600 Subject: [PATCH 146/179] Update ruby config --- .github/workflows/gem-push.yml | 4 ++-- .github/workflows/ruby.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/gem-push.yml b/.github/workflows/gem-push.yml index ead605f..f118289 100644 --- a/.github/workflows/gem-push.yml +++ b/.github/workflows/gem-push.yml @@ -15,11 +15,11 @@ jobs: packages: write steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Ruby 2.7 uses: actions/setup-ruby@v1 with: - ruby-version: 2.7.x + ruby-version: 2.7.6 - name: Publish to GPR run: | diff --git a/.github/workflows/ruby.yml b/.github/workflows/ruby.yml index 91f608a..02d2de1 100644 --- a/.github/workflows/ruby.yml +++ b/.github/workflows/ruby.yml @@ -19,7 +19,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - ruby-version: ['2.4', '2.5', '2.6', '2.7.2', '3.0', '3.1', '3.2'] + ruby-version: ['2.4', '2.5', '2.6', '2.7', '3.0', '3.1', '3.2'] steps: - uses: actions/checkout@v3 From abaeb57636073fd1698bcc3711619731b98fd2c0 Mon Sep 17 00:00:00 2001 From: Shane Duvall Date: Thu, 2 Mar 2023 16:46:41 -0600 Subject: [PATCH 147/179] Update gem yaml --- .github/workflows/gem-push.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/gem-push.yml b/.github/workflows/gem-push.yml index f118289..60a8145 100644 --- a/.github/workflows/gem-push.yml +++ b/.github/workflows/gem-push.yml @@ -16,10 +16,10 @@ jobs: steps: - uses: actions/checkout@v3 - - name: Set up Ruby 2.7 - uses: actions/setup-ruby@v1 + - name: Set up Ruby + uses: ruby/setup-ruby@v1 with: - ruby-version: 2.7.6 + ruby-version: 2.7.x - name: Publish to GPR run: | From 13a0ddcef3db6c7e461c4289f977598ebea7c773 Mon Sep 17 00:00:00 2001 From: Shane Duvall Date: Thu, 2 Mar 2023 16:47:47 -0600 Subject: [PATCH 148/179] Corrected Version --- .github/workflows/gem-push.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/gem-push.yml b/.github/workflows/gem-push.yml index 60a8145..f82bf12 100644 --- a/.github/workflows/gem-push.yml +++ b/.github/workflows/gem-push.yml @@ -19,7 +19,7 @@ jobs: - name: Set up Ruby uses: ruby/setup-ruby@v1 with: - ruby-version: 2.7.x + ruby-version: 2.7.7 - name: Publish to GPR run: | From e9890bab9346acca9635cd650253fd8e0e510713 Mon Sep 17 00:00:00 2001 From: Aaron B <97627964+bockets@users.noreply.github.com> Date: Thu, 19 Oct 2023 14:44:56 -0500 Subject: [PATCH 149/179] Update README.md The write key is required even if using a test queue. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e6cdb6b..bc35023 100644 --- a/README.md +++ b/README.md @@ -97,7 +97,7 @@ You can use the `test: true` option to Segment::Analytics.new to cause all reque A test queue can be used as follows: ```ruby -client = Segment::Analytics.new(test: true) +client = Segment::Analytics.new(write_key: 'YOUR_WRITE_KEY', test: true) client.test_queue # => # From c440393016b13c39716610143a1c309668e48d68 Mon Sep 17 00:00:00 2001 From: Alan Charles <50601149+alanjcharles@users.noreply.github.com> Date: Tue, 13 Feb 2024 09:48:34 -0700 Subject: [PATCH 150/179] Update README.md --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index bc35023..ab3eeb4 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,9 @@ analytics-ruby analytics-ruby is a ruby client for [Segment](https://segment.com) +### ⚠️ Maintenance ⚠️ +This library is in maintenance mode. It will send data as intended, but receive no new feature support and only critical maintenance updates from Segment. +

You can't fix what you can't measure

From e31e74293572c60deac5da6fdd6150374758a7db Mon Sep 17 00:00:00 2001 From: Shane Duvall Date: Wed, 17 Apr 2024 10:53:38 -0500 Subject: [PATCH 151/179] Push raising error message --- lib/segment/analytics/message_batch.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/segment/analytics/message_batch.rb b/lib/segment/analytics/message_batch.rb index 15eef43..19700ac 100644 --- a/lib/segment/analytics/message_batch.rb +++ b/lib/segment/analytics/message_batch.rb @@ -29,6 +29,7 @@ def <<(message) message_json_size = message_json.bytesize if message_too_big?(message_json_size) logger.error('a message exceeded the maximum allowed size') + raise JSONGenerationError, "Message Exceeded Maximum Allowed Size" else @messages << message @json_size += message_json_size + 1 # One byte for the comma From d3cb44a800defbd376b74a7357dbac6c47f2cb2d Mon Sep 17 00:00:00 2001 From: Michael Grosse Huelsewiesche Date: Wed, 1 May 2024 14:00:12 -0700 Subject: [PATCH 152/179] Update codecov token --- codecov.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/codecov.yml b/codecov.yml index c6e5dff..8c43af9 100644 --- a/codecov.yml +++ b/codecov.yml @@ -1,2 +1,4 @@ ignore: - "spec/**/*" +codecov: +token: ${{ secrets.CODECOV_TOKEN }} From 89fe8e20c7284174d0d89ae43ba632fb69c991bd Mon Sep 17 00:00:00 2001 From: Michael Grosse Huelsewiesche Date: Wed, 1 May 2024 14:03:06 -0700 Subject: [PATCH 153/179] Trying another format --- codecov.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/codecov.yml b/codecov.yml index 8c43af9..2af45be 100644 --- a/codecov.yml +++ b/codecov.yml @@ -1,4 +1,4 @@ ignore: - "spec/**/*" codecov: -token: ${{ secrets.CODECOV_TOKEN }} +token: ${ secrets.CODECOV_TOKEN } From 18fc214d1b87fd46e1e1074f4bdbfd22b8151c9f Mon Sep 17 00:00:00 2001 From: Michael Grosse Huelsewiesche Date: Wed, 1 May 2024 14:13:50 -0700 Subject: [PATCH 154/179] Add codecov gh action to replace per-test upload --- .github/workflows/ruby.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/ruby.yml b/.github/workflows/ruby.yml index 02d2de1..e602836 100644 --- a/.github/workflows/ruby.yml +++ b/.github/workflows/ruby.yml @@ -30,3 +30,7 @@ jobs: bundler-cache: true # runs 'bundle install' and caches installed gems automatically - name: Run tests run: bundle exec rake + - name: Upload coverage reports to Codecov + uses: codecov/codecov-action@v4.2.0 + env: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} From c1b795126dd8edfb6594b82036dc75c467b3e32f Mon Sep 17 00:00:00 2001 From: Michael Grosse Huelsewiesche Date: Wed, 1 May 2024 14:18:46 -0700 Subject: [PATCH 155/179] Remove deprecated codecov uploader --- spec/spec_helper.rb | 6 ------ 1 file changed, 6 deletions(-) diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index b1d8435..d4ebab3 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,11 +1,5 @@ # frozen_string_literal: true -# https://github.com/codecov/codecov-ruby#usage -require 'simplecov' -SimpleCov.start -require 'codecov' -SimpleCov.formatter = SimpleCov::Formatter::Codecov - require 'segment/analytics' require 'active_support/time' From 08e2049b48e278a0ffada1e99856982e013a3e4a Mon Sep 17 00:00:00 2001 From: Michael Grosse Huelsewiesche Date: Wed, 1 May 2024 14:19:48 -0700 Subject: [PATCH 156/179] Removing codecov dependency, using GH actions instead --- analytics-ruby.gemspec | 1 - 1 file changed, 1 deletion(-) diff --git a/analytics-ruby.gemspec b/analytics-ruby.gemspec index aad3553..b45b31d 100644 --- a/analytics-ruby.gemspec +++ b/analytics-ruby.gemspec @@ -27,5 +27,4 @@ Gem::Specification.new do |spec| spec.add_development_dependency 'oj', '~> 3.6.2' end spec.add_development_dependency 'rubocop', '~> 1.0' - spec.add_development_dependency 'codecov', '~> 0.6' end From a77c1f30f330f8844a9f4c46dac71b78e5883d72 Mon Sep 17 00:00:00 2001 From: Michael Grosse Huelsewiesche Date: Wed, 1 May 2024 14:40:08 -0700 Subject: [PATCH 157/179] Adding new codecov depenencies --- Gemfile | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Gemfile b/Gemfile index 817f62a..d54fb9a 100644 --- a/Gemfile +++ b/Gemfile @@ -1,2 +1,5 @@ source 'http://rubygems.org' gemspec + +gem 'simplecov' +gem 'simplecov-cobertura' From a0f577f55f14b34a0482d6553d43ea55efde1915 Mon Sep 17 00:00:00 2001 From: Michael Grosse Huelsewiesche Date: Wed, 1 May 2024 14:43:02 -0700 Subject: [PATCH 158/179] Adding new cov formatter --- spec/spec_helper.rb | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index d4ebab3..20f7799 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,5 +1,10 @@ # frozen_string_literal: true +require "simplecov" +SimpleCov.start +require 'simplecov-cobertura' +SimpleCov.formatter = SimpleCov::Formatter::CoberturaFormatter + require 'segment/analytics' require 'active_support/time' From fbe081bfe1270b9ce840562b6ff398467ad5d1d1 Mon Sep 17 00:00:00 2001 From: Michael Grosse Huelsewiesche Date: Wed, 1 May 2024 14:46:11 -0700 Subject: [PATCH 159/179] Fixing nit from codecov's tutorial code --- spec/spec_helper.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 20f7799..8dc8634 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require "simplecov" +require 'simplecov' SimpleCov.start require 'simplecov-cobertura' SimpleCov.formatter = SimpleCov::Formatter::CoberturaFormatter From 42d4dd8381d349879dc43e9d2ef0b2824a1807b7 Mon Sep 17 00:00:00 2001 From: Michael Grosse Huelsewiesche Date: Wed, 1 May 2024 14:50:01 -0700 Subject: [PATCH 160/179] Codecov says commit yaml was invalid, reverting? --- codecov.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/codecov.yml b/codecov.yml index 2af45be..c6e5dff 100644 --- a/codecov.yml +++ b/codecov.yml @@ -1,4 +1,2 @@ ignore: - "spec/**/*" -codecov: -token: ${ secrets.CODECOV_TOKEN } From 91548dbca59c70ce0707bdb47af3194ecadd0a73 Mon Sep 17 00:00:00 2001 From: Shane Duvall Date: Wed, 17 Apr 2024 10:53:38 -0500 Subject: [PATCH 161/179] Push raising error message --- lib/segment/analytics/message_batch.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/segment/analytics/message_batch.rb b/lib/segment/analytics/message_batch.rb index 15eef43..19700ac 100644 --- a/lib/segment/analytics/message_batch.rb +++ b/lib/segment/analytics/message_batch.rb @@ -29,6 +29,7 @@ def <<(message) message_json_size = message_json.bytesize if message_too_big?(message_json_size) logger.error('a message exceeded the maximum allowed size') + raise JSONGenerationError, "Message Exceeded Maximum Allowed Size" else @messages << message @json_size += message_json_size + 1 # One byte for the comma From 8efb70764a030b0a8b45c925ed47e1fe7e5d0f60 Mon Sep 17 00:00:00 2001 From: Michael Grosse Huelsewiesche Date: Fri, 17 May 2024 14:17:16 -0700 Subject: [PATCH 162/179] Export github issues to jira --- .github/workflows/create_jira.yml | 39 +++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 .github/workflows/create_jira.yml diff --git a/.github/workflows/create_jira.yml b/.github/workflows/create_jira.yml new file mode 100644 index 0000000..8180ac0 --- /dev/null +++ b/.github/workflows/create_jira.yml @@ -0,0 +1,39 @@ +name: Create Jira Ticket + +on: + issues: + types: + - opened + +jobs: + create_jira: + name: Create Jira Ticket + runs-on: ubuntu-latest + environment: IssueTracker + steps: + - name: Checkout + uses: actions/checkout@master + - name: Login + uses: atlassian/gajira-login@master + env: + JIRA_BASE_URL: ${{ secrets.JIRA_BASE_URL }} + JIRA_USER_EMAIL: ${{ secrets.JIRA_USER_EMAIL }} + JIRA_API_TOKEN: ${{ secrets.JIRA_TOKEN }} + JIRA_EPIC_KEY: ${{ secrets.JIRA_EPIC_KEY }} + JIRA_PROJECT: ${{ secrets.JIRA_PROJECT }} + + - name: Create + id: create + uses: atlassian/gajira-create@master + with: + project: ${{ secrets.JIRA_PROJECT }} + issuetype: Bug + summary: | + [${{ github.event.repository.name }}] (${{ github.event.issue.number }}): ${{ github.event.issue.title }} + description: | + Github Link: ${{ github.event.issue.html_url }} + ${{ github.event.issue.body }} + fields: '{"parent": {"key": "${{ secrets.JIRA_EPIC_KEY }}"}}' + + - name: Log created issue + run: echo "Issue ${{ steps.create.outputs.issue }} was created" \ No newline at end of file From d993c8e341fe18ff3be27fa9320b5a8f88ffa70d Mon Sep 17 00:00:00 2001 From: Shane Duvall Date: Tue, 28 May 2024 09:52:09 -0500 Subject: [PATCH 163/179] Resolve test response issue --- .github/workflows/gem-push.yml | 2 +- spec/segment/analytics/message_batch_spec.rb | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/gem-push.yml b/.github/workflows/gem-push.yml index f82bf12..ec95c5f 100644 --- a/.github/workflows/gem-push.yml +++ b/.github/workflows/gem-push.yml @@ -15,7 +15,7 @@ jobs: packages: write steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Ruby uses: ruby/setup-ruby@v1 with: diff --git a/spec/segment/analytics/message_batch_spec.rb b/spec/segment/analytics/message_batch_spec.rb index 98f7be7..0cba732 100644 --- a/spec/segment/analytics/message_batch_spec.rb +++ b/spec/segment/analytics/message_batch_spec.rb @@ -19,6 +19,8 @@ class Analytics subject << message expect(subject.length).to eq(0) + + expect { subject << message }.to raise_error("Message Exceeded Maximum Allowed Size") end end From ce515b2f6ae0c1d77484cfa91ccc77c94d39d5d8 Mon Sep 17 00:00:00 2001 From: Shane Duvall Date: Tue, 28 May 2024 09:59:44 -0500 Subject: [PATCH 164/179] update --- spec/segment/analytics/message_batch_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/segment/analytics/message_batch_spec.rb b/spec/segment/analytics/message_batch_spec.rb index 0cba732..8057a78 100644 --- a/spec/segment/analytics/message_batch_spec.rb +++ b/spec/segment/analytics/message_batch_spec.rb @@ -20,7 +20,7 @@ class Analytics subject << message expect(subject.length).to eq(0) - expect { subject << message }.to raise_error("Message Exceeded Maximum Allowed Size") + expect { message }.to raise_error("Message Exceeded Maximum Allowed Size") end end From aded43ddec2493fb4d9e0dbb0f9a24828fa18362 Mon Sep 17 00:00:00 2001 From: Shane Duvall Date: Tue, 28 May 2024 10:05:58 -0500 Subject: [PATCH 165/179] update --- .github/workflows/ruby.yml | 2 +- spec/segment/analytics/message_batch_spec.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ruby.yml b/.github/workflows/ruby.yml index e602836..99831cc 100644 --- a/.github/workflows/ruby.yml +++ b/.github/workflows/ruby.yml @@ -22,7 +22,7 @@ jobs: ruby-version: ['2.4', '2.5', '2.6', '2.7', '3.0', '3.1', '3.2'] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Ruby uses: ruby/setup-ruby@v1 with: diff --git a/spec/segment/analytics/message_batch_spec.rb b/spec/segment/analytics/message_batch_spec.rb index 8057a78..dac883c 100644 --- a/spec/segment/analytics/message_batch_spec.rb +++ b/spec/segment/analytics/message_batch_spec.rb @@ -20,7 +20,7 @@ class Analytics subject << message expect(subject.length).to eq(0) - expect { message }.to raise_error("Message Exceeded Maximum Allowed Size") + expect { MessageBatch }.to raise_error("Message Exceeded Maximum Allowed Size") end end From 2440e09135ee8b2b631d398c5abda7e9af4c766a Mon Sep 17 00:00:00 2001 From: Shane Duvall Date: Tue, 28 May 2024 10:09:49 -0500 Subject: [PATCH 166/179] update --- spec/segment/analytics/message_batch_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/segment/analytics/message_batch_spec.rb b/spec/segment/analytics/message_batch_spec.rb index dac883c..0cba732 100644 --- a/spec/segment/analytics/message_batch_spec.rb +++ b/spec/segment/analytics/message_batch_spec.rb @@ -20,7 +20,7 @@ class Analytics subject << message expect(subject.length).to eq(0) - expect { MessageBatch }.to raise_error("Message Exceeded Maximum Allowed Size") + expect { subject << message }.to raise_error("Message Exceeded Maximum Allowed Size") end end From 9b190ddf49fc8b7e6d70d099998faeb91ee7ef72 Mon Sep 17 00:00:00 2001 From: Shane Duvall Date: Tue, 28 May 2024 10:22:14 -0500 Subject: [PATCH 167/179] update --- spec/segment/analytics/message_batch_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/segment/analytics/message_batch_spec.rb b/spec/segment/analytics/message_batch_spec.rb index 0cba732..57ef715 100644 --- a/spec/segment/analytics/message_batch_spec.rb +++ b/spec/segment/analytics/message_batch_spec.rb @@ -20,7 +20,7 @@ class Analytics subject << message expect(subject.length).to eq(0) - expect { subject << message }.to raise_error("Message Exceeded Maximum Allowed Size") + expect { MessageBatch.new }.to raise_error("Message Exceeded Maximum Allowed Size") end end From 44c94ce2c9778566423a577f5c2f8e9dba0c0feb Mon Sep 17 00:00:00 2001 From: Shane Duvall Date: Tue, 28 May 2024 10:24:44 -0500 Subject: [PATCH 168/179] upd --- spec/segment/analytics/message_batch_spec.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/spec/segment/analytics/message_batch_spec.rb b/spec/segment/analytics/message_batch_spec.rb index 57ef715..35c988a 100644 --- a/spec/segment/analytics/message_batch_spec.rb +++ b/spec/segment/analytics/message_batch_spec.rb @@ -19,7 +19,9 @@ class Analytics subject << message expect(subject.length).to eq(0) + end + it 'raises messages that exceed the maximum allowed size' do expect { MessageBatch.new }.to raise_error("Message Exceeded Maximum Allowed Size") end end From bad6777c40f5e204abf3be877d50ad0d7081990f Mon Sep 17 00:00:00 2001 From: Shane Duvall Date: Tue, 28 May 2024 10:27:55 -0500 Subject: [PATCH 169/179] upd --- spec/segment/analytics/message_batch_spec.rb | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/spec/segment/analytics/message_batch_spec.rb b/spec/segment/analytics/message_batch_spec.rb index 35c988a..efdadd1 100644 --- a/spec/segment/analytics/message_batch_spec.rb +++ b/spec/segment/analytics/message_batch_spec.rb @@ -19,10 +19,7 @@ class Analytics subject << message expect(subject.length).to eq(0) - end - - it 'raises messages that exceed the maximum allowed size' do - expect { MessageBatch.new }.to raise_error("Message Exceeded Maximum Allowed Size") + expect { Message }.to raise_error("Message Exceeded Maximum Allowed Size") end end From b4506dd56b5e8b17527ca32b5e498864e2130d38 Mon Sep 17 00:00:00 2001 From: Shane Duvall Date: Tue, 28 May 2024 10:30:00 -0500 Subject: [PATCH 170/179] upd --- spec/segment/analytics/message_batch_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/segment/analytics/message_batch_spec.rb b/spec/segment/analytics/message_batch_spec.rb index efdadd1..73ff7f2 100644 --- a/spec/segment/analytics/message_batch_spec.rb +++ b/spec/segment/analytics/message_batch_spec.rb @@ -19,7 +19,7 @@ class Analytics subject << message expect(subject.length).to eq(0) - expect { Message }.to raise_error("Message Exceeded Maximum Allowed Size") + expect { subject }.to raise_error("Message Exceeded Maximum Allowed Size") end end From 78bf82e239550f474d8846a093d20cfda2df0039 Mon Sep 17 00:00:00 2001 From: Shane Duvall Date: Tue, 28 May 2024 10:53:04 -0500 Subject: [PATCH 171/179] upd --- spec/segment/analytics/message_batch_spec.rb | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/spec/segment/analytics/message_batch_spec.rb b/spec/segment/analytics/message_batch_spec.rb index 73ff7f2..ccaf08d 100644 --- a/spec/segment/analytics/message_batch_spec.rb +++ b/spec/segment/analytics/message_batch_spec.rb @@ -19,7 +19,11 @@ class Analytics subject << message expect(subject.length).to eq(0) - expect { subject }.to raise_error("Message Exceeded Maximum Allowed Size") + + end + + it 'raises' do + expect { MessageBatch }.to raise_error("Message Exceeded Maximum Allowed Size") end end From f26d642bc8cec258c678d8aeab8d9051d7b19f99 Mon Sep 17 00:00:00 2001 From: Shane Duvall Date: Tue, 28 May 2024 10:59:16 -0500 Subject: [PATCH 172/179] test --- spec/segment/analytics/message_batch_spec.rb | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/spec/segment/analytics/message_batch_spec.rb b/spec/segment/analytics/message_batch_spec.rb index ccaf08d..d3e28a4 100644 --- a/spec/segment/analytics/message_batch_spec.rb +++ b/spec/segment/analytics/message_batch_spec.rb @@ -19,11 +19,7 @@ class Analytics subject << message expect(subject.length).to eq(0) - - end - - it 'raises' do - expect { MessageBatch }.to raise_error("Message Exceeded Maximum Allowed Size") + expect { raise StandardError }.to raise_error("Message Exceeded Maximum Allowed Size") end end From 91a24a5dea8e7d64b343aab4cac02f39d8abfee0 Mon Sep 17 00:00:00 2001 From: Shane Duvall Date: Tue, 28 May 2024 11:06:04 -0500 Subject: [PATCH 173/179] updt --- spec/segment/analytics/message_batch_spec.rb | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/spec/segment/analytics/message_batch_spec.rb b/spec/segment/analytics/message_batch_spec.rb index d3e28a4..5f7b2fd 100644 --- a/spec/segment/analytics/message_batch_spec.rb +++ b/spec/segment/analytics/message_batch_spec.rb @@ -18,8 +18,7 @@ class Analytics message = { 'a' => 'b' * max_bytes } subject << message - expect(subject.length).to eq(0) - expect { raise StandardError }.to raise_error("Message Exceeded Maximum Allowed Size") + expect(subject.length).to eq(0).and_raise(JSONGenerationError.new("Message Exceeded Maximum Allowed Size")) end end From 6ede327a8a11ead4dd4acd864182c8ed41e86ec4 Mon Sep 17 00:00:00 2001 From: Shane Duvall Date: Tue, 28 May 2024 14:06:17 -0500 Subject: [PATCH 174/179] Final Adjustment --- spec/segment/analytics/message_batch_spec.rb | 33 +++++++++++--------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/spec/segment/analytics/message_batch_spec.rb b/spec/segment/analytics/message_batch_spec.rb index 5f7b2fd..8dd5370 100644 --- a/spec/segment/analytics/message_batch_spec.rb +++ b/spec/segment/analytics/message_batch_spec.rb @@ -1,38 +1,43 @@ # frozen_string_literal: true -require 'spec_helper' +require "spec_helper" module Segment class Analytics describe MessageBatch do subject { described_class.new(100) } - describe '#<<' do - it 'appends messages' do - subject << { 'a' => 'b' } + describe "#<<" do + it "appends messages" do + expect(subject.length).to eq(0) + + subject << {"a" => "b"} + expect(subject.length).to eq(1) end - it 'rejects messages that exceed the maximum allowed size' do + it "rejects messages that exceed the maximum allowed size" do max_bytes = Defaults::Message::MAX_BYTES - message = { 'a' => 'b' * max_bytes } + message = {"a" => "b" * max_bytes} + + expect(subject.length).to eq(0) - subject << message - expect(subject.length).to eq(0).and_raise(JSONGenerationError.new("Message Exceeded Maximum Allowed Size")) + expect { subject << message }.to raise_error(MessageBatch::JSONGenerationError) + .with_message("Message Exceeded Maximum Allowed Size") end end - describe '#full?' do - it 'returns true once item count is exceeded' do - 99.times { subject << { a: 'b' } } + describe "#full?" do + it "returns true once item count is exceeded" do + 99.times { subject << {a: "b"} } expect(subject.full?).to be(false) - subject << { a: 'b' } + subject << {a: "b"} expect(subject.full?).to be(true) end - it 'returns true once max size is almost exceeded' do - message = { a: 'b' * (Defaults::Message::MAX_BYTES - 10) } + it "returns true once max size is almost exceeded" do + message = {a: "b" * (Defaults::Message::MAX_BYTES - 10)} message_size = message.to_json.bytesize From bdf9d9d5d50e77d6a7c533e7ba3cbca6385941fb Mon Sep 17 00:00:00 2001 From: Shane Duvall Date: Tue, 28 May 2024 14:11:07 -0500 Subject: [PATCH 175/179] Fix auto correct quotes and spaces --- spec/segment/analytics/message_batch_spec.rb | 26 ++++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/spec/segment/analytics/message_batch_spec.rb b/spec/segment/analytics/message_batch_spec.rb index 8dd5370..0666dc5 100644 --- a/spec/segment/analytics/message_batch_spec.rb +++ b/spec/segment/analytics/message_batch_spec.rb @@ -1,43 +1,43 @@ # frozen_string_literal: true -require "spec_helper" +require 'spec_helper' module Segment class Analytics describe MessageBatch do subject { described_class.new(100) } - describe "#<<" do - it "appends messages" do + describe '#<<' do + it 'appends messages' do expect(subject.length).to eq(0) - subject << {"a" => "b"} + subject << { 'a' => 'b' } expect(subject.length).to eq(1) end - it "rejects messages that exceed the maximum allowed size" do + it 'rejects messages that exceed the maximum allowed size' do max_bytes = Defaults::Message::MAX_BYTES - message = {"a" => "b" * max_bytes} + message = { 'a' => 'b' * max_bytes } expect(subject.length).to eq(0) expect { subject << message }.to raise_error(MessageBatch::JSONGenerationError) - .with_message("Message Exceeded Maximum Allowed Size") + .with_message('Message Exceeded Maximum Allowed Size') end end - describe "#full?" do - it "returns true once item count is exceeded" do - 99.times { subject << {a: "b"} } + describe '#full?' do + it 'returns true once item count is exceeded' do + 99.times { subject << { a: 'b' } } expect(subject.full?).to be(false) - subject << {a: "b"} + subject << { a: 'b' } expect(subject.full?).to be(true) end - it "returns true once max size is almost exceeded" do - message = {a: "b" * (Defaults::Message::MAX_BYTES - 10)} + it 'returns true once max size is almost exceeded' do + message = { a: "b" * (Defaults::Message::MAX_BYTES - 10) } message_size = message.to_json.bytesize From ee68d6594f4e402dc4170136b0b3480b32cdb5e2 Mon Sep 17 00:00:00 2001 From: Shane Duvall Date: Tue, 28 May 2024 14:12:53 -0500 Subject: [PATCH 176/179] Missed a few single quotes --- lib/segment/analytics/message_batch.rb | 2 +- spec/segment/analytics/message_batch_spec.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/segment/analytics/message_batch.rb b/lib/segment/analytics/message_batch.rb index 19700ac..29e37d2 100644 --- a/lib/segment/analytics/message_batch.rb +++ b/lib/segment/analytics/message_batch.rb @@ -29,7 +29,7 @@ def <<(message) message_json_size = message_json.bytesize if message_too_big?(message_json_size) logger.error('a message exceeded the maximum allowed size') - raise JSONGenerationError, "Message Exceeded Maximum Allowed Size" + raise JSONGenerationError, 'Message Exceeded Maximum Allowed Size' else @messages << message @json_size += message_json_size + 1 # One byte for the comma diff --git a/spec/segment/analytics/message_batch_spec.rb b/spec/segment/analytics/message_batch_spec.rb index 0666dc5..b85930a 100644 --- a/spec/segment/analytics/message_batch_spec.rb +++ b/spec/segment/analytics/message_batch_spec.rb @@ -37,7 +37,7 @@ class Analytics end it 'returns true once max size is almost exceeded' do - message = { a: "b" * (Defaults::Message::MAX_BYTES - 10) } + message = { a: 'b' * (Defaults::Message::MAX_BYTES - 10) } message_size = message.to_json.bytesize From 5153c1acdb610e49ca2ae380225da51799a704cc Mon Sep 17 00:00:00 2001 From: Shane Duvall Date: Wed, 17 Jul 2024 12:56:18 -0500 Subject: [PATCH 177/179] Update Release notes --- History.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/History.md b/History.md index 6417a98..d07c50a 100644 --- a/History.md +++ b/History.md @@ -1,3 +1,16 @@ +2.5.0 / 2024-07-17 +================== + +* Fix silent failures (https://github.com/segmentio/analytics-ruby/pull/269) +* Update to Ruby 3.2 (https://github.com/segmentio/analytics-ruby/pull/262) +* Rename Segment namespace to SegmentIO (https://github.com/segmentio/analytics-ruby/pull/259) +* Lower allocated and retained strings (https://github.com/segmentio/analytics-ruby/pull/258) +* Modify timestamp to have 3 fractional digits (https://github.com/segmentio/analytics-ruby/pull/250 && https://github.com/segmentio/analytics-ruby/pull/251) +* Fix for empty user_id or anonymous_id (https://github.com/segmentio/analytics-ruby/pull/245) +* Not enqueuing test action in real queue (https://github.com/segmentio/analytics-ruby/pull/237) +* Fix test queue reset! documentation (https://github.com/segmentio/analytics-ruby/pull/235) + + 2.4.0 / 2021-05-05 ================== From 5052ed9614baaff1a285e50fad5440f2226a3a53 Mon Sep 17 00:00:00 2001 From: Michael Grosse Huelsewiesche Date: Mon, 7 Oct 2024 18:21:25 -0400 Subject: [PATCH 178/179] Release 2.5.0. --- .github/workflows/gem-push.yml | 5 +---- RELEASING.md | 25 +++++-------------------- lib/segment/analytics/version.rb | 2 +- 3 files changed, 7 insertions(+), 25 deletions(-) diff --git a/.github/workflows/gem-push.yml b/.github/workflows/gem-push.yml index ec95c5f..6f120bd 100644 --- a/.github/workflows/gem-push.yml +++ b/.github/workflows/gem-push.yml @@ -1,10 +1,7 @@ name: Ruby Gem on: - push: - branches: [ master ] - pull_request: - branches: [ master ] + workflow_dispatch: jobs: build: diff --git a/RELEASING.md b/RELEASING.md index b6041eb..108b55a 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -1,28 +1,13 @@ We automatically push tags to Rubygems via CI. -Pre-releases +Release ============ - Make sure you're on the latest `master` - Bump the version in [`version.rb`](lib/segment/analytics/version.rb) - Update [`History.md`](History.md) -- Commit these changes. `git commit -am "Release x.y.z.pre"` -- Tag the pre-release. `git tag -a -m "Version x.y.z.pre" x.y.z.pre` -- `git push -u origin master && git push --tags`. The tagged commit will be - pushed to RubyGems via Travis - - -Promoting pre-releases -====================== - -- Find the tag for the pre-release you want to promote. Use `git tag --list - '*.pre'` to list all pre-release tags -- Checkout that tag. `git checkout tags/x.y.z.pre` -- Update the version in [`version.rb`](lib/segment/analytics/version.rb) to not - include the `.pre` suffix -- Commit these changes. `git commit -am "Promote x.y.z.pre"` +- Commit these changes. `git commit -am "Release x.y.z."` - Tag the release. `git tag -a -m "Version x.y.z" x.y.z` -- `git push -u origin master && git push --tags`. The tagged commit will be - pushed to RubyGems via Travis -- On `master`, add an entry to [`History.md`](History.md) under `x.y.z` that - says 'Promoted pre-release to stable' +- `git push -u origin master && git push --tags +- Run the publish action on Github + diff --git a/lib/segment/analytics/version.rb b/lib/segment/analytics/version.rb index eaabbdf..8416349 100644 --- a/lib/segment/analytics/version.rb +++ b/lib/segment/analytics/version.rb @@ -2,6 +2,6 @@ module Segment class Analytics - VERSION = '2.4.2' + VERSION = '2.5.0' end end From 132aa1967ded1c5204df14945c9d360a2b914fb5 Mon Sep 17 00:00:00 2001 From: Michael Grosse Huelsewiesche Date: Mon, 7 Oct 2024 18:54:24 -0400 Subject: [PATCH 179/179] Removing github gem publish step --- .github/workflows/gem-push.yml | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/.github/workflows/gem-push.yml b/.github/workflows/gem-push.yml index 6f120bd..5deca81 100644 --- a/.github/workflows/gem-push.yml +++ b/.github/workflows/gem-push.yml @@ -18,18 +18,6 @@ jobs: with: ruby-version: 2.7.7 - - name: Publish to GPR - run: | - mkdir -p $HOME/.gem - touch $HOME/.gem/credentials - chmod 0600 $HOME/.gem/credentials - printf -- "---\n:github: ${GEM_HOST_API_KEY}\n" > $HOME/.gem/credentials - gem build *.gemspec - gem push --KEY github --host https://rubygems.pkg.github.com/${OWNER} *.gem - env: - GEM_HOST_API_KEY: "Bearer ${{secrets.GITHUB_TOKEN}}" - OWNER: ${{ github.repository_owner }} - - name: Publish to RubyGems run: | mkdir -p $HOME/.gem