fluidfeatures 0.3.0 → 0.3.1

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.
@@ -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