fluidfeatures 0.4.1 → 0.4.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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