pirate_metrics_agent 0.1

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,5 @@
1
+ .DS_Store
2
+ Thumbs.db
3
+ Gemfile.lock
4
+ pkg/
5
+ *.gem
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --colour --format Fuubar
data/CHANGELOG.md ADDED
@@ -0,0 +1,3 @@
1
+ ### 0.1 [October 29, 2012]
2
+ * Initial Version
3
+ * Based on Instrumental Agent 75c4b8c461972ebfa3e792b92c1fdf8d6f630b83
data/Gemfile ADDED
@@ -0,0 +1,9 @@
1
+ source :rubygems
2
+
3
+ gemspec
4
+ gem 'faraday', '~> 0.8.4'
5
+
6
+ if RUBY_VERSION < "1.9"
7
+ # Built and installed via ext/mkrf_conf.rb
8
+ gem 'system_timer', '~> 1.2'
9
+ end
data/Guardfile ADDED
@@ -0,0 +1,5 @@
1
+ guard 'rspec', :version => 2, :cli => '--format Fuubar' do
2
+ watch(%r{^spec/.+_spec\.rb$})
3
+ watch(%r{^lib/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" }
4
+ watch(%r{spec/(spec_helper|test_server).rb}) { "spec/" }
5
+ end
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ Initial software Copyright (c) 2011 Fastest Forward
2
+ Pirate Metrics portions Copyright (c) 2012 Expected Behavior
3
+
4
+ Permission is hereby granted, free of charge, to any person obtaining
5
+ a copy of this software and associated documentation files (the
6
+ "Software"), to deal in the Software without restriction, including
7
+ without limitation the rights to use, copy, modify, merge, publish,
8
+ distribute, sublicense, and/or sell copies of the Software, and to
9
+ permit persons to whom the Software is furnished to do so, subject to
10
+ the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be
13
+ included in all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,59 @@
1
+ # Pirate Metrics Agent
2
+
3
+ Get to know your customers
4
+
5
+ ## Setup & Usage
6
+
7
+ Add the gem to your Gemfile.
8
+
9
+ ```sh
10
+ gem 'pirate_metrics_agent'
11
+ ```
12
+
13
+ Visit [piratemetrics.com](https://piratemetrics.com) and create an account, then initialize the agent with your API key, found on the main project page.
14
+
15
+ ```sh
16
+ PM = PirateMetrics::Agent.new('YOUR_API_KEY', :enabled => Rails.env.production?)
17
+ ```
18
+
19
+ You'll probably want something like the above, only enabling the agent in production mode so you don't have development and production data writing to the same value. Or you can setup two projects, so that you can verify stats in one, and release them to production in another.
20
+
21
+ Now you can begin to use Pirate Metrics to track your application.
22
+
23
+ ```sh
24
+ PM.acquisition({ :email => 'joe@example.com'}) # new user acquisition
25
+ ```
26
+
27
+ **Note**: For your app's safety, the agent is meant to isolate your app from any problems our service might suffer. If it is unable to connect to the service, it will discard data after reaching a low memory threshold.
28
+
29
+ ## Backfilling
30
+
31
+ You almost certainly have events that occurred before you signed up for Pirate Metrics. To get all of your users into the proper context, Pirate Metrics allows you to backfill data.
32
+
33
+ When backfilling, you may send tens of thousands of metrics per second, and the command buffer may start discarding data it isn't able to send fast enough. Using the ! form of the various API calls will force them to be synchronous.
34
+
35
+ **Warning**: You should only use synchronous mode for backfilling data as any issues with the Pirate Metrics service issues will cause this code to halt until it can reconnect.
36
+
37
+ ```sh
38
+ acquisition_data = []
39
+ User.find_in_batches(:batch_size => 100) do |users|
40
+ users.each do |user|
41
+ acquisition_data << {:email => user.email, :occurred_at => user.created_at}
42
+ end
43
+ PM.acquisition!(acquisition_data)
44
+ acquisition_data.clear
45
+ end
46
+ ```
47
+ ## Agent Control
48
+
49
+ Need to quickly disable the agent? set :enabled to false on initialization and you don't need to change any application code.
50
+
51
+ ## Tracking metrics in Resque jobs (and Resque-like scenarios)
52
+
53
+ If you plan on tracking metrics in Resque jobs, you will need to explicitly cleanup after the agent when the jobs are finished. You can accomplish this by adding `after_perform` and `on_failure` hooks to your Resque jobs. See the Resque [hooks documentation](https://github.com/defunkt/resque/blob/master/docs/HOOKS.md) for more information.
54
+
55
+ You're required to do this because Resque calls `exit!` when a worker has finished processing, which bypasses Ruby's `at_exit` hooks. The Pirate Metrics Agent installs an `at_exit` hook to flush any pending metrics to the servers, but this hook is bypassed by the `exit!` call; any other code you rely that uses `exit!` should call `PM.cleanup` to ensure any pending metrics are correctly sent to the server before exiting the process.
56
+
57
+ ## Troubleshooting & Help
58
+
59
+ We are here to help. Email us at [support@piratemetrics.com](mailto:support@piratemetrics.com).
data/Rakefile ADDED
@@ -0,0 +1,9 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rspec/core/rake_task'
3
+
4
+ task :default => :spec
5
+
6
+ RSpec::Core::RakeTask.new(:spec) do |spec|
7
+ spec.pattern = 'spec/*_spec.rb'
8
+ spec.rspec_opts = ['--color --backtrace']
9
+ end
@@ -0,0 +1,266 @@
1
+ require 'pirate_metrics/version'
2
+ require 'pirate_metrics/system_timer'
3
+ require 'faraday'
4
+ require 'logger'
5
+ require 'thread'
6
+ require 'socket'
7
+
8
+
9
+ module PirateMetrics
10
+ class Agent
11
+ BACKOFF = 2.0
12
+ MAX_RECONNECT_DELAY = 15
13
+ MAX_BUFFER = 5000
14
+ REPLY_TIMEOUT = 10
15
+ CONNECT_TIMEOUT = 20
16
+ EXIT_FLUSH_TIMEOUT = 5
17
+
18
+ attr_accessor :host, :port, :synchronous, :queue
19
+ attr_reader :connection, :enabled
20
+
21
+ def self.logger=(l)
22
+ @logger = l
23
+ end
24
+
25
+ def self.logger
26
+ if !@logger
27
+ @logger = Logger.new(STDERR)
28
+ @logger.level = Logger::WARN
29
+ end
30
+ @logger
31
+ end
32
+
33
+ # Sets up a connection to the collector.
34
+ #
35
+ # PirateMetrics::Agent.new(API_KEY)
36
+ # PirateMetrics::Agent.new(API_KEY, :collector => 'hostname:port')
37
+ def initialize(api_key, options = {})
38
+ # symbolize options keys
39
+ options.replace(
40
+ options.inject({}) { |m, (k, v)| m[(k.to_sym rescue k) || k] = v; m }
41
+ )
42
+
43
+ # defaults
44
+ # host: piratemetrics.com
45
+ # port: 80
46
+ # enabled: true
47
+ # synchronous: false
48
+ @api_key = api_key
49
+ @host, @port = options[:collector].to_s.split(':')
50
+ @host = options[:host] || 'https://piratemetrics.com'
51
+ @port = (options[:port] || 443).to_i
52
+ @enabled = options.has_key?(:enabled) ? !!options[:enabled] : true
53
+ @synchronous = !!options[:synchronous]
54
+ @pid = Process.pid
55
+ @allow_reconnect = true
56
+
57
+ setup_cleanup_at_exit if @enabled
58
+ end
59
+
60
+ # Store a customer metric
61
+ #
62
+ # agent.acquisition!({ :email => 'test@example.com',
63
+ # :occurred_at => user.created_at,
64
+ # :level => 'Double Uranium'})
65
+ [:acquisition, :activation, :retention, :revenue, :referral].each do |metric|
66
+ define_method metric do |customer|
67
+ begin
68
+ payload = customer.is_a?(Array) ? customer : [customer]
69
+ send_metric(metric, payload, @synchronous)
70
+ rescue Exception => ex
71
+ report_exception ex
72
+ return nil
73
+ end
74
+ end
75
+ define_method "#{metric}!" do |customer|
76
+ begin
77
+ payload = customer.is_a?(Array) ? customer : [customer]
78
+ send_metric(metric, payload, true)
79
+ rescue Exception => ex
80
+ report_exception ex
81
+ return nil
82
+ end
83
+ end
84
+ end
85
+
86
+ # Synchronously flush all pending metrics out to the server
87
+ # By default will not try to reconnect to the server if a
88
+ # connection failure happens during the flush, though you
89
+ # may optionally override this behavior by passing true.
90
+ #
91
+ # agent.flush
92
+ def flush(allow_reconnect = false)
93
+ queue_metric('flush', nil, {
94
+ :synchronous => true,
95
+ :allow_reconnect => allow_reconnect
96
+ }) if running?
97
+ end
98
+
99
+ def enabled?
100
+ @enabled
101
+ end
102
+
103
+ def logger=(logger)
104
+ @logger = logger
105
+ end
106
+
107
+ def logger
108
+ @logger || self.class.logger
109
+ end
110
+
111
+ # Stopping the agent will immediately stop all communication
112
+ # to PirateMetrics. If you call this and submit another metric,
113
+ # the agent will start again.
114
+ #
115
+ # Calling stop will cause all metrics waiting to be sent to be
116
+ # discarded. Don't call it unless you are expecting this behavior.
117
+ #
118
+ # agent.stop
119
+ #
120
+ def stop
121
+ if @thread
122
+ @thread.kill
123
+ @thread = nil
124
+ end
125
+ end
126
+
127
+ # Called when a process is exiting to give it some extra time to
128
+ # push events to the service. An at_exit handler is automatically
129
+ # registered for this method, but can be called manually in cases
130
+ # where at_exit is bypassed like Resque workers.
131
+ def cleanup
132
+ if running?
133
+ logger.info "Cleaning up agent, queue size: #{@queue.size}, thread running: #{@thread.alive?}"
134
+ @allow_reconnect = false
135
+ if @queue.size > 0
136
+ queue_metric('exit')
137
+ begin
138
+ with_timeout(EXIT_FLUSH_TIMEOUT) { @thread.join }
139
+ rescue Timeout::Error
140
+ if @queue.size > 0
141
+ logger.error "Timed out working agent thread on exit, dropping #{@queue.size} metrics"
142
+ else
143
+ logger.error "Timed out PirateMetrics Agent, exiting"
144
+ end
145
+ end
146
+ end
147
+ end
148
+ end
149
+
150
+ private
151
+
152
+ def with_timeout(time, &block)
153
+ PirateMetricsTimeout.timeout(time) { yield }
154
+ end
155
+
156
+ def report_exception(e)
157
+ logger.error "Exception occurred: #{e.message}\n#{e.backtrace.join("\n")}"
158
+ end
159
+
160
+ def send_metric(metric, payload, synchronous = false)
161
+ if enabled?
162
+ start_connection_worker if !running?
163
+ if @queue.size < MAX_BUFFER
164
+ @queue_full_warning = false
165
+ logger.debug "Queueing: #{metric} -> #{payload.inspect}"
166
+ queue_metric(metric, payload, { :synchronous => synchronous })
167
+ else
168
+ if !@queue_full_warning
169
+ @queue_full_warning = true
170
+ logger.warn "Queue full(#{@queue.size}), dropping commands..."
171
+ end
172
+ logger.debug "Dropping command, queue full(#{@queue.size}): #{metric}"
173
+ nil
174
+ end
175
+ end
176
+ end
177
+
178
+ def queue_metric(metric, payload = nil, options = {})
179
+ if @enabled
180
+ options ||= {}
181
+ if options[:allow_reconnect].nil?
182
+ options[:allow_reconnect] = @allow_reconnect
183
+ end
184
+ synchronous = options.delete(:synchronous)
185
+ if synchronous
186
+ options[:sync_resource] ||= ConditionVariable.new
187
+ @sync_mutex.synchronize {
188
+ @queue << [metric, payload, options]
189
+ options[:sync_resource].wait(@sync_mutex)
190
+ }
191
+ else
192
+ @queue << [metric, payload, options]
193
+ end
194
+ end
195
+ metric
196
+ end
197
+
198
+ def start_connection_worker
199
+ if enabled?
200
+ @pid = Process.pid
201
+ @queue = Queue.new
202
+ @sync_mutex = Mutex.new
203
+ @failures = 0
204
+ logger.info "Starting thread"
205
+ @thread = Thread.new do
206
+ run_worker_loop
207
+ end
208
+ end
209
+ end
210
+
211
+ def run_worker_loop
212
+ command_and_args = nil
213
+ command_options = nil
214
+ @piratemetrics = Faraday.new(:url => "#{@host}:#{@port}") do |faraday|
215
+ faraday.request :url_encoded
216
+ faraday.adapter Faraday.default_adapter
217
+ end
218
+ logger.info "connected to collector at #{host}:#{port}"
219
+ @failures = 0
220
+ loop do
221
+ begin
222
+ metric, payload, options = @queue.pop
223
+ sync_resource = options && options[:sync_resource]
224
+ case metric
225
+ when 'exit'
226
+ logger.info "Exiting, #{@queue.size} commands remain"
227
+ return true
228
+ when 'flush'
229
+ release_resource = true
230
+ else
231
+ logger.debug "Sending: #{metric} -> #{payload.inspect}"
232
+ result = @piratemetrics.post("/api/v1/#{metric}s", { :api_key => @api_key, :data => payload})
233
+ end
234
+ metric = payload = options = nil
235
+ rescue Exception => err
236
+ queue_metric(metric, payload, options) if metric
237
+ sleep MAX_RECONNECT_DELAY
238
+ end
239
+ if sync_resource
240
+ @sync_mutex.synchronize do
241
+ sync_resource.signal
242
+ end
243
+ end
244
+ end
245
+ rescue Exception => err
246
+ if err.is_a?(EOFError)
247
+ # nop
248
+ elsif err.is_a?(Errno::ECONNREFUSED)
249
+ logger.error "unable to connect to PirateMetrics."
250
+ else
251
+ report_exception(err)
252
+ end
253
+ end
254
+
255
+ def setup_cleanup_at_exit
256
+ at_exit do
257
+ cleanup
258
+ end
259
+ end
260
+
261
+ def running?
262
+ !@thread.nil? && @pid == Process.pid
263
+ end
264
+
265
+ end
266
+ end
@@ -0,0 +1,31 @@
1
+ if RUBY_VERSION < "1.9" && RUBY_PLATFORM != "java"
2
+ timeout_lib = nil
3
+ ["SystemTimer", "system_timer"].each do |lib|
4
+ begin
5
+ unless timeout_lib
6
+ gem lib
7
+ require "system_timer"
8
+ timeout_lib = SystemTimer
9
+ end
10
+ rescue Exception => e
11
+ end
12
+ end
13
+ if !timeout_lib
14
+ puts <<-EOMSG
15
+ WARNING:: You do not currently have system_timer installed.
16
+ It is strongly advised that you install this gem when using
17
+ pirate_metrics_agent with Ruby 1.8.x. You can install it in
18
+ your Gemfile via:
19
+ gem 'system_timer'
20
+ or manually via:
21
+ gem install system_timer
22
+ EOMSG
23
+ require 'timeout'
24
+ PirateMetricsTimeout = Timeout
25
+ else
26
+ PirateMetricsTimeout = timeout_lib
27
+ end
28
+ else
29
+ require 'timeout'
30
+ PirateMetricsTimeout = Timeout
31
+ end
@@ -0,0 +1,3 @@
1
+ module PirateMetrics
2
+ VERSION = '0.1'
3
+ end
@@ -0,0 +1 @@
1
+ require 'pirate_metrics/agent'
@@ -0,0 +1,28 @@
1
+ $:.push File.expand_path("../lib", __FILE__)
2
+ require "pirate_metrics/version"
3
+
4
+ Gem::Specification.new do |s|
5
+ s.name = "pirate_metrics_agent"
6
+ s.version = PirateMetrics::VERSION
7
+ s.authors = ["Elijah Miller", "Christopher Zelenak", "Kristopher Chambers", "Matthew Hassfurder", "Expected Behavior"]
8
+ s.email = ["support@piratemetrics.com"]
9
+ s.homepage = "http://github.com/expectedbehavior/pirate_metrics_agent"
10
+ s.summary = %q{Agent for reporting data to piratemetrics.com}
11
+ s.description = %q{Get to know your customers.}
12
+
13
+ s.files = `git ls-files`.split("\n")
14
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
15
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
16
+ s.require_paths = ["lib"]
17
+ s.add_development_dependency(%q<rake>, [">= 0"])
18
+ s.add_development_dependency(%q<rack>, [">= 0"])
19
+ s.add_development_dependency(%q<rspec>, ["~> 2.0"])
20
+ s.add_development_dependency(%q<fuubar>, [">= 0"])
21
+ if RUBY_VERSION >= "1.8.7"
22
+ s.add_development_dependency(%q<pry>, ["~> 0.9"])
23
+ s.add_development_dependency(%q<guard>, [">= 0"])
24
+ s.add_development_dependency(%q<guard-rspec>, [">= 0"])
25
+ s.add_development_dependency(%q<growl>, [">= 0"])
26
+ s.add_development_dependency(%q<rb-fsevent>, [">= 0"])
27
+ end
28
+ end
@@ -0,0 +1,436 @@
1
+ require 'spec_helper'
2
+
3
+ def wait
4
+ sleep 0.1 # FIXME: hack
5
+ end
6
+
7
+ PirateMetrics::Agent.logger.level = Logger::FATAL
8
+ describe PirateMetrics::Agent, "disabled" do
9
+ before do
10
+ @server = TestServer.new
11
+ @agent = @server.fresh_agent(:enabled => false)
12
+ end
13
+
14
+ after do
15
+ @agent.stop
16
+ @agent = nil
17
+ @server.stop
18
+ end
19
+
20
+ it "should not connect to the server" do
21
+ wait
22
+ @server.connect_count.should == 0
23
+ end
24
+
25
+ it "should not connect to the server after receiving a metric" do
26
+ wait
27
+ @agent.acquisition({:email => 'test@example.com'})
28
+ wait
29
+ @server.connect_count.should == 0
30
+ end
31
+
32
+ it "should no op on flush without reconnect" do
33
+ 1.upto(100) { |i| @agent.acquisition({:email => "test#{i}@example.com"}) }
34
+ @agent.flush(false)
35
+ wait
36
+ @server.metrics.should be_empty
37
+ end
38
+
39
+ it "should no op on flush with reconnect" do
40
+ 1.upto(100) { |i| @agent.acquisition({:email => "test#{i}@example.com"}) }
41
+ @agent.flush(true)
42
+ wait
43
+ @server.metrics.should be_empty
44
+ end
45
+
46
+ it "should no op on an empty flush" do
47
+ @agent.flush(true)
48
+ wait
49
+ @server.metrics.should be_empty
50
+ end
51
+ end
52
+
53
+ describe PirateMetrics::Agent, "enabled" do
54
+ before do
55
+ @server = TestServer.new
56
+ @agent = @server.fresh_agent
57
+ end
58
+
59
+ after do
60
+ @agent.stop
61
+ @agent = nil
62
+ @server.stop
63
+ end
64
+
65
+ it "should send an api key" do
66
+ @agent.acquisition({:email => "test@example.com"})
67
+ wait
68
+ @server.last_api_key == "test_token"
69
+ end
70
+ end
71
+
72
+ describe PirateMetrics::Agent, "acquisitions" do
73
+ before do
74
+ @server = TestServer.new
75
+ @agent = @server.fresh_agent
76
+ end
77
+
78
+ after do
79
+ @agent.stop
80
+ @agent = nil
81
+ @server.stop
82
+ end
83
+
84
+ it "should not connect to the server" do
85
+ wait
86
+ @server.connect_count.should == 0
87
+ end
88
+
89
+ it "should report an acquisition using the hash form" do
90
+ @agent.acquisition({:email => "test@example.com"})
91
+ wait
92
+ @server.acquisitions.last.should == { "email" => "test@example.com"}
93
+ end
94
+
95
+ it "should report an acquisition using the array form" do
96
+ @agent.acquisition([{:email => "test1@example.com"}, { :email => "test2@example.com"}])
97
+ wait
98
+ @server.acquisitions.first.should == { "email" => "test1@example.com"}
99
+ @server.acquisitions.last.should == { "email" => "test2@example.com"}
100
+ end
101
+
102
+ it "should be able to report acquisitions synchronously" do
103
+ @agent.acquisition!({:email => "test@example.com"})
104
+ wait
105
+ @server.acquisitions.last.should == { "email" => "test@example.com"}
106
+ end
107
+ end
108
+
109
+ describe PirateMetrics::Agent, "activations" do
110
+ before do
111
+ @server = TestServer.new
112
+ @agent = @server.fresh_agent
113
+ end
114
+
115
+ after do
116
+ @agent.stop
117
+ @agent = nil
118
+ @server.stop
119
+ end
120
+
121
+ it "should not connect to the server" do
122
+ wait
123
+ @server.connect_count.should == 0
124
+ end
125
+
126
+ it "should report an activation using the hash form" do
127
+ @agent.activation({:email => "test@example.com"})
128
+ wait
129
+ @server.activations.last.should == { "email" => "test@example.com"}
130
+ end
131
+
132
+ it "should report an activation using the array form" do
133
+ @agent.activation([{:email => "test1@example.com"}, { :email => "test2@example.com"}])
134
+ wait
135
+ @server.activations.first.should == { "email" => "test1@example.com"}
136
+ @server.activations.last.should == { "email" => "test2@example.com"}
137
+ end
138
+
139
+ it "should be able to report activations synchronously" do
140
+ @agent.activation!({:email => "test@example.com"})
141
+ wait
142
+ @server.activations.last.should == { "email" => "test@example.com"}
143
+ end
144
+ end
145
+
146
+ describe PirateMetrics::Agent, "retentions" do
147
+ before do
148
+ @server = TestServer.new
149
+ @agent = @server.fresh_agent
150
+ end
151
+
152
+ after do
153
+ @agent.stop
154
+ @agent = nil
155
+ @server.stop
156
+ end
157
+
158
+ it "should not connect to the server" do
159
+ wait
160
+ @server.connect_count.should == 0
161
+ end
162
+
163
+ it "should report an retention using the hash form" do
164
+ @agent.retention({:email => "test@example.com"})
165
+ wait
166
+ @server.retentions.last.should == { "email" => "test@example.com"}
167
+ end
168
+
169
+ it "should report an retention using the array form" do
170
+ @agent.retention([{:email => "test1@example.com"}, { :email => "test2@example.com"}])
171
+ wait
172
+ @server.retentions.first.should == { "email" => "test1@example.com"}
173
+ @server.retentions.last.should == { "email" => "test2@example.com"}
174
+ end
175
+
176
+ it "should be able to report retentions synchronously" do
177
+ @agent.retention!({:email => "test@example.com"})
178
+ wait
179
+ @server.retentions.last.should == { "email" => "test@example.com"}
180
+ end
181
+ end
182
+
183
+ describe PirateMetrics::Agent, "revenues" do
184
+ before do
185
+ @server = TestServer.new
186
+ @agent = @server.fresh_agent
187
+ end
188
+
189
+ after do
190
+ @agent.stop
191
+ @agent = nil
192
+ @server.stop
193
+ end
194
+
195
+ it "should not connect to the server" do
196
+ wait
197
+ @server.connect_count.should == 0
198
+ end
199
+
200
+ it "should report an revenue using the hash form" do
201
+ @agent.revenue({:email => "test@example.com", :amount_in_cents => "1000"})
202
+ wait
203
+ @server.revenues.last.should == { "email" => "test@example.com", "amount_in_cents" => "1000"}
204
+ end
205
+
206
+ it "should report an revenue using the array form" do
207
+ @agent.revenue([{:email => "test1@example.com", :amount_in_cents => "1000"}, { :email => "test2@example.com", :amount_in_cents => "2000"}])
208
+ wait
209
+ @server.revenues.first.should == { "email" => "test1@example.com", "amount_in_cents" => "1000"}
210
+ @server.revenues.last.should == { "email" => "test2@example.com", "amount_in_cents" => "2000"}
211
+ end
212
+
213
+ it "should be able to report revenues synchronously" do
214
+ @agent.revenue!({:email => "test@example.com", :amount_in_cents => "1000"})
215
+ wait
216
+ @server.revenues.last.should == { "email" => "test@example.com", "amount_in_cents" => "1000"}
217
+ end
218
+ end
219
+
220
+
221
+ describe PirateMetrics::Agent, "referrals" do
222
+ before do
223
+ @server = TestServer.new
224
+ @agent = @server.fresh_agent
225
+ end
226
+
227
+ after do
228
+ @agent.stop
229
+ @agent = nil
230
+ @server.stop
231
+ end
232
+
233
+ it "should not connect to the server" do
234
+ wait
235
+ @server.connect_count.should == 0
236
+ end
237
+
238
+ it "should report an referral using the hash form" do
239
+ @agent.referral({:customer_email => "test@example.com", :referree_email => "ref@example.com"})
240
+ wait
241
+ @server.referrals.last.should == { "customer_email" => "test@example.com", "referree_email" => "ref@example.com"}
242
+ end
243
+
244
+ it "should report an referral using the array form" do
245
+ @agent.referral([{:customer_email => "test1@example.com", :referree_email => "ref@example.com"}, { :customer_email => "test2@example.com", :referree_email => "ref2@example.com"}])
246
+ wait
247
+ @server.referrals.first.should == { "customer_email" => "test1@example.com", "referree_email" => "ref@example.com"}
248
+ @server.referrals.last.should == { "customer_email" => "test2@example.com", "referree_email" => "ref2@example.com"}
249
+ end
250
+
251
+ it "should be able to report referrals synchronously" do
252
+ @agent.referral!({:customer_email => "test@example.com", :referree_email => "ref@example.com"})
253
+ wait
254
+ @server.referrals.last.should == { "customer_email" => "test@example.com", "referree_email" => "ref@example.com"}
255
+ end
256
+ end
257
+
258
+ describe PirateMetrics::Agent, "agent queueing, synchronicity, reliability" do
259
+ before do
260
+ @server = TestServer.new
261
+ @agent = @server.fresh_agent
262
+ end
263
+
264
+ after do
265
+ @agent.stop
266
+ @agent = nil
267
+ @server.stop
268
+ end
269
+
270
+ it "should discard data that overflows the buffer" do
271
+ with_constants('PirateMetrics::Agent::MAX_BUFFER' => 3) do
272
+ 5.times do |i|
273
+ @agent.acquisition({ :email => "test#{i}@example.com"})
274
+ end
275
+ wait
276
+ @server.acquisitions.should include({ "email" => "test0@example.com"})
277
+ @server.acquisitions.should include({ "email" => "test1@example.com"})
278
+ @server.acquisitions.should include({ "email" => "test2@example.com"})
279
+ @server.acquisitions.should_not include({ "email" => "test3@example.com"})
280
+ @server.acquisitions.should_not include({ "email" => "test4@example.com"})
281
+ end
282
+ end
283
+
284
+ it "should send all data in synchronous mode" do
285
+ with_constants('PirateMetrics::Agent::MAX_BUFFER' => 3) do
286
+ 5.times do |i|
287
+ @agent.acquisition!({ :email => "test#{i}@example.com"})
288
+ end
289
+ @agent.instance_variable_get(:@queue).size.should == 0
290
+ wait
291
+ @server.acquisitions.should include({ "email" => "test0@example.com"})
292
+ @server.acquisitions.should include({ "email" => "test1@example.com"})
293
+ @server.acquisitions.should include({ "email" => "test2@example.com"})
294
+ @server.acquisitions.should include({ "email" => "test3@example.com"})
295
+ @server.acquisitions.should include({ "email" => "test4@example.com"})
296
+ end
297
+ end
298
+
299
+ it "should send all data in synchronous mode (agent-level)" do
300
+ with_constants('PirateMetrics::Agent::MAX_BUFFER' => 3) do
301
+ @agent.synchronous = true
302
+ 5.times do |i|
303
+ @agent.acquisition({ :email => "test#{i}@example.com"})
304
+ end
305
+ @agent.instance_variable_get(:@queue).size.should == 0
306
+ wait
307
+ @server.acquisitions.should include({ "email" => "test0@example.com"})
308
+ @server.acquisitions.should include({ "email" => "test1@example.com"})
309
+ @server.acquisitions.should include({ "email" => "test2@example.com"})
310
+ @server.acquisitions.should include({ "email" => "test3@example.com"})
311
+ @server.acquisitions.should include({ "email" => "test4@example.com"})
312
+ end
313
+ end
314
+
315
+ it "should automatically reconnect when forked" do
316
+ wait
317
+ @agent.acquisition({ :email => "test0@example.com"})
318
+ fork do
319
+ @agent.acquisition({ :email => "test1@example.com"})
320
+ end
321
+ wait
322
+ @agent.acquisition({ :email => "test2@example.com"})
323
+ wait
324
+ @server.acquisitions.should include({ "email" => "test0@example.com"})
325
+ @server.acquisitions.should include({ "email" => "test1@example.com"})
326
+ @server.acquisitions.should include({ "email" => "test2@example.com"})
327
+ end
328
+
329
+ it "should never let an exception reach the user" do
330
+ @agent.stub!(:send_metric).and_raise(Exception.new("Test Exception"))
331
+ @agent.acquisition({ :email => "test@example.com"}).should be_nil
332
+ wait
333
+ @agent.activation({ :email => "test@example.com"}).should be_nil
334
+ wait
335
+ end
336
+
337
+ it "should allow outgoing metrics to be stopped" do
338
+ tm = Time.now
339
+ @agent.acquisition({ :email => "testbad@example.com"})
340
+ @agent.stop
341
+ wait
342
+ @agent.acquisition({ :email => "testgood@example.com"})
343
+ wait
344
+ @server.acquisitions.should_not include({ "email" => "testbad@example.com"})
345
+ @server.acquisitions.should include({ "email" => "testgood@example.com"})
346
+ end
347
+
348
+ it "should allow flushing pending values to the server" do
349
+ 1.upto(100) { |i| @agent.acquisition({ :email => "test#{i}@example.com"}) }
350
+ @agent.instance_variable_get(:@queue).size.should >= 100
351
+ @agent.flush
352
+ @agent.instance_variable_get(:@queue).size.should == 0
353
+ wait
354
+ @server.acquisitions.size.should == 100
355
+ end
356
+
357
+ it "should no op on an empty flush" do
358
+ @agent.flush(true)
359
+ wait
360
+ @server.connect_count.should == 0
361
+ end
362
+ end
363
+
364
+ describe PirateMetrics::Agent, "connection problems" do
365
+ after do
366
+ @agent.stop
367
+ @server.stop
368
+ end
369
+
370
+ it "should automatically reconnect on disconnect" do
371
+ @server = TestServer.new
372
+ @agent = @server.fresh_agent
373
+ @agent.acquisition({ :email => "test1@example.com"})
374
+ wait
375
+ @server.disconnect_all
376
+ wait
377
+ @agent.acquisition({ :email => "test2@example.com"})
378
+ wait
379
+ @server.connect_count.should == 2
380
+ @server.acquisitions.last.should == { "email" => "test2@example.com"}
381
+ end
382
+
383
+ it "should buffer commands when server is down" do
384
+ @server = TestServer.new(:listen => false)
385
+ @agent = @server.fresh_agent
386
+ wait
387
+ @agent.retention({ :email => "test@example.com"})
388
+ wait
389
+ @agent.queue.size.should == 1
390
+ end
391
+
392
+ it "should send commands in a short-lived process" do
393
+ @server = TestServer.new
394
+ @agent = @server.fresh_agent
395
+ if pid = fork { @agent.acquisition({ :email => "test@example.com"}) }
396
+ Process.wait(pid)
397
+ @server.acquisitions.size.should == 1
398
+ end
399
+ end
400
+
401
+ it "should send commands in a process that bypasses at_exit when using #cleanup" do
402
+ @server = TestServer.new
403
+ @agent = @server.fresh_agent
404
+ if pid = fork { @agent.acquisition({ :email => "test@example.com"}); @agent.cleanup; exit! }
405
+ Process.wait(pid)
406
+ @server.acquisitions.size.should == 1
407
+ end
408
+ end
409
+ end
410
+
411
+ describe PirateMetrics::Agent, "enabled with sync option" do
412
+ before do
413
+ @server = TestServer.new
414
+ @agent = @server.fresh_agent({ :synchronous => true})
415
+ end
416
+
417
+ after do
418
+ @agent.stop
419
+ @server.stop
420
+ end
421
+
422
+ it "should send all data in synchronous mode" do
423
+ with_constants('PirateMetrics::Agent::MAX_BUFFER' => 3) do
424
+ 5.times do |i|
425
+ @agent.acquisition({ :email => "test#{i}@example.com"})
426
+ end
427
+ wait # let the server receive the commands
428
+ @server.acquisitions.should include({ "email" => "test0@example.com"})
429
+ @server.acquisitions.should include({ "email" => "test1@example.com"})
430
+ @server.acquisitions.should include({ "email" => "test2@example.com"})
431
+ @server.acquisitions.should include({ "email" => "test3@example.com"})
432
+ @server.acquisitions.should include({ "email" => "test4@example.com"})
433
+ end
434
+ end
435
+ end
436
+
@@ -0,0 +1,72 @@
1
+ $: << File.join(File.dirname(__FILE__), "..", "lib")
2
+
3
+ require 'pirate_metrics_agent'
4
+ require 'test_server'
5
+
6
+ RSpec.configure do |config|
7
+
8
+ config.before(:all) do
9
+ end
10
+
11
+ config.after(:all) do
12
+ end
13
+
14
+ end
15
+
16
+
17
+ def parse_constant(constant)
18
+ constant = constant.to_s
19
+ parts = constant.split("::")
20
+ constant_name = parts.pop
21
+ source = parts.join("::")
22
+ [source.constantize, constant_name]
23
+ end
24
+
25
+ def with_constants(constants, &block)
26
+ saved_constants = {}
27
+ constants.each do |constant, val|
28
+ source_object, const_name = parse_constant(constant)
29
+
30
+ saved_constants[constant] = source_object.const_get(const_name)
31
+ Kernel::silence_warnings { source_object.const_set(const_name, val) }
32
+ end
33
+
34
+ begin
35
+ block.call
36
+ ensure
37
+ constants.each do |constant, val|
38
+ source_object, const_name = parse_constant(constant)
39
+
40
+ Kernel::silence_warnings { source_object.const_set(const_name, saved_constants[constant]) }
41
+ end
42
+ end
43
+ end
44
+ alias :with_constant :with_constants
45
+
46
+ class String
47
+ # From Rails
48
+ def constantize
49
+ names = split('::')
50
+ names.shift if names.empty? || names.first.empty?
51
+
52
+ constant = Object
53
+ names.each do |name|
54
+ constant = constant.const_defined?(name) ? constant.const_get(name) : constant.const_missing(name)
55
+ end
56
+ constant
57
+ end
58
+ end
59
+
60
+ module Kernel
61
+ # File activesupport/lib/active_support/core_ext/kernel/reporting.rb, line 10
62
+ def silence_warnings
63
+ with_warnings(nil) { yield }
64
+ end
65
+
66
+ def with_warnings(flag)
67
+ old_verbose, $VERBOSE = $VERBOSE, flag
68
+ yield
69
+ ensure
70
+ $VERBOSE = old_verbose
71
+ end
72
+ end
@@ -0,0 +1,97 @@
1
+ require 'rack'
2
+
3
+ class TestServer
4
+ attr_accessor :host, :port, :connect_count, :metrics, :last_api_key
5
+
6
+ def initialize(options={})
7
+ default_options = {
8
+ :listen => true,
9
+ :authenticate => true,
10
+ :response => true,
11
+ }
12
+ @options = default_options.merge(options)
13
+
14
+ @connect_count = 0
15
+ @connections = []
16
+ @metrics = Hash.new{ |h,k| h[k] = Array.new}
17
+ @last_api_key = ""
18
+ @host = 'http://localhost'
19
+ @main_thread = nil
20
+ @response = options[:response]
21
+ listen if @options[:listen]
22
+ end
23
+
24
+ def listen
25
+ @port ||= 10001
26
+ @server = TCPServer.new(@port)
27
+ @main_thread = Thread.new do
28
+ begin
29
+ loop do
30
+ begin
31
+ client = @server.accept
32
+ @connections << client
33
+ @connect_count += 1
34
+
35
+ while command = client.readline
36
+ if command.start_with? 'POST'
37
+ metric = command[/v1\/(.*)\sHTTP/, 1]
38
+ end
39
+ if command.start_with? "Content-Length: "
40
+ content_length = command.gsub("Content-Length: ", "").to_i
41
+ end
42
+ break if command == "\r\n"
43
+ end
44
+ content = client.read(content_length)
45
+ payload = Rack::Utils.parse_nested_query(content)
46
+ @metrics[metric] += payload["data"]
47
+ @last_api_key = payload[:api_key]
48
+
49
+ headers = ["HTTP/1.1 200 OK"].join("\r\n")
50
+ client.puts headers
51
+ client.close
52
+ rescue Exception => e
53
+ puts "Error in test server: #{e.inspect}"
54
+ end
55
+ end
56
+ end
57
+ end
58
+
59
+ rescue Errno::EADDRINUSE => err
60
+ puts "#{err.inspect} failed to get port #{@port}"
61
+ puts err.message
62
+ @port += 1
63
+ retry
64
+ end
65
+
66
+ def host_and_port
67
+ "#{host}:#{port}"
68
+ end
69
+
70
+ def stop
71
+ @stopping = true
72
+ disconnect_all
73
+ @main_thread.kill if @main_thread
74
+ @main_thread = nil
75
+ begin
76
+ @server.close if @server
77
+ rescue Exception => e
78
+ end
79
+ end
80
+
81
+ def disconnect_all
82
+ @connections.each { |c|
83
+ c.close rescue false
84
+ }
85
+ @connections = []
86
+ end
87
+
88
+ def fresh_agent(options = { })
89
+ PirateMetrics::Agent.new('test_token', { :host => host, :port => port, :enabled => true}.merge(options))
90
+ end
91
+
92
+ [:acquisitions, :activations, :retentions, :referrals, :revenues].each do |metric|
93
+ define_method metric do
94
+ @metrics[metric.to_s]
95
+ end
96
+ end
97
+ end
metadata ADDED
@@ -0,0 +1,212 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: pirate_metrics_agent
3
+ version: !ruby/object:Gem::Version
4
+ version: '0.1'
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Elijah Miller
9
+ - Christopher Zelenak
10
+ - Kristopher Chambers
11
+ - Matthew Hassfurder
12
+ - Expected Behavior
13
+ autorequire:
14
+ bindir: bin
15
+ cert_chain: []
16
+ date: 2012-10-31 00:00:00.000000000 Z
17
+ dependencies:
18
+ - !ruby/object:Gem::Dependency
19
+ name: rake
20
+ requirement: !ruby/object:Gem::Requirement
21
+ none: false
22
+ requirements:
23
+ - - ! '>='
24
+ - !ruby/object:Gem::Version
25
+ version: '0'
26
+ type: :development
27
+ prerelease: false
28
+ version_requirements: !ruby/object:Gem::Requirement
29
+ none: false
30
+ requirements:
31
+ - - ! '>='
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ - !ruby/object:Gem::Dependency
35
+ name: rack
36
+ requirement: !ruby/object:Gem::Requirement
37
+ none: false
38
+ requirements:
39
+ - - ! '>='
40
+ - !ruby/object:Gem::Version
41
+ version: '0'
42
+ type: :development
43
+ prerelease: false
44
+ version_requirements: !ruby/object:Gem::Requirement
45
+ none: false
46
+ requirements:
47
+ - - ! '>='
48
+ - !ruby/object:Gem::Version
49
+ version: '0'
50
+ - !ruby/object:Gem::Dependency
51
+ name: rspec
52
+ requirement: !ruby/object:Gem::Requirement
53
+ none: false
54
+ requirements:
55
+ - - ~>
56
+ - !ruby/object:Gem::Version
57
+ version: '2.0'
58
+ type: :development
59
+ prerelease: false
60
+ version_requirements: !ruby/object:Gem::Requirement
61
+ none: false
62
+ requirements:
63
+ - - ~>
64
+ - !ruby/object:Gem::Version
65
+ version: '2.0'
66
+ - !ruby/object:Gem::Dependency
67
+ name: fuubar
68
+ requirement: !ruby/object:Gem::Requirement
69
+ none: false
70
+ requirements:
71
+ - - ! '>='
72
+ - !ruby/object:Gem::Version
73
+ version: '0'
74
+ type: :development
75
+ prerelease: false
76
+ version_requirements: !ruby/object:Gem::Requirement
77
+ none: false
78
+ requirements:
79
+ - - ! '>='
80
+ - !ruby/object:Gem::Version
81
+ version: '0'
82
+ - !ruby/object:Gem::Dependency
83
+ name: pry
84
+ requirement: !ruby/object:Gem::Requirement
85
+ none: false
86
+ requirements:
87
+ - - ~>
88
+ - !ruby/object:Gem::Version
89
+ version: '0.9'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ none: false
94
+ requirements:
95
+ - - ~>
96
+ - !ruby/object:Gem::Version
97
+ version: '0.9'
98
+ - !ruby/object:Gem::Dependency
99
+ name: guard
100
+ requirement: !ruby/object:Gem::Requirement
101
+ none: false
102
+ requirements:
103
+ - - ! '>='
104
+ - !ruby/object:Gem::Version
105
+ version: '0'
106
+ type: :development
107
+ prerelease: false
108
+ version_requirements: !ruby/object:Gem::Requirement
109
+ none: false
110
+ requirements:
111
+ - - ! '>='
112
+ - !ruby/object:Gem::Version
113
+ version: '0'
114
+ - !ruby/object:Gem::Dependency
115
+ name: guard-rspec
116
+ requirement: !ruby/object:Gem::Requirement
117
+ none: false
118
+ requirements:
119
+ - - ! '>='
120
+ - !ruby/object:Gem::Version
121
+ version: '0'
122
+ type: :development
123
+ prerelease: false
124
+ version_requirements: !ruby/object:Gem::Requirement
125
+ none: false
126
+ requirements:
127
+ - - ! '>='
128
+ - !ruby/object:Gem::Version
129
+ version: '0'
130
+ - !ruby/object:Gem::Dependency
131
+ name: growl
132
+ requirement: !ruby/object:Gem::Requirement
133
+ none: false
134
+ requirements:
135
+ - - ! '>='
136
+ - !ruby/object:Gem::Version
137
+ version: '0'
138
+ type: :development
139
+ prerelease: false
140
+ version_requirements: !ruby/object:Gem::Requirement
141
+ none: false
142
+ requirements:
143
+ - - ! '>='
144
+ - !ruby/object:Gem::Version
145
+ version: '0'
146
+ - !ruby/object:Gem::Dependency
147
+ name: rb-fsevent
148
+ requirement: !ruby/object:Gem::Requirement
149
+ none: false
150
+ requirements:
151
+ - - ! '>='
152
+ - !ruby/object:Gem::Version
153
+ version: '0'
154
+ type: :development
155
+ prerelease: false
156
+ version_requirements: !ruby/object:Gem::Requirement
157
+ none: false
158
+ requirements:
159
+ - - ! '>='
160
+ - !ruby/object:Gem::Version
161
+ version: '0'
162
+ description: Get to know your customers.
163
+ email:
164
+ - support@piratemetrics.com
165
+ executables: []
166
+ extensions: []
167
+ extra_rdoc_files: []
168
+ files:
169
+ - .gitignore
170
+ - .rspec
171
+ - CHANGELOG.md
172
+ - Gemfile
173
+ - Guardfile
174
+ - LICENSE
175
+ - README.md
176
+ - Rakefile
177
+ - lib/pirate_metrics/agent.rb
178
+ - lib/pirate_metrics/system_timer.rb
179
+ - lib/pirate_metrics/version.rb
180
+ - lib/pirate_metrics_agent.rb
181
+ - pirate_metrics_agent.gemspec
182
+ - spec/agent_spec.rb
183
+ - spec/spec_helper.rb
184
+ - spec/test_server.rb
185
+ homepage: http://github.com/expectedbehavior/pirate_metrics_agent
186
+ licenses: []
187
+ post_install_message:
188
+ rdoc_options: []
189
+ require_paths:
190
+ - lib
191
+ required_ruby_version: !ruby/object:Gem::Requirement
192
+ none: false
193
+ requirements:
194
+ - - ! '>='
195
+ - !ruby/object:Gem::Version
196
+ version: '0'
197
+ required_rubygems_version: !ruby/object:Gem::Requirement
198
+ none: false
199
+ requirements:
200
+ - - ! '>='
201
+ - !ruby/object:Gem::Version
202
+ version: '0'
203
+ requirements: []
204
+ rubyforge_project:
205
+ rubygems_version: 1.8.24
206
+ signing_key:
207
+ specification_version: 3
208
+ summary: Agent for reporting data to piratemetrics.com
209
+ test_files:
210
+ - spec/agent_spec.rb
211
+ - spec/spec_helper.rb
212
+ - spec/test_server.rb