fluidfeatures 0.3.0 → 0.3.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,44 @@
1
+
2
+ require "fluidfeatures/const"
3
+
4
+ module FluidFeatures
5
+ class AppFeatureVersion
6
+
7
+ attr_accessor :app, :feature_name, :version_name
8
+
9
+ DEFAULT_VERSION_NAME = "default"
10
+
11
+ def initialize(app, feature_name, version_name=DEFAULT_VERSION_NAME)
12
+
13
+ raise "app invalid : #{app}" unless app.is_a? FluidFeatures::App
14
+ raise "feature_name invalid : #{feature_name}" unless feature_name.is_a? String
15
+ version_name ||= ::FluidFeatures::DEFAULT_VERSION_NAME
16
+ raise "version_name invalid : #{version_name}" unless version_name.is_a? String
17
+
18
+ @app = client
19
+ @feature_name = feature_name
20
+ @version_name = version_name
21
+
22
+ end
23
+
24
+ #
25
+ # This can be used to control how much of your user-base sees a
26
+ # particular feature. It may be easier to use the dashboard provided
27
+ # at https://www.fluidfeatures.com/dashboard to manage this.
28
+ #
29
+ def set_enabled_percent(percent)
30
+
31
+ unless percent.is_a? Numeric and percent >= 0.0 and percent <= 100.0
32
+ raise "percent invalid : #{percent}"
33
+ end
34
+
35
+ app.put("/feature/#{feature_name}/#{version_name}/enabled/percent", {
36
+ :enabled => {
37
+ :percent => enabled_percent
38
+ }
39
+ })
40
+ end
41
+
42
+ end
43
+ end
44
+
@@ -0,0 +1,180 @@
1
+
2
+ require "fluidfeatures/const"
3
+
4
+ module FluidFeatures
5
+ class AppUser
6
+
7
+ attr_accessor :app, :unique_id, :display_name, :anonymous, :unique_attrs, :cohort_attrs
8
+
9
+ def initialize(app, user_id, display_name, is_anonymous, unique_attrs, cohort_attrs)
10
+
11
+ raise "app invalid : #{app}" unless app.is_a? FluidFeatures::App
12
+
13
+ @app = app
14
+ @unique_id = user_id
15
+ @display_name = display_name
16
+ @anonymous = is_anonymous
17
+ @unique_attrs = unique_attrs
18
+ @cohort_attrs = cohort_attrs
19
+
20
+ @features = nil
21
+ @features_hit = {}
22
+ @goals_hit = {}
23
+ @unknown_features = {}
24
+
25
+ if not unique_id or is_anonymous
26
+
27
+ # We're an anonymous user
28
+ @anonymous = true
29
+
30
+ # if we were not given a user[:id] for this anonymous user, then get
31
+ # it from an existing cookie or create a new one.
32
+ unless unique_id
33
+ # Create new unique id (for cookie). Use rand + micro-seconds of current time
34
+ @unique_id = "anon-" + Random.rand(9999999999).to_s + "-" + ((Time.now.to_f * 1000000).to_i % 1000000).to_s
35
+ end
36
+ end
37
+
38
+ end
39
+
40
+ #
41
+ # Returns all the features enabled for a specific user.
42
+ # This will depend on the user's unique_id and how many
43
+ # users each feature is enabled for.
44
+ #
45
+ def load_features
46
+
47
+ # extract just attribute ids into simple hash
48
+ attribute_ids = {
49
+ :anonymous => anonymous
50
+ }
51
+ [unique_attrs, cohort_attrs].each do |attrs|
52
+ if attrs
53
+ attrs.each do |attr_key, attr|
54
+ if attr.is_a? Hash
55
+ if attr.has_key? :id
56
+ attribute_ids[attr_key] = attr[:id]
57
+ end
58
+ else
59
+ attribute_ids[attr_key] = attr
60
+ end
61
+ end
62
+ end
63
+ end
64
+
65
+ # normalize attributes ids as strings
66
+ attribute_ids.each do |attr_key, attr_id|
67
+ if attr_id.is_a? FalseClass or attr_id.is_a? TrueClass
68
+ attribute_ids[attr_key] = attr_id.to_s.downcase
69
+ elsif not attr_id.is_a? String
70
+ attribute_ids[attr_key] = attr_id.to_s
71
+ end
72
+ end
73
+
74
+ app.get("/user/#{unique_id}/features", attribute_ids) || {}
75
+ end
76
+
77
+ def features
78
+ @features ||= load_features
79
+ end
80
+
81
+ def feature_enabled?(feature_name, version_name, default_enabled)
82
+
83
+ raise "feature_name invalid : #{feature_name}" unless feature_name.is_a? String
84
+ version_name ||= ::FluidFeatures::DEFAULT_VERSION_NAME
85
+
86
+ if features.has_key? feature_name
87
+ feature = features[feature_name]
88
+ if feature.is_a? Hash
89
+ if feature.has_key? version_name
90
+ enabled = feature[version_name]
91
+ end
92
+ end
93
+ end
94
+
95
+ if enabled === nil
96
+ enabled = default_enabled
97
+
98
+ # Tell FluidFeatures about this amazing new feature...
99
+ unknown_feature_hit(feature_name, version_name, default_enabled)
100
+ end
101
+
102
+ if enabled
103
+ @features_hit[feature_name] ||= {}
104
+ @features_hit[feature_name][version_name.to_s] = {}
105
+ end
106
+
107
+ enabled
108
+ end
109
+
110
+ #
111
+ # This is called when we encounter a feature_name that
112
+ # FluidFeatures has no record of for your application.
113
+ # This will be reported back to the FluidFeatures service so
114
+ # that it can populate your dashboard with this feature.
115
+ # The parameter "default_enabled" is a boolean that says whether
116
+ # this feature should be enabled to all users or no users.
117
+ # Usually, this is "true" for existing features that you are
118
+ # planning to phase out and "false" for new feature that you
119
+ # intend to phase in.
120
+ #
121
+ def unknown_feature_hit(feature_name, version_name, default_enabled)
122
+ if not @unknown_features[feature_name]
123
+ @unknown_features[feature_name] = { :versions => {} }
124
+ end
125
+ @unknown_features[feature_name][:versions][version_name] = default_enabled
126
+ end
127
+
128
+ def goal_hit(goal_name, goal_version_name)
129
+ sleep 10
130
+ raise "goal_name invalid : #{goal_name}" unless goal_name.is_a? String
131
+ goal_version_name ||= ::FluidFeatures::DEFAULT_VERSION_NAME
132
+ raise "goal_version_name invalid : #{goal_version_name}" unless goal_version_name.is_a? String
133
+ @goals_hit[goal_name.to_s] ||= {}
134
+ @goals_hit[goal_name.to_s][goal_version_name.to_s] = {}
135
+ end
136
+
137
+ #
138
+ # This reports back to FluidFeatures which features we
139
+ # encountered during this request, the request duration,
140
+ # and statistics on time spent talking to the FluidFeatures
141
+ # service. Any new features encountered will also be reported
142
+ # back with the default_enabled status (see unknown_feature_hit)
143
+ # so that FluidFeatures can auto-populate the dashboard.
144
+ #
145
+ def end_transaction(url, stats)
146
+
147
+ payload = {
148
+ :url => url,
149
+ :user => {
150
+ :id => unique_id
151
+ },
152
+ :hits => {
153
+ :feature => @features_hit,
154
+ :goal => @goals_hit
155
+ }
156
+ }
157
+
158
+ if stats
159
+ raise "stats invalid : #{stats}" unless stats.is_a? Hash
160
+ payload[:stats] = stats
161
+ end
162
+
163
+ payload_user = payload[:user] ||= {}
164
+ payload_user[:name] = display_name if display_name
165
+ payload_user[:anonymous] = anonymous if anonymous
166
+ payload_user[:unique] = unique_attrs if unique_attrs
167
+ payload_user[:cohorts] = cohort_attrs if cohort_attrs
168
+
169
+ (payload[:stats] ||= {})[:ff_latency] = app.client.last_fetch_duration
170
+ if @unknown_features.size
171
+ (payload[:features] ||= {})[:unknown] = @unknown_features
172
+ @unknown_features = {}
173
+ end
174
+
175
+ app.post("/user/#{unique_id}/features/hit", payload)
176
+
177
+ end
178
+
179
+ end
180
+ end
@@ -0,0 +1,53 @@
1
+
2
+ require "fluidfeatures/app/user"
3
+ require "fluidfeatures/app/feature"
4
+
5
+ module FluidFeatures
6
+ class App
7
+
8
+ attr_accessor :client, :app_id, :secret, :logger
9
+
10
+ def initialize(client, app_id, secret, logger)
11
+
12
+ raise "client invalid : #{client}" unless client.is_a? FluidFeatures::Client
13
+ raise "app_id invalid : #{app_id}" unless app_id.is_a? String
14
+ raise "secret invalid : #{secret}" unless secret.is_a? String
15
+
16
+ @client = client
17
+ @app_id = app_id
18
+ @secret = secret
19
+ @logger = logger
20
+
21
+ end
22
+
23
+ def get(path, params=nil)
24
+ client.get("/app/#{app_id}#{path}", secret, params)
25
+ end
26
+
27
+ def put(path, payload)
28
+ client.put("/app/#{app_id}#{path}", secret, payload)
29
+ end
30
+
31
+ def post(path, payload)
32
+ client.post("/app/#{app_id}#{path}", secret, payload)
33
+ end
34
+
35
+ #
36
+ # Returns all the features that FluidFeatures knows about for
37
+ # your application. The enabled percentage (how much of your user-base)
38
+ # sees each feature is also provided.
39
+ #
40
+ def features
41
+ get("/features")
42
+ end
43
+
44
+ def user(user_id, display_name, is_anonymous, unique_attrs, cohort_attrs)
45
+ ::FluidFeatures::AppUser.new(self, user_id, display_name, is_anonymous, unique_attrs, cohort_attrs)
46
+ end
47
+
48
+ def feature_version(feature_name, version_name)
49
+ ::FluidFeatures::AppFeatureVersion.new(self, feature_name, version_name)
50
+ end
51
+
52
+ end
53
+ end
@@ -1,204 +1,97 @@
1
1
 
2
- require "logger"
2
+ require 'net/http'
3
+ require 'persistent_http'
4
+ require "pre_ruby192/uri" if RUBY_VERSION < "1.9.2"
3
5
 
4
- if RUBY_VERSION < "1.9.2"
5
- require "pre_ruby192/uri"
6
- end
6
+ require "fluidfeatures/app"
7
7
 
8
8
  module FluidFeatures
9
9
  class Client
10
10
 
11
- attr_accessor :logger
12
-
13
- def initialize(base_uri, app_id, secret, options={})
11
+ attr_accessor :base_uri, :logger, :last_fetch_duration
14
12
 
15
- @logger = options[:logger] || ::Logger.new(STDERR)
13
+ def initialize(base_uri, logger)
16
14
 
17
- @baseuri = base_uri
18
- @app_id = app_id
19
- @secret = secret
15
+ @logger = logger
16
+ @base_uri = base_uri
20
17
 
21
- @http = PersistentHTTP.new(
18
+ @http = ::PersistentHTTP.new(
22
19
  :name => 'fluidfeatures',
23
- :logger => @logger,
20
+ :logger => logger,
24
21
  :pool_size => 10,
25
22
  :warn_timeout => 0.25,
26
23
  :force_retry => true,
27
- :url => @baseuri
24
+ :url => base_uri
28
25
  )
29
26
 
30
- @unknown_features = {}
31
27
  @last_fetch_duration = nil
32
28
 
33
29
  end
34
30
 
35
- #
36
- # This can be used to control how much of your user-base sees a
37
- # particular feature. It may be easier to use the dashboard provided
38
- # at https://www.fluidfeatures.com/dashboard to manage this, or to
39
- # set timers to automate the gradual rollout of your new features.
40
- #
41
- def feature_set_enabled_percent(feature_name, enabled_percent)
42
- begin
43
- uri = URI(@baseuri + "/app/" + @app_id.to_s + "/features/" + feature_name.to_s)
44
- request = Net::HTTP::Put.new uri.path
45
- request["Content-Type"] = "application/json"
46
- request["Accept"] = "application/json"
47
- request['AUTHORIZATION'] = @secret
48
- payload = {
49
- :enabled => {
50
- :percent => enabled_percent
51
- }
52
- }
53
- request.body = JSON.dump(payload)
54
- response = @http.request uri, request
55
- if response.is_a?(Net::HTTPSuccess)
56
- logger.error{"[FF] [" + response.code.to_s + "] Failed to set feature enabled percent : " + uri.to_s + " : " + response.body.to_s}
31
+ def get(path, auth_token, url_params=nil)
32
+ payload = nil
33
+
34
+ uri = URI(@base_uri + path)
35
+ url_path = uri.path
36
+ if url_params
37
+ uri.query = URI.encode_www_form( url_params )
38
+ if uri.query
39
+ url_path += "?" + uri.query
57
40
  end
58
- rescue
59
- logger.error{"[FF] Request to set feature enabled percent failed : " + uri.to_s}
60
- raise
61
41
  end
62
- end
63
42
 
64
- #
65
- # Returns all the features that FluidFeatures knows about for
66
- # your application. The enabled percentage (how much of your user-base)
67
- # sees each feature is also provided.
68
- #
69
- def get_feature_set
70
- features = nil
71
43
  begin
72
- uri = URI(@baseuri + "/app/" + @app_id.to_s + "/features")
73
- request = Net::HTTP::Get.new uri.path
44
+ request = Net::HTTP::Get.new url_path
74
45
  request["Accept"] = "application/json"
75
- request['AUTHORIZATION'] = @secret
46
+ request['AUTHORIZATION'] = auth_token
47
+ fetch_start_time = Time.now
76
48
  response = @http.request request
77
49
  if response.is_a?(Net::HTTPSuccess)
78
- features = JSON.parse(response.body)
50
+ payload = JSON.parse(response.body)
51
+ @last_fetch_duration = Time.now - fetch_start_time
79
52
  end
80
53
  rescue
81
- logger.error{"[FF] Request failed when getting feature set from " + uri.to_s}
54
+ logger.error{"[FF] Request failed when getting #{path}"}
82
55
  raise
83
56
  end
84
- if not features
85
- logger.error{"[FF] Empty feature set returned from " + uri.to_s}
57
+ if not payload
58
+ logger.error{"[FF] Empty response from #{path}"}
86
59
  end
87
- features
60
+ payload
88
61
  end
89
62
 
90
- #
91
- # Returns all the features enabled for a specific user.
92
- # This will depend on the user_id and how many users each
93
- # feature is enabled for.
94
- #
95
- def get_user_features(user)
96
- logger.debug{"[FF] get_user_features #{user}"}
97
- if not user
98
- raise "user object should be a Hash"
99
- end
100
- if not user[:id]
101
- raise "user does not contain :id field"
102
- end
103
-
104
- # extract just attribute ids into simple hash
105
- attribute_ids = {
106
- :anonymous => !!user[:anonymous]
107
- }
108
- [:unique, :cohorts].each do |attr_type|
109
- if user.has_key? attr_type
110
- user[attr_type].each do |attr_key, attr|
111
- if attr.is_a? Hash
112
- if attr.has_key? :id
113
- attribute_ids[attr_key] = attr[:id]
114
- end
115
- else
116
- attribute_ids[attr_key] = attr
117
- end
118
- end
119
- end
120
- end
121
-
122
- # normalize attributes ids as strings
123
- attribute_ids.each do |attr_key, attr_id|
124
- if attr_id.is_a? FalseClass or attr_id.is_a? TrueClass
125
- attribute_ids[attr_key] = attr_id.to_s.downcase
126
- elsif not attr_id.is_a? String
127
- attribute_ids[attr_key] = attr_id.to_s
128
- end
129
- end
130
-
131
- features = {}
132
- fetch_start_time = Time.now
63
+ def put(path, auth_token, payload)
133
64
  begin
134
- uri = URI("#{@baseuri}/app/#{@app_id}/user/#{user[:id]}/features")
135
- uri.query = URI.encode_www_form( attribute_ids )
136
- url_path = uri.path
137
- if uri.query
138
- url_path += "?" + uri.query
139
- end
140
- request = Net::HTTP::Get.new url_path
65
+ uri = URI(@base_uri + path)
66
+ request = Net::HTTP::Put.new uri.path
67
+ request["Content-Type"] = "application/json"
141
68
  request["Accept"] = "application/json"
142
- request['AUTHORIZATION'] = @secret
143
- response = @http.request request
144
- if response.is_a?(Net::HTTPSuccess)
145
- features = JSON.parse(response.body)
146
- else
147
- logger.error{"[FF] [#{response.code}] Failed to get user features : #{uri} : #{response.body}"}
69
+ request['AUTHORIZATION'] = auth_token
70
+ request.body = JSON.dump(payload)
71
+ response = @http.request uri, request
72
+ unless response.is_a?(Net::HTTPSuccess)
73
+ logger.error{"[FF] Request unsuccessful when putting #{path}"}
148
74
  end
149
- rescue
150
- logger.error{"[FF] Request to get user features failed : #{uri}"}
75
+ rescue Exception => err
76
+ logger.error{"[FF] Request failed putting #{path} : #{err.message}"}
151
77
  raise
152
78
  end
153
- @last_fetch_duration = Time.now - fetch_start_time
154
- features
155
- end
156
-
157
- #
158
- # This is called when we encounter a feature_name that
159
- # FluidFeatures has no record of for your application.
160
- # This will be reported back to the FluidFeatures service so
161
- # that it can populate your dashboard with this feature.
162
- # The parameter "default_enabled" is a boolean that says whether
163
- # this feature should be enabled to all users or no users.
164
- # Usually, this is "true" for existing features that you are
165
- # planning to phase out and "false" for new feature that you
166
- # intend to phase in.
167
- #
168
- def unknown_feature_hit(feature_name, version_name, defaults)
169
- if not @unknown_features[feature_name]
170
- @unknown_features[feature_name] = { :versions => {} }
171
- end
172
- @unknown_features[feature_name][:versions][version_name] = defaults
173
79
  end
174
-
175
- #
176
- # This reports back to FluidFeatures which features we
177
- # encountered during this request, the request duration,
178
- # and statistics on time spent talking to the FluidFeatures
179
- # service. Any new features encountered will also be reported
180
- # back with the default_enabled status (see unknown_feature_hit)
181
- # so that FluidFeatures can auto-populate the dashboard.
182
- #
183
- def log_request(user_id, payload)
80
+
81
+ def post(path, auth_token, payload)
184
82
  begin
185
- (payload[:stats] ||= {})[:ff_latency] = @last_fetch_duration
186
- if @unknown_features.size
187
- (payload[:features] ||= {})[:unknown] = @unknown_features
188
- @unknown_features = {}
189
- end
190
- uri = URI(@baseuri + "/app/#{@app_id}/user/#{user_id}/features/hit")
83
+ uri = URI(@base_uri + path)
191
84
  request = Net::HTTP::Post.new uri.path
192
85
  request["Content-Type"] = "application/json"
193
86
  request["Accept"] = "application/json"
194
- request['AUTHORIZATION'] = @secret
87
+ request['AUTHORIZATION'] = auth_token
195
88
  request.body = JSON.dump(payload)
196
89
  response = @http.request request
197
90
  unless response.is_a?(Net::HTTPSuccess)
198
- logger.error{"[FF] [" + response.code.to_s + "] Failed to log features hit : " + uri.to_s + " : " + response.body.to_s}
91
+ logger.error{"[FF] Request unsuccessful when posting #{path}"}
199
92
  end
200
- rescue Exception => e
201
- logger.error{"[FF] Request to log user features hit failed : " + uri.to_s}
93
+ rescue Exception => err
94
+ logger.error{"[FF] Request failed posting #{path} : #{err.message}"}
202
95
  raise
203
96
  end
204
97
  end
@@ -0,0 +1,4 @@
1
+
2
+ module FluidFeatures
3
+ DEFAULT_VERSION_NAME = "default"
4
+ end
@@ -1,3 +1,3 @@
1
1
  module FluidFeatures
2
- VERSION = '0.3.0'
2
+ VERSION = '0.3.1'
3
3
  end
@@ -0,0 +1,15 @@
1
+
2
+ require "logger"
3
+
4
+ require "fluidfeatures/client"
5
+ require "fluidfeatures/app"
6
+
7
+ module FluidFeatures
8
+
9
+ def self.app(base_uri, app_id, secret, logger=nil)
10
+ logger ||= ::Logger.new(STDERR)
11
+ client = ::FluidFeatures::Client.new(base_uri, logger)
12
+ ::FluidFeatures::App.new(client, app_id, secret, logger)
13
+ end
14
+
15
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fluidfeatures
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.3.1
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -38,7 +38,12 @@ files:
38
38
  - Gemfile
39
39
  - README.md
40
40
  - fluidfeatures.gemspec
41
+ - lib/fluidfeatures.rb
42
+ - lib/fluidfeatures/app.rb
43
+ - lib/fluidfeatures/app/feature.rb
44
+ - lib/fluidfeatures/app/user.rb
41
45
  - lib/fluidfeatures/client.rb
46
+ - lib/fluidfeatures/const.rb
42
47
  - lib/fluidfeatures/version.rb
43
48
  - lib/pre_ruby192/uri.rb
44
49
  homepage: https://github.com/FluidFeatures/fluidfeatures-ruby