fluidfeatures 0.3.0
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 +21 -0
- data/Gemfile +4 -0
- data/README.md +7 -0
- data/fluidfeatures.gemspec +17 -0
- data/lib/fluidfeatures/client.rb +207 -0
- data/lib/fluidfeatures/version.rb +3 -0
- data/lib/pre_ruby192/uri.rb +120 -0
- metadata +68 -0
data/.gitignore
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
*.gem
|
2
|
+
*.swp
|
3
|
+
.bundle
|
4
|
+
Gemfile.lock
|
5
|
+
pkg/*
|
6
|
+
*.rbc
|
7
|
+
*.sassc
|
8
|
+
.sass-cache
|
9
|
+
capybara-*.html
|
10
|
+
.rspec
|
11
|
+
/.bundle
|
12
|
+
/vendor/bundle
|
13
|
+
/log/*
|
14
|
+
/tmp/*
|
15
|
+
/db/*.sqlite3
|
16
|
+
/public/system/*
|
17
|
+
/coverage/
|
18
|
+
/spec/tmp/*
|
19
|
+
**.orig
|
20
|
+
rerun.txt
|
21
|
+
pickle-email-*.html
|
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "fluidfeatures/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "fluidfeatures"
|
7
|
+
s.version = FluidFeatures::VERSION
|
8
|
+
s.authors = ["Phil Whelan"]
|
9
|
+
s.email = ["phil@fluidfeatures.com"]
|
10
|
+
s.homepage = "https://github.com/FluidFeatures/fluidfeatures-ruby"
|
11
|
+
s.summary = %q{Ruby client for the FluidFeatures service.}
|
12
|
+
s.description = %q{Ruby client for the FluidFeatures service.}
|
13
|
+
s.rubyforge_project = s.name
|
14
|
+
s.files = `git ls-files`.split("\n")
|
15
|
+
s.require_paths = ["lib"]
|
16
|
+
s.add_dependency "persistent_http", "~>1.0.3"
|
17
|
+
end
|
@@ -0,0 +1,207 @@
|
|
1
|
+
|
2
|
+
require "logger"
|
3
|
+
|
4
|
+
if RUBY_VERSION < "1.9.2"
|
5
|
+
require "pre_ruby192/uri"
|
6
|
+
end
|
7
|
+
|
8
|
+
module FluidFeatures
|
9
|
+
class Client
|
10
|
+
|
11
|
+
attr_accessor :logger
|
12
|
+
|
13
|
+
def initialize(base_uri, app_id, secret, options={})
|
14
|
+
|
15
|
+
@logger = options[:logger] || ::Logger.new(STDERR)
|
16
|
+
|
17
|
+
@baseuri = base_uri
|
18
|
+
@app_id = app_id
|
19
|
+
@secret = secret
|
20
|
+
|
21
|
+
@http = PersistentHTTP.new(
|
22
|
+
:name => 'fluidfeatures',
|
23
|
+
:logger => @logger,
|
24
|
+
:pool_size => 10,
|
25
|
+
:warn_timeout => 0.25,
|
26
|
+
:force_retry => true,
|
27
|
+
:url => @baseuri
|
28
|
+
)
|
29
|
+
|
30
|
+
@unknown_features = {}
|
31
|
+
@last_fetch_duration = nil
|
32
|
+
|
33
|
+
end
|
34
|
+
|
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}
|
57
|
+
end
|
58
|
+
rescue
|
59
|
+
logger.error{"[FF] Request to set feature enabled percent failed : " + uri.to_s}
|
60
|
+
raise
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
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
|
+
begin
|
72
|
+
uri = URI(@baseuri + "/app/" + @app_id.to_s + "/features")
|
73
|
+
request = Net::HTTP::Get.new uri.path
|
74
|
+
request["Accept"] = "application/json"
|
75
|
+
request['AUTHORIZATION'] = @secret
|
76
|
+
response = @http.request request
|
77
|
+
if response.is_a?(Net::HTTPSuccess)
|
78
|
+
features = JSON.parse(response.body)
|
79
|
+
end
|
80
|
+
rescue
|
81
|
+
logger.error{"[FF] Request failed when getting feature set from " + uri.to_s}
|
82
|
+
raise
|
83
|
+
end
|
84
|
+
if not features
|
85
|
+
logger.error{"[FF] Empty feature set returned from " + uri.to_s}
|
86
|
+
end
|
87
|
+
features
|
88
|
+
end
|
89
|
+
|
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
|
133
|
+
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
|
141
|
+
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}"}
|
148
|
+
end
|
149
|
+
rescue
|
150
|
+
logger.error{"[FF] Request to get user features failed : #{uri}"}
|
151
|
+
raise
|
152
|
+
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
|
+
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)
|
184
|
+
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")
|
191
|
+
request = Net::HTTP::Post.new uri.path
|
192
|
+
request["Content-Type"] = "application/json"
|
193
|
+
request["Accept"] = "application/json"
|
194
|
+
request['AUTHORIZATION'] = @secret
|
195
|
+
request.body = JSON.dump(payload)
|
196
|
+
response = @http.request request
|
197
|
+
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}
|
199
|
+
end
|
200
|
+
rescue Exception => e
|
201
|
+
logger.error{"[FF] Request to log user features hit failed : " + uri.to_s}
|
202
|
+
raise
|
203
|
+
end
|
204
|
+
end
|
205
|
+
|
206
|
+
end
|
207
|
+
end
|
@@ -0,0 +1,120 @@
|
|
1
|
+
|
2
|
+
# https://bitbucket.org/ged/ruby-axis/raw/ef212387adcbd567a39fa0d51eb6dc6051c416bf/lib/axis/monkeypatches.rb
|
3
|
+
|
4
|
+
# Backport of Ruby 1.9.2 URI methods to 1.8.7.
|
5
|
+
module URIFormEncoding
|
6
|
+
|
7
|
+
TBLENCWWWCOMP_ = {} # :nodoc:
|
8
|
+
TBLDECWWWCOMP_ = {} # :nodoc:
|
9
|
+
|
10
|
+
|
11
|
+
# Encode given +str+ to URL-encoded form data.
|
12
|
+
#
|
13
|
+
# This doesn't convert *, -, ., 0-9, A-Z, _, a-z,
|
14
|
+
# does convert SP to +, and convert others to %XX.
|
15
|
+
#
|
16
|
+
# This refers http://www.w3.org/TR/html5/forms.html#url-encoded-form-data
|
17
|
+
#
|
18
|
+
# See URI.decode_www_form_component, URI.encode_www_form
|
19
|
+
def encode_www_form_component( str )
|
20
|
+
if TBLENCWWWCOMP_.empty?
|
21
|
+
256.times do |i|
|
22
|
+
TBLENCWWWCOMP_[i.chr] = '%%%02X' % i
|
23
|
+
end
|
24
|
+
TBLENCWWWCOMP_[' '] = '+'
|
25
|
+
TBLENCWWWCOMP_.freeze
|
26
|
+
end
|
27
|
+
return str.to_s.gsub(/[^*\-.0-9A-Z_a-z]/, TBLENCWWWCOMP_)
|
28
|
+
end
|
29
|
+
|
30
|
+
# Decode given +str+ of URL-encoded form data.
|
31
|
+
#
|
32
|
+
# This decodes + to SP.
|
33
|
+
#
|
34
|
+
# See URI.encode_www_form_component, URI.decode_www_form
|
35
|
+
def decode_www_form_component( str )
|
36
|
+
if TBLDECWWWCOMP_.empty?
|
37
|
+
256.times do |i|
|
38
|
+
h, l = i>>4, i&15
|
39
|
+
TBLDECWWWCOMP_['%%%X%X' % [h, l]] = i.chr
|
40
|
+
TBLDECWWWCOMP_['%%%x%X' % [h, l]] = i.chr
|
41
|
+
TBLDECWWWCOMP_['%%%X%x' % [h, l]] = i.chr
|
42
|
+
TBLDECWWWCOMP_['%%%x%x' % [h, l]] = i.chr
|
43
|
+
end
|
44
|
+
TBLDECWWWCOMP_['+'] = ' '
|
45
|
+
TBLDECWWWCOMP_.freeze
|
46
|
+
end
|
47
|
+
raise ArgumentError, "invalid %-encoding (#{str})" unless /\A(?:%\h\h|[^%]+)*\z/ =~ str
|
48
|
+
return str.gsub( /\+|%\h\h/, TBLDECWWWCOMP_ )
|
49
|
+
end
|
50
|
+
|
51
|
+
# Generate URL-encoded form data from given +enum+.
|
52
|
+
#
|
53
|
+
# This generates application/x-www-form-urlencoded data defined in HTML5
|
54
|
+
# from given an Enumerable object.
|
55
|
+
#
|
56
|
+
# This internally uses URI.encode_www_form_component(str).
|
57
|
+
#
|
58
|
+
# This doesn't convert encodings of give items, so convert them before call
|
59
|
+
# this method if you want to send data as other than original encoding or
|
60
|
+
# mixed encoding data. (strings which is encoded in HTML5 ASCII incompatible
|
61
|
+
# encoding is converted to UTF-8)
|
62
|
+
#
|
63
|
+
# This doesn't treat files. When you send a file, use multipart/form-data.
|
64
|
+
#
|
65
|
+
# This refers http://www.w3.org/TR/html5/forms.html#url-encoded-form-data
|
66
|
+
#
|
67
|
+
# See URI.encode_www_form_component, URI.decode_www_form
|
68
|
+
def encode_www_form( enum )
|
69
|
+
str = nil
|
70
|
+
enum.each do |k,v|
|
71
|
+
if str
|
72
|
+
str << '&'
|
73
|
+
else
|
74
|
+
str = nil.to_s
|
75
|
+
end
|
76
|
+
str << encode_www_form_component(k)
|
77
|
+
str << '='
|
78
|
+
str << encode_www_form_component(v)
|
79
|
+
end
|
80
|
+
str
|
81
|
+
end
|
82
|
+
|
83
|
+
WFKV_ = '(?:%\h\h|[^%#=;&])' # :nodoc:
|
84
|
+
|
85
|
+
# Decode URL-encoded form data from given +str+.
|
86
|
+
#
|
87
|
+
# This decodes application/x-www-form-urlencoded data
|
88
|
+
# and returns array of key-value array.
|
89
|
+
# This internally uses URI.decode_www_form_component.
|
90
|
+
#
|
91
|
+
# This refers http://www.w3.org/TR/html5/forms.html#url-encoded-form-data
|
92
|
+
#
|
93
|
+
# ary = URI.decode_www_form("a=1&a=2&b=3")
|
94
|
+
# p ary #=> [['a', '1'], ['a', '2'], ['b', '3']]
|
95
|
+
# p ary.assoc('a').last #=> '1'
|
96
|
+
# p ary.assoc('b').last #=> '3'
|
97
|
+
# p ary.rassoc('a').last #=> '2'
|
98
|
+
# p Hash[ary] # => {"a"=>"2", "b"=>"3"}
|
99
|
+
#
|
100
|
+
# See URI.decode_www_form_component, URI.encode_www_form
|
101
|
+
def decode_www_form( str )
|
102
|
+
return [] if str.empty?
|
103
|
+
unless /\A#{WFKV_}*=#{WFKV_}*(?:[;&]#{WFKV_}*=#{WFKV_}*)*\z/o =~ str
|
104
|
+
raise ArgumentError, "invalid data of application/x-www-form-urlencoded (#{str})"
|
105
|
+
end
|
106
|
+
ary = []
|
107
|
+
$&.scan(/([^=;&]+)=([^;&]*)/) do
|
108
|
+
ary << [decode_www_form_component($1, enc), decode_www_form_component($2, enc)]
|
109
|
+
end
|
110
|
+
ary
|
111
|
+
end
|
112
|
+
|
113
|
+
end
|
114
|
+
|
115
|
+
|
116
|
+
unless URI.methods.include?( :encode_www_form )
|
117
|
+
URI.extend( URIFormEncoding )
|
118
|
+
end
|
119
|
+
|
120
|
+
|
metadata
ADDED
@@ -0,0 +1,68 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: fluidfeatures
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.3.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Phil Whelan
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-10-31 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: persistent_http
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ~>
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: 1.0.3
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ~>
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: 1.0.3
|
30
|
+
description: Ruby client for the FluidFeatures service.
|
31
|
+
email:
|
32
|
+
- phil@fluidfeatures.com
|
33
|
+
executables: []
|
34
|
+
extensions: []
|
35
|
+
extra_rdoc_files: []
|
36
|
+
files:
|
37
|
+
- .gitignore
|
38
|
+
- Gemfile
|
39
|
+
- README.md
|
40
|
+
- fluidfeatures.gemspec
|
41
|
+
- lib/fluidfeatures/client.rb
|
42
|
+
- lib/fluidfeatures/version.rb
|
43
|
+
- lib/pre_ruby192/uri.rb
|
44
|
+
homepage: https://github.com/FluidFeatures/fluidfeatures-ruby
|
45
|
+
licenses: []
|
46
|
+
post_install_message:
|
47
|
+
rdoc_options: []
|
48
|
+
require_paths:
|
49
|
+
- lib
|
50
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
51
|
+
none: false
|
52
|
+
requirements:
|
53
|
+
- - ! '>='
|
54
|
+
- !ruby/object:Gem::Version
|
55
|
+
version: '0'
|
56
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ! '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
requirements: []
|
63
|
+
rubyforge_project: fluidfeatures
|
64
|
+
rubygems_version: 1.8.24
|
65
|
+
signing_key:
|
66
|
+
specification_version: 3
|
67
|
+
summary: Ruby client for the FluidFeatures service.
|
68
|
+
test_files: []
|