fluidfeatures 0.4.1 → 0.4.4

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore CHANGED
@@ -1,6 +1,8 @@
1
1
  *.gem
2
2
  *.swp
3
3
  .bundle
4
+ .idea
5
+ .rvmrc
4
6
  Gemfile.lock
5
7
  pkg/*
6
8
  *.rbc
data/.travis.yml ADDED
@@ -0,0 +1,7 @@
1
+ language: ruby
2
+ rvm:
3
+ - 1.9.3
4
+ gemfile:
5
+ - Gemfile
6
+ script: bundle exec rake spec
7
+ env: FLUIDFEATURES_APPID=1vu33ki6emqe3 FLUIDFEATURES_SECRET=secret FLUIDFEATURES_BASEURI=https://www.fluidfeatures.com/service
data/Guardfile ADDED
@@ -0,0 +1,6 @@
1
+ guard 'rspec' do
2
+ watch(%r{^spec/.+_spec\.rb$})
3
+ watch(%r{^lib/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" }
4
+ watch(%r{^lib/fluidfeatures/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" }
5
+ watch('spec/spec_helper.rb') { "spec" }
6
+ end
data/Rakefile ADDED
@@ -0,0 +1,3 @@
1
+ require 'rspec/core/rake_task'
2
+
3
+ RSpec::Core::RakeTask.new('spec')
@@ -15,4 +15,11 @@ Gem::Specification.new do |s|
15
15
  s.require_paths = ["lib"]
16
16
  s.add_dependency "persistent_http", "~>1.0.3"
17
17
  s.add_dependency "uuid", "~>2.3.5"
18
+
19
+ s.add_development_dependency('rake', '~> 10.0.2')
20
+ s.add_development_dependency('rspec', '~> 2.12.0')
21
+ s.add_development_dependency('guard-rspec', '~> 2.2.1')
22
+ s.add_development_dependency('rb-inotify', '~> 0.8.8')
23
+ s.add_development_dependency('vcr', '~> 2.3.0')
24
+ s.add_development_dependency('fakeweb', '~> 1.3.0')
18
25
  end
@@ -15,7 +15,7 @@ module FluidFeatures
15
15
  version_name ||= ::FluidFeatures::DEFAULT_VERSION_NAME
16
16
  raise "version_name invalid : #{version_name}" unless version_name.is_a? String
17
17
 
18
- @app = client
18
+ @app = app
19
19
  @feature_name = feature_name
20
20
  @version_name = version_name
21
21
 
@@ -34,7 +34,7 @@ module FluidFeatures
34
34
 
35
35
  app.put("/feature/#{feature_name}/#{version_name}/enabled/percent", {
36
36
  :enabled => {
37
- :percent => enabled_percent
37
+ :percent => percent
38
38
  }
39
39
  })
40
40
  end
@@ -1,4 +1,3 @@
1
-
2
1
  require "fluidfeatures/const"
3
2
  require "thread"
4
3
 
@@ -27,9 +26,12 @@ module FluidFeatures
27
26
  WAIT_BETWEEN_SEND_FAILURES = 5 # seconds
28
27
 
29
28
  def initialize(app)
30
-
31
29
  raise "app invalid : #{app}" unless app.is_a? ::FluidFeatures::App
30
+ configure(app)
31
+ run_loop
32
+ end
32
33
 
34
+ def configure(app)
33
35
  @app = app
34
36
 
35
37
  @buckets = []
@@ -41,13 +43,10 @@ module FluidFeatures
41
43
 
42
44
  @unknown_features = {}
43
45
  @unknown_features_lock = ::Mutex.new
44
-
45
- run_transcation_sender
46
-
47
46
  end
48
47
 
49
- # Pass FluidFeatures::AppTransaction for reporting back to the
50
- # FluidFeatures service.
48
+ # Pass FluidFeatures::AppUserTransaction for reporting
49
+ # back to the FluidFeatures service.
51
50
  def report_transaction(transaction)
52
51
 
53
52
  user = transaction.user
@@ -81,7 +80,7 @@ module FluidFeatures
81
80
 
82
81
  end
83
82
 
84
- def run_transcation_sender
83
+ def run_loop
85
84
  Thread.new do
86
85
  while true
87
86
  begin
@@ -91,7 +90,7 @@ module FluidFeatures
91
90
  next
92
91
  end
93
92
 
94
- success = send_transcations
93
+ success = send_transactions
95
94
 
96
95
  if success
97
96
  # Unless we have a full bucket waiting do not make
@@ -109,7 +108,7 @@ module FluidFeatures
109
108
 
110
109
  rescue Exception => err
111
110
  # catch errors, so that we do not affect the rest of the application
112
- app.logger.error "[FF] send_transcations failed : #{err.message}\n#{err.backtrace.join("\n")}"
111
+ app.logger.error "[FF] send_transactions failed : #{err.message}\n#{err.backtrace.join("\n")}"
113
112
  # hold off for a little while and try again
114
113
  sleep WAIT_BETWEEN_SEND_FAILURES
115
114
  end
@@ -135,9 +134,9 @@ module FluidFeatures
135
134
  end
136
135
 
137
136
  @private
138
- def send_transcations
137
+ def send_transactions
139
138
  bucket = remove_bucket
140
-
139
+
141
140
  # Take existing unknown features and reset
142
141
  unknown_features = nil
143
142
  @unknown_features_lock.synchronize do
@@ -173,7 +172,7 @@ module FluidFeatures
173
172
  unless success
174
173
  # return bucket into bucket queue until the next attempt at sending
175
174
  if not unremove_bucket(bucket)
176
- app.logger.warn "[FF] Discarded #{discarded_bucket.size} transactions due to reporter backlog. These will not be reported to FluidFeatures."
175
+ app.logger.warn "[FF] Discarded transactions due to reporter backlog. These will not be reported to FluidFeatures."
177
176
  end
178
177
  # return unknown features to queue until the next attempt at sending
179
178
  queue_unknown_features(unknown_features)
@@ -251,7 +250,7 @@ module FluidFeatures
251
250
  def queue_unknown_features(unknown_features)
252
251
  raise "unknown_features should be a Hash" unless unknown_features.is_a? Hash
253
252
  unknown_features.each_pair do |feature_name, versions|
254
- raise "unknown_features values should be Hash. versions=#{versions}" unless versions.is_a? Hash
253
+ raise "unknown_features values should be a Hash. versions=#{versions}" unless versions.is_a? Hash
255
254
  end
256
255
  @unknown_features_lock.synchronize do
257
256
  unknown_features.each_pair do |feature_name, versions|
@@ -1,4 +1,3 @@
1
-
2
1
  require "digest/sha1"
3
2
  require "set"
4
3
  require "thread"
@@ -8,10 +7,11 @@ require "fluidfeatures/app/transaction"
8
7
 
9
8
  module FluidFeatures
10
9
  class AppState
11
-
10
+
12
11
  attr_accessor :app
13
- USER_ID_NUMERIC = Regexp.compile("^\d+$")
14
-
12
+
13
+ USER_ID_NUMERIC = /^\d+$/
14
+
15
15
  # Request to FluidFeatures API to long-poll for max
16
16
  # 30 seconds. The API may choose a different duration.
17
17
  # If not change in this time, API will return HTTP 304.
@@ -28,15 +28,15 @@ module FluidFeatures
28
28
  WAIT_BETWEEN_FETCH_FAILURES = 5 # seconds
29
29
 
30
30
  def initialize(app)
31
+ raise "app invalid : #{app}" unless app.is_a? ::FluidFeatures::App
32
+ configure(app)
33
+ run_loop
34
+ end
31
35
 
32
- raise "app invalid : #{app}" unless app.is_a? FluidFeatures::App
33
-
36
+ def configure(app)
34
37
  @app = app
35
38
  @features = {}
36
39
  @features_lock = ::Mutex.new
37
-
38
- run_state_fetcher
39
-
40
40
  end
41
41
 
42
42
  def features
@@ -54,7 +54,7 @@ module FluidFeatures
54
54
  end
55
55
  end
56
56
 
57
- def run_state_fetcher
57
+ def run_loop
58
58
  Thread.new do
59
59
  while true
60
60
  begin
@@ -105,7 +105,7 @@ module FluidFeatures
105
105
  raise "version_name invalid : #{version_name}" unless version_name.is_a? String
106
106
 
107
107
  #assert(isinstance(user_id, basestring))
108
-
108
+
109
109
  user_attributes ||= {}
110
110
  user_attributes["user"] = user_id.to_s
111
111
  if user_id.is_a? Integer
@@ -122,7 +122,6 @@ module FluidFeatures
122
122
  modulus = user_id_hash % feature["num_parts"]
123
123
  enabled = version["parts"].include? modulus
124
124
 
125
- # check attributes
126
125
  feature["versions"].each_pair do |other_version_name, other_version|
127
126
  if other_version
128
127
  version_attributes = (other_version["enabled"] || {})["attributes"]
@@ -12,7 +12,7 @@ module FluidFeatures
12
12
  @url = url
13
13
 
14
14
  # take a snap-shot of the features end at
15
- # the beginning of the transaction
15
+ # the beginning of the transactionapplication
16
16
  @features = user.features
17
17
 
18
18
  @features_hit = {}
@@ -99,11 +99,10 @@ module FluidFeatures
99
99
  # so that FluidFeatures can auto-populate the dashboard.
100
100
  #
101
101
  def end_transaction
102
-
103
- raise "transaction already ended" if ended
104
- @ended = true
105
- @duration = Time.now - start_time
102
+ raise "transaction ended" if ended
103
+ @duration = duration #Time.now - start_time
106
104
  user.app.reporter.report_transaction(self)
105
+ @ended = true
107
106
  end
108
107
 
109
108
  end
@@ -76,7 +76,7 @@ module FluidFeatures
76
76
  end
77
77
 
78
78
  if ENV["FLUIDFEATURES_USER_FEATURES_FROM_API"]
79
- features_enabled = get("/features", attribute_ids) || {}
79
+ success, features_enabled = get("/features", attribute_ids) || {}
80
80
  else
81
81
  features_enabled = {}
82
82
  app.state.features.each do |feature_name, feature|
@@ -4,6 +4,7 @@ require "persistent_http"
4
4
  require "pre_ruby192/uri" if RUBY_VERSION < "1.9.2"
5
5
  require "thread"
6
6
  require "uuid"
7
+ require "json"
7
8
 
8
9
  require "fluidfeatures/app"
9
10
 
@@ -149,14 +150,14 @@ module FluidFeatures
149
150
  success = false
150
151
  begin
151
152
 
152
- request = Net::HTTP::Put.new uri_path
153
+ request = Net::HTTP::Put.new url_path
153
154
  request["Authorization"] = auth_token
154
155
  request["Accept"] = "application/json"
155
156
  request["Accept-Encoding"] = "gzip"
156
157
  encode_request_body(request, payload)
157
158
 
158
159
  request_start_time = Time.now
159
- response = @http.request uri, request
160
+ response = @http.request request
160
161
  duration = Time.now - request_start_time
161
162
 
162
163
  raise "expected Net::HTTPResponse" if not response.is_a? Net::HTTPResponse
@@ -1,3 +1,3 @@
1
1
  module FluidFeatures
2
- VERSION = '0.4.1'
2
+ VERSION = '0.4.4'
3
3
  end
@@ -0,0 +1,58 @@
1
+ require "spec_helper"
2
+
3
+ describe FluidFeatures::AppFeatureVersion do
4
+
5
+ let(:app) { mock "FluidFeatures::App", client: mock("client", uuid: 'client uuid'), logger: mock('logger') }
6
+
7
+ before(:each) do
8
+ app.stub!(:is_a?).and_return(false)
9
+ app.stub!(:is_a?).with(FluidFeatures::App).and_return(true)
10
+ end
11
+
12
+ context "initialization" do
13
+
14
+ it "should raise error if invalid application passed" do
15
+ app.stub!(:is_a?).with(FluidFeatures::App).and_return(false)
16
+ expect { described_class.new(app, "Feature", "a") }.to raise_error /app invalid/
17
+ end
18
+
19
+ it "should raise error if invalid feature name passed" do
20
+ expect { described_class.new(app, 0, "a") }.to raise_error /feature_name invalid/
21
+
22
+ end
23
+
24
+ it "should raise error if invalid version name passed" do
25
+ expect { described_class.new(app, "Feature", 0) }.to raise_error /version_name invalid/
26
+ end
27
+
28
+ it "should use default version name if omitted" do
29
+ described_class.new(app, "Feature").version_name.should == described_class::DEFAULT_VERSION_NAME
30
+ end
31
+
32
+ it "should initialize instance variables with passed values" do
33
+ feature = described_class.new(app, "Feature", "a")
34
+ feature.app.should == app
35
+ feature.feature_name.should == "Feature"
36
+ feature.version_name.should == "a"
37
+ end
38
+
39
+ end
40
+
41
+ describe "#set_enabled_percent" do
42
+
43
+ let(:feature) { described_class.new(app, "Feature", "a") }
44
+
45
+ ["50", 120, -12.2, nil].each do |invalid_percent|
46
+ it "should raise error if '#{invalid_percent}' passed as percentage value" do
47
+ expect { feature.set_enabled_percent(invalid_percent) }.to raise_error /percent invalid/
48
+ end
49
+ end
50
+
51
+ it "should update percentage on server" do
52
+ app.should_receive(:put).with("/feature/Feature/a/enabled/percent", {:enabled=>{:percent=>44}})
53
+ feature.set_enabled_percent(44)
54
+ end
55
+
56
+ end
57
+
58
+ end
@@ -0,0 +1,127 @@
1
+ require 'spec_helper'
2
+
3
+ describe FluidFeatures::AppReporter do
4
+
5
+ it_should_behave_like "polling loop", :send_transactions do
6
+ before(:each) { described_class.any_instance.stub(:transactions_queued?).and_return(true) }
7
+ end
8
+
9
+ context do
10
+
11
+ let(:reporter) { described_class.new(app) }
12
+
13
+ let(:user) { mock('user', unique_id: 'unique id', display_name: 'John Doe', anonymous: false, unique_attrs: [], cohort_attrs: []) }
14
+
15
+ let(:transaction) { mock('transaction', url: 'http://example.com/source.html', duration: 999, user: user, unknown_features: [], features_hit: %w[feature], goals_hit: %w[goal]) }
16
+
17
+ before(:each) do
18
+ described_class.any_instance.stub(:run_loop)
19
+ end
20
+
21
+ describe "#report_transaction" do
22
+
23
+ before(:each) do
24
+ reporter.stub!(:queue_transaction_payload)
25
+ reporter.stub!(:queue_unknown_features)
26
+ end
27
+
28
+ it "should queue transaction payload" do
29
+ reporter.should_not_receive(:queue_unknown_features)
30
+ reporter.should_receive(:queue_transaction_payload).with(
31
+ url: "http://example.com/source.html",
32
+ user: { id: "unique id", name: "John Doe", unique: [], cohorts: [] },
33
+ hits: { feature: ["feature"], goal: ["goal"] },
34
+ stats: { duration: 999 }
35
+ )
36
+ reporter.report_transaction(transaction)
37
+ end
38
+
39
+ it "should queue unknown features if any" do
40
+ transaction.stub!(:unknown_features).and_return(%w[feature])
41
+ reporter.should_receive(:queue_unknown_features).with(transaction.unknown_features)
42
+ reporter.report_transaction(transaction)
43
+ end
44
+
45
+ end
46
+
47
+ describe "#transactions_queued?" do
48
+ before(:each) do
49
+ reporter.configure(app)
50
+ end
51
+
52
+ it "should return false if no transaction queued" do
53
+ reporter.transactions_queued?.should be_false
54
+ end
55
+
56
+ it "should return true if at least one transaction queued" do
57
+ reporter.instance_variable_set(:@buckets, %w[bucket])
58
+ reporter.instance_variable_set(:@current_bucket, %w[payload])
59
+ reporter.transactions_queued?.should be_true
60
+ end
61
+ end
62
+
63
+ describe "#queue_transaction_payload" do
64
+ it "should push transaction payload to current bucket" do
65
+ reporter.instance_variable_set(:@current_bucket, [])
66
+ reporter.queue_transaction_payload({ foo: 'bar' })
67
+ reporter.instance_variable_get(:@current_bucket).should == [{ foo: 'bar' }]
68
+ end
69
+
70
+ it "should create new bucket if current bucket is full" do
71
+ reporter.instance_variable_set(:@current_bucket, [])
72
+ current_bucket = reporter.instance_variable_get(:@current_bucket)
73
+ current_bucket.stub!(:size).and_return(described_class::MAX_BUCKET_SIZE + 1)
74
+ reporter.should_receive(:new_bucket).and_return([])
75
+ reporter.queue_transaction_payload({ foo: 'bar' })
76
+ end
77
+ end
78
+
79
+ describe "#queue_unknown_features" do
80
+ let(:unknown_features) { { "feature" => { "a" => true } } }
81
+ it "should add unknown_features to instance variable" do
82
+ reporter.queue_unknown_features(unknown_features)
83
+ reporter.instance_variable_get(:@unknown_features).should == unknown_features
84
+ end
85
+
86
+ it "should raise error if unknown_features is not Hash" do
87
+ expect { reporter.queue_unknown_features("not cool") }.to raise_error /should be a Hash/
88
+ end
89
+
90
+ it "should raise error if versions is not Hash" do
91
+ expect { reporter.queue_unknown_features("feature" => "not cool") }.to raise_error /should be a Hash/
92
+ end
93
+
94
+ end
95
+
96
+ describe "#send_transactions" do
97
+ let(:unknown_features) { { "feature" => { "a" => true } } }
98
+ let(:app) { mock "FluidFeatures::App", client: mock("client", uuid: 'client uuid'), logger: mock('logger') }
99
+
100
+ before(:each) do
101
+ app.stub!(:is_a?).and_return(false)
102
+ app.stub!(:is_a?).with(FluidFeatures::App).and_return(true)
103
+ app.client.stub(:siphon_api_request_log).and_return("api log")
104
+ app.stub!(:post)
105
+ reporter.stub(:remove_bucket).and_return(["transactions"])
106
+ reporter.instance_variable_set(:@unknown_features, unknown_features)
107
+ reporter.instance_variable_set(:@buckets, [%w[one], %w[one two]])
108
+ end
109
+
110
+ it "should send transaction to server" do
111
+ app.should_receive(:post).with("/report/transactions", :client_uuid=>"client uuid", :transactions=>["transactions"], :stats=>{:waiting_buckets=>[1, 2]}, :unknown_features=>{"feature"=>{"a"=>true}}, :api_request_log=>"api log").and_return(true)
112
+ reporter.send_transactions
113
+ end
114
+
115
+ it "should emit warning and queue unknown features on failure" do
116
+ app.stub!(:post).and_return(false)
117
+ reporter.stub!(:unremove_bucket).and_return(false)
118
+ app.logger.should_receive(:warn)
119
+ reporter.should_receive(:queue_unknown_features).with(unknown_features)
120
+ reporter.send_transactions
121
+ end
122
+
123
+ end
124
+
125
+ end
126
+
127
+ end