featureflow 0.5.1 → 0.6.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.
- checksums.yaml +5 -5
- data/.gitignore +1 -0
- data/CHANGELOG.md +4 -0
- data/Gemfile.lock +3 -3
- data/README.md +33 -22
- data/featureflow.gemspec +2 -2
- data/lib/featureflow.rb +27 -5
- data/lib/featureflow/client.rb +43 -50
- data/lib/featureflow/configuration.rb +36 -0
- data/lib/featureflow/evaluate.rb +27 -19
- data/lib/featureflow/evaluate_helpers.rb +4 -4
- data/lib/featureflow/events_client.rb +86 -11
- data/lib/featureflow/polling_client.rb +25 -6
- data/lib/featureflow/rails/rails_client.rb +2 -3
- data/lib/featureflow/rails/railtie.rb +6 -36
- data/lib/featureflow/{context_builder.rb → user_builder.rb} +11 -11
- data/lib/featureflow/version.rb +1 -1
- data/publish-gem.sh +1 -0
- data/test.rb +22 -10
- metadata +8 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 6e44302ab7573a1fe36b0fac9deaac9958ab96d01b73a865e3c98d00bd71ebb3
|
4
|
+
data.tar.gz: 8b641fa3bd07be005e7cf6f722a441da2544d2fa1141b40c35e1d3abb4df6db6
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: dd22e33787e281e6a61ca62464f771c3e4d1778d6893f6689ff189c57dbdf3c433e7b49b6a881f73e113f5b5d21216c7b3a6dd67e313c8aeaf4e317ed4c858b9
|
7
|
+
data.tar.gz: 679d1c6f0efbefca2497da6467109c22f93bdde4a6bc165dd5e6b0d03b35daaa02888c5aeec0296dec6f962fd0aeea5a257b701e26798b15f2d394fe324aa544
|
data/.gitignore
CHANGED
data/CHANGELOG.md
CHANGED
data/Gemfile.lock
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
featureflow (0.
|
4
|
+
featureflow (0.6.0)
|
5
5
|
excon (~> 0.57.0)
|
6
6
|
|
7
7
|
GEM
|
@@ -20,7 +20,7 @@ GEM
|
|
20
20
|
gherkin (~> 4.0)
|
21
21
|
cucumber-wire (0.0.1)
|
22
22
|
diff-lcs (1.3)
|
23
|
-
excon (0.57.
|
23
|
+
excon (0.57.1)
|
24
24
|
gherkin (4.1.3)
|
25
25
|
multi_json (1.12.1)
|
26
26
|
multi_test (0.1.2)
|
@@ -50,4 +50,4 @@ DEPENDENCIES
|
|
50
50
|
rspec (~> 3.0)
|
51
51
|
|
52
52
|
BUNDLED WITH
|
53
|
-
1.15.
|
53
|
+
1.15.3
|
data/README.md
CHANGED
@@ -15,7 +15,7 @@ The easiest way to get started is to follow the [Featureflow quick start guides]
|
|
15
15
|
|
16
16
|
## Change Log
|
17
17
|
|
18
|
-
Please see [CHANGELOG](https://github.com/featureflow/featureflow-
|
18
|
+
Please see [CHANGELOG](https://github.com/featureflow/featureflow-ruby-sdk/blob/master/CHANGELOG.md).
|
19
19
|
|
20
20
|
## Usage
|
21
21
|
|
@@ -30,7 +30,7 @@ gem 'featureflow'
|
|
30
30
|
```
|
31
31
|
|
32
32
|
Requiring `featureflow` in your ruby application will expose the classes
|
33
|
-
`Featureflow::Client`, `Featuerflow::
|
33
|
+
`Featureflow::Client`, `Featuerflow::UserBuilder` and `Featureflow::Feature`.
|
34
34
|
|
35
35
|
The usage of each class is documented below.
|
36
36
|
|
@@ -79,45 +79,45 @@ end
|
|
79
79
|
|
80
80
|
|
81
81
|
|
82
|
-
#### Defining
|
82
|
+
#### Defining a User
|
83
83
|
|
84
|
-
Before evaluating a feature you must define a
|
85
|
-
Featureflow uses
|
86
|
-
A featureflow
|
87
|
-
Featureflow requires the
|
84
|
+
Before evaluating a feature you must define a user.
|
85
|
+
Featureflow uses users to target different user groups to specific feature variants.
|
86
|
+
A featureflow user has an `id`, which should uniquely identify the current user, and optionally additional `attributes`.
|
87
|
+
Featureflow requires the user `id` to be unique per user for gradual rollout of features.
|
88
88
|
|
89
|
-
There are two ways to define
|
89
|
+
There are two ways to define a user:
|
90
90
|
```ruby
|
91
91
|
require 'featureflow'
|
92
|
-
|
92
|
+
user_id = '<unique_user_identifier>'
|
93
93
|
|
94
|
-
# option 1, use the
|
95
|
-
|
96
|
-
.
|
94
|
+
# option 1, use the user builder
|
95
|
+
user = Featureflow::UserBuilder.new(user_id)
|
96
|
+
.with_attributes(country: 'US',
|
97
97
|
roles: %w[USER_ADMIN, BETA_CUSTOMER])
|
98
98
|
.build
|
99
99
|
|
100
100
|
# option 2, use just a string
|
101
|
-
|
101
|
+
user = user_id
|
102
102
|
```
|
103
103
|
|
104
104
|
#### Evaluating Features
|
105
105
|
|
106
106
|
In your code, you can test the value of your feature using something similar to below
|
107
|
-
For these examples below, assume the feature `my-feature-key` is equal to `'on'` for the current `
|
107
|
+
For these examples below, assume the feature `my-feature-key` is equal to `'on'` for the current `user`
|
108
108
|
```ruby
|
109
|
-
if featureflow.evaluate('my-feature-key',
|
110
|
-
# this code will be run because 'my-feature-key' is set to 'on' for the given
|
109
|
+
if featureflow.evaluate('my-feature-key', user).is? 'on'
|
110
|
+
# this code will be run because 'my-feature-key' is set to 'on' for the given user
|
111
111
|
end
|
112
112
|
```
|
113
113
|
Because the most common variants for a feature are `'on'` and `'off'`, we have provided two helper methods `.on?` and `.off?`
|
114
114
|
|
115
115
|
```ruby
|
116
|
-
if featureflow.evaluate('my-feature-key',
|
116
|
+
if featureflow.evaluate('my-feature-key', user).on?
|
117
117
|
# this feature code will be run because 'my-feature-key' is set to 'on'
|
118
118
|
end
|
119
119
|
|
120
|
-
if featureflow.evaluate('my-feature-key',
|
120
|
+
if featureflow.evaluate('my-feature-key', user).off?
|
121
121
|
# this feature code won't be run because 'my-feature-key' is not set to 'off'
|
122
122
|
end
|
123
123
|
```
|
@@ -147,9 +147,9 @@ featureflow = Featureflow::Client.new(api_key: FEATUREFLOW_SERVER_KEY,
|
|
147
147
|
])
|
148
148
|
|
149
149
|
# ... app has been started offline
|
150
|
-
featureflow.evaluate('key-one',
|
151
|
-
featureflow.evaluate('key-two',
|
152
|
-
featureflow.evaluate('key-three',
|
150
|
+
featureflow.evaluate('key-one', user).on? # == true
|
151
|
+
featureflow.evaluate('key-two', user).off? # == true
|
152
|
+
featureflow.evaluate('key-three', user).is? 'custom' # == true
|
153
153
|
|
154
154
|
```
|
155
155
|
|
@@ -170,4 +170,15 @@ Apache-2.0
|
|
170
170
|
[rubygems-img]: https://badge.fury.io/rb/featureflow.png
|
171
171
|
|
172
172
|
[dependency-url]: https://www.featureflow.io
|
173
|
-
[dependency-img]: https://www.featureflow.io/wp-content/uploads/2016/12/featureflow-web.png
|
173
|
+
[dependency-img]: https://www.featureflow.io/wp-content/uploads/2016/12/featureflow-web.png
|
174
|
+
|
175
|
+
#Developer documentation
|
176
|
+
|
177
|
+
To build and test the SDK
|
178
|
+
|
179
|
+
```
|
180
|
+
rvm install 2.5.1
|
181
|
+
rvm use --default 2.5.1
|
182
|
+
bundle install
|
183
|
+
ruby test.rb
|
184
|
+
```
|
data/featureflow.gemspec
CHANGED
@@ -6,8 +6,8 @@ require 'featureflow/version'
|
|
6
6
|
Gem::Specification.new do |spec|
|
7
7
|
spec.name = "featureflow"
|
8
8
|
spec.version = Featureflow::VERSION
|
9
|
-
spec.authors = ["
|
10
|
-
spec.email = ["
|
9
|
+
spec.authors = ["Oliver Oldfield-Hodge"]
|
10
|
+
spec.email = ["contact@featureflow.io"]
|
11
11
|
|
12
12
|
spec.summary = "Ruby SDK for Featureflow"
|
13
13
|
spec.description = "Connects your server with Featureflow. Create features at https://featureflow.io."
|
data/lib/featureflow.rb
CHANGED
@@ -1,16 +1,38 @@
|
|
1
|
+
require 'logger'
|
1
2
|
require 'featureflow/version'
|
3
|
+
require 'featureflow/configuration'
|
2
4
|
require 'featureflow/client'
|
3
|
-
require 'featureflow/
|
5
|
+
require 'featureflow/user_builder'
|
4
6
|
require 'featureflow/feature'
|
5
7
|
|
6
8
|
module Featureflow
|
7
9
|
class << self
|
8
|
-
|
10
|
+
def configure(config_hash = nil)
|
11
|
+
if config_hash
|
12
|
+
config_hash.each do |k, v|
|
13
|
+
configuration.send("#{k}=", v) rescue nil if configuration.respond_to?("#{k}=")
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
yield(configuration) if block_given?
|
18
|
+
end
|
19
|
+
|
20
|
+
def configuration
|
21
|
+
@configuration ||= Featureflow::Configuration.new
|
22
|
+
end
|
9
23
|
|
10
24
|
def logger
|
11
|
-
|
12
|
-
|
13
|
-
|
25
|
+
configuration.logger
|
26
|
+
end
|
27
|
+
|
28
|
+
def client
|
29
|
+
@client ||= Featureflow::Client.new(configuration)
|
30
|
+
end
|
31
|
+
|
32
|
+
alias featureflow client
|
33
|
+
|
34
|
+
def evaluate(*args)
|
35
|
+
client.evaluate(*args)
|
14
36
|
end
|
15
37
|
end
|
16
38
|
end
|
data/lib/featureflow/client.rb
CHANGED
@@ -1,83 +1,76 @@
|
|
1
|
-
require 'logger'
|
2
|
-
require 'time'
|
3
|
-
require 'pp'
|
4
1
|
require 'featureflow/evaluate'
|
5
2
|
require 'featureflow/polling_client'
|
6
3
|
require 'featureflow/events_client'
|
7
4
|
|
8
5
|
module Featureflow
|
9
6
|
class Client
|
10
|
-
def initialize(
|
11
|
-
|
12
|
-
|
13
|
-
Featureflow.logger.info 'initializing client'
|
7
|
+
def initialize(configuration = nil)
|
8
|
+
@configuration = configuration || Featureflow.configuration
|
9
|
+
@configuration.validate!
|
14
10
|
@features = {}
|
15
|
-
@config = {
|
16
|
-
api_key: api_key,
|
17
|
-
url: 'https://app.featureflow.io',
|
18
|
-
path: '/api/sdk/v1/features',
|
19
|
-
with_features: [],
|
20
|
-
disable_events: false
|
21
|
-
}.merge(config)
|
22
11
|
|
23
|
-
|
24
|
-
raise ArgumentError, 'config[:with_features] must be an array of Feature hashes. Use Featureflow::Feature.create(key, failover_variant)'
|
25
|
-
end
|
12
|
+
Featureflow.logger.info 'initializing client'
|
26
13
|
|
27
14
|
@failover_variants = {}
|
28
|
-
@
|
15
|
+
@configuration.with_features.each do |feature|
|
29
16
|
Featureflow.logger.info "Registering feature with key #{feature[:key]}"
|
30
17
|
failover = feature[:failover_variant];
|
31
18
|
@failover_variants[feature[:key]] = failover if failover.is_a?(String) && !failover.empty?
|
32
19
|
end
|
33
20
|
|
34
|
-
unless @
|
35
|
-
@events_client = EventsClient.new @
|
36
|
-
@events_client.register_features @
|
21
|
+
unless @configuration.disable_events
|
22
|
+
@events_client = EventsClient.new @configuration.event_endpoint, @configuration.api_key
|
23
|
+
@events_client.register_features @configuration.with_features
|
37
24
|
end
|
38
25
|
|
39
|
-
|
40
|
-
@config[:api_key],
|
41
|
-
delay: 30,
|
42
|
-
timeout: 30) {|features| update_features(features)}
|
26
|
+
reload
|
43
27
|
|
44
28
|
Featureflow.logger.info 'client initialized'
|
45
29
|
end
|
46
30
|
|
47
|
-
def
|
48
|
-
@
|
31
|
+
def reload
|
32
|
+
@polling_client.finish if @polling_client
|
33
|
+
@polling_client = build_polling_client
|
49
34
|
end
|
50
35
|
|
51
|
-
def
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
context = ContextBuilder.new(context).build if context.is_a?(String)
|
60
|
-
|
61
|
-
context = context.dup
|
62
|
-
context[:values] = context[:values].merge('featureflow.key' => context[:key],
|
63
|
-
'featureflow.date' => Time.now.iso8601)
|
36
|
+
def build_polling_client
|
37
|
+
PollingClient.new(
|
38
|
+
@configuration.endpoint,
|
39
|
+
@configuration.api_key,
|
40
|
+
poll_interval: 10,
|
41
|
+
timeout: 30
|
42
|
+
)
|
43
|
+
end
|
64
44
|
|
65
|
-
|
45
|
+
def feature(key)
|
46
|
+
@polling_client.feature(key)
|
66
47
|
end
|
67
48
|
|
68
|
-
|
49
|
+
def failover_variant(key)
|
69
50
|
@failover_variants[key]
|
70
51
|
end
|
71
52
|
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
53
|
+
def evaluate(key, user)
|
54
|
+
raise ArgumentError, 'key must be a string' unless key.is_a?(String)
|
55
|
+
raise ArgumentError, 'user is required' unless user
|
56
|
+
unless user.is_a?(String) || user.is_a?(Hash)
|
57
|
+
raise ArgumentError, 'user must be either a string user id,' + \
|
58
|
+
' or a Hash built using Featureflow::UserBuilder)'
|
59
|
+
end
|
60
|
+
|
61
|
+
user = UserBuilder.new(user).build if user.is_a?(String)
|
62
|
+
|
63
|
+
user = user.dup
|
64
|
+
user[:attributes] = user[:attributes].merge('featureflow.user.id' => user[:id])
|
76
65
|
|
77
|
-
|
78
|
-
|
79
|
-
feature
|
80
|
-
|
66
|
+
Evaluate.new(
|
67
|
+
feature_key: key,
|
68
|
+
feature: feature(key),
|
69
|
+
failover_variant: failover_variant(key),
|
70
|
+
user: user,
|
71
|
+
salt: '1',
|
72
|
+
events_client: @events_client
|
73
|
+
)
|
81
74
|
end
|
82
75
|
end
|
83
76
|
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
class Featureflow::Configuration
|
2
|
+
attr_accessor :api_key
|
3
|
+
attr_accessor :endpoint
|
4
|
+
attr_accessor :event_endpoint
|
5
|
+
attr_accessor :disable_events
|
6
|
+
attr_accessor :with_features
|
7
|
+
attr_accessor :logger
|
8
|
+
|
9
|
+
DEFAULT_ENDPOINT = 'https://app.featureflow.io'
|
10
|
+
DEFAULT_EVENT_ENDPOINT = 'https://events.featureflow.io'
|
11
|
+
|
12
|
+
def initialize
|
13
|
+
self.api_key = ENV["FEATUREFLOW_SERVER_KEY"]
|
14
|
+
self.endpoint = DEFAULT_ENDPOINT
|
15
|
+
self.event_endpoint = DEFAULT_EVENT_ENDPOINT
|
16
|
+
self.disable_events = false
|
17
|
+
self.with_features = []
|
18
|
+
|
19
|
+
self.logger = Logger.new(STDOUT)
|
20
|
+
self.logger.level = Logger::WARN
|
21
|
+
end
|
22
|
+
|
23
|
+
def validate!
|
24
|
+
unless with_features_valid? @with_features
|
25
|
+
raise ArgumentError, 'with_features must be an array of Feature hashes. Use Featureflow::Feature.create(key, failover_variant)'
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def with_features_valid?(features)
|
32
|
+
features.all? do |feature|
|
33
|
+
feature[:key].is_a?(String) && feature[:failover_variant].is_a?(String) && feature[:variants].is_a?(Array)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
data/lib/featureflow/evaluate.rb
CHANGED
@@ -2,15 +2,22 @@ require 'featureflow/evaluate_helpers'
|
|
2
2
|
|
3
3
|
module Featureflow
|
4
4
|
class Evaluate
|
5
|
-
def initialize(feature_key
|
6
|
-
@evaluated_variant = calculate_variant feature_key, feature, failover_variant, context, salt
|
7
|
-
@events_client = events_client
|
8
|
-
@context = context
|
5
|
+
def initialize(feature_key:, feature:, failover_variant:, user:, salt:, events_client: nil)
|
9
6
|
@key = feature_key
|
7
|
+
@feature = feature
|
8
|
+
@failover_variant = failover_variant
|
9
|
+
@user = user
|
10
|
+
@salt = salt
|
11
|
+
@events_client = events_client
|
12
|
+
|
13
|
+
@has_failover = @failover_variant.is_a?(String)
|
14
|
+
@failover_variant = 'off' unless @has_failover
|
15
|
+
|
16
|
+
@evaluated_variant = evaluate_variant
|
10
17
|
end
|
11
18
|
|
12
19
|
def is?(value)
|
13
|
-
@events_client.evaluate
|
20
|
+
@events_client.evaluate(@key, @evaluated_variant, value, @user) if @events_client
|
14
21
|
@evaluated_variant == value
|
15
22
|
end
|
16
23
|
|
@@ -28,22 +35,23 @@ module Featureflow
|
|
28
35
|
@evaluated_variant
|
29
36
|
end
|
30
37
|
|
31
|
-
private
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
Featureflow.logger.info "Evaluating nil feature '#{feature_key}' using the "\
|
36
|
-
"#{has_failover ? 'provided' : 'default'} failover '#{failover_variant}'"
|
37
|
-
return failover_variant
|
38
|
+
private
|
39
|
+
|
40
|
+
def evaluate_variant
|
41
|
+
unless @feature
|
42
|
+
Featureflow.logger.info "Evaluating nil feature '#{@feature_key}' using the "\
|
43
|
+
"#{@has_failover ? 'provided' : 'default'} failover '#{@failover_variant}'"
|
44
|
+
return @failover_variant
|
38
45
|
end
|
39
46
|
|
40
|
-
return feature['offVariantKey'] unless feature['enabled']
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
47
|
+
return @feature['offVariantKey'] unless @feature['enabled']
|
48
|
+
|
49
|
+
@feature['rules'].each do |rule|
|
50
|
+
next unless EvaluateHelpers.rule_matches(rule, @user)
|
51
|
+
hash = EvaluateHelpers.calculate_hash(@salt, @feature['key'], @user['id'])
|
52
|
+
variant_value = EvaluateHelpers.get_variant_value(hash)
|
53
|
+
return EvaluateHelpers.get_variant_split_key(rule['variantSplits'], variant_value)
|
46
54
|
end
|
47
55
|
end
|
48
56
|
end
|
49
|
-
end
|
57
|
+
end
|
@@ -3,15 +3,15 @@ require 'digest/sha1'
|
|
3
3
|
|
4
4
|
module Featureflow
|
5
5
|
class EvaluateHelpers
|
6
|
-
def self.rule_matches(rule,
|
6
|
+
def self.rule_matches(rule, user)
|
7
7
|
if rule['defaultRule']
|
8
8
|
true # the default rule will always match true
|
9
9
|
else
|
10
10
|
rule['audience']['conditions'].all? do |condition|
|
11
|
-
|
11
|
+
user_attributes = user[:attributes][condition['target']]
|
12
12
|
# convert to array to work with test
|
13
|
-
Array(
|
14
|
-
Conditions.test condition['operator'],
|
13
|
+
Array(user_attributes).any? do |attribute|
|
14
|
+
Conditions.test condition['operator'], attribute, condition['values']
|
15
15
|
end
|
16
16
|
end
|
17
17
|
end
|
@@ -1,13 +1,31 @@
|
|
1
1
|
require 'excon'
|
2
2
|
require 'json'
|
3
|
+
require 'thread'
|
4
|
+
require 'time'
|
3
5
|
|
4
6
|
module Featureflow
|
7
|
+
#LOCK = Mutex.new
|
8
|
+
|
5
9
|
class EventsClient
|
6
10
|
def initialize(url, api_key)
|
7
11
|
@url = url
|
8
12
|
@api_key = api_key
|
13
|
+
@eventsQueue = Queue.new
|
14
|
+
@scheduler = start_scheduler
|
15
|
+
end
|
16
|
+
|
17
|
+
def start_scheduler()
|
18
|
+
Thread.new do
|
19
|
+
loop do
|
20
|
+
begin
|
21
|
+
sleep 10
|
22
|
+
send_queue
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
9
26
|
end
|
10
27
|
|
28
|
+
#register features are not queued and go straight out
|
11
29
|
def register_features(with_features)
|
12
30
|
Thread.new do
|
13
31
|
features = []
|
@@ -16,28 +34,63 @@ module Featureflow
|
|
16
34
|
variants: feature[:variants],
|
17
35
|
failoverVariant: feature[:failover_variant])
|
18
36
|
end
|
19
|
-
send_event 'Register Features', :put, 'api/sdk/v1/register', features
|
37
|
+
send_event 'Register Features', :put, '/api/sdk/v1/register', features
|
20
38
|
end
|
21
39
|
end
|
22
40
|
|
23
|
-
def evaluate(key, evaluated_variant, expected_variant,
|
41
|
+
def evaluate(key, evaluated_variant, expected_variant, user)
|
24
42
|
Thread.new do
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
43
|
+
timestamp = Time.now.iso8601
|
44
|
+
queue_event ({
|
45
|
+
featureKey: key,
|
46
|
+
evaluatedVariant: evaluated_variant,
|
47
|
+
expectedVaraint: expected_variant,
|
48
|
+
user: user,
|
49
|
+
timestamp: timestamp
|
50
|
+
})
|
31
51
|
end
|
32
52
|
end
|
33
53
|
|
34
|
-
|
54
|
+
def queue_event(event)
|
55
|
+
#add to queue
|
56
|
+
|
57
|
+
@eventsQueue.push(event)
|
58
|
+
|
59
|
+
if !@scheduler.alive?
|
60
|
+
@scheduler = start_scheduler
|
61
|
+
end
|
62
|
+
|
63
|
+
if @eventsQueue.length >= 10000
|
64
|
+
send_queue
|
65
|
+
end
|
66
|
+
#id queue = 10000 then send_queue
|
67
|
+
end
|
68
|
+
|
69
|
+
def send_queue()
|
70
|
+
events =[]
|
71
|
+
begin
|
72
|
+
loop do
|
73
|
+
events << @eventsQueue.pop(true)
|
74
|
+
end
|
75
|
+
rescue ThreadError
|
76
|
+
end
|
77
|
+
|
78
|
+
if !events.empty?
|
79
|
+
send_event 'Evaluate Variant', :post, '/api/sdk/v1/events', events
|
80
|
+
end
|
81
|
+
|
82
|
+
end
|
83
|
+
|
84
|
+
=begin
|
85
|
+
private def send_queue()
|
86
|
+
|
35
87
|
connection = Excon.new(@url)
|
36
88
|
response = connection.request(method: method,
|
37
89
|
path: path,
|
38
90
|
headers: {
|
39
91
|
'Authorization' => "Bearer #{@api_key}",
|
40
|
-
'Content-Type' => 'application/json;charset=UTF-8'
|
92
|
+
'Content-Type' => 'application/json;charset=UTF-8',
|
93
|
+
'X-Featureflow-Client' => 'RubyClient/' + Featureflow::VERSION
|
41
94
|
},
|
42
95
|
omit_default_port: true,
|
43
96
|
body: JSON.generate(body))
|
@@ -48,5 +101,27 @@ module Featureflow
|
|
48
101
|
rescue => e
|
49
102
|
Featureflow.logger.error e.inspect
|
50
103
|
end
|
104
|
+
=end
|
105
|
+
|
106
|
+
private def send_event(event_name, method, path, body)
|
107
|
+
connection = Excon.new(@url)
|
108
|
+
response = connection.request(method: method,
|
109
|
+
path: path,
|
110
|
+
headers: {
|
111
|
+
'Authorization' => "Bearer #{@api_key}",
|
112
|
+
'Content-Type' => 'application/json;charset=UTF-8',
|
113
|
+
'Accept' => 'Application/Json',
|
114
|
+
'X-Featureflow-Client' => 'RubyClient/' + Featureflow::VERSION
|
115
|
+
},
|
116
|
+
omit_default_port: true,
|
117
|
+
body: JSON.generate(body))
|
118
|
+
if response.status >= 400
|
119
|
+
Featureflow.logger.error "unable to send event #{event_name} to #{@url+path}. Failed with response status #{response.status}"
|
120
|
+
Featureflow.logger.error response.to_s
|
121
|
+
end
|
122
|
+
rescue => e
|
123
|
+
Featureflow.logger.error e.inspect
|
124
|
+
end
|
125
|
+
|
51
126
|
end
|
52
|
-
end
|
127
|
+
end
|
@@ -7,15 +7,18 @@ module Featureflow
|
|
7
7
|
poll_interval: 30,
|
8
8
|
timeout: 30
|
9
9
|
}.freeze
|
10
|
-
|
10
|
+
LOCK = Mutex.new
|
11
|
+
|
12
|
+
def initialize(url, api_key, options = {})
|
11
13
|
@etag = ''
|
12
14
|
@url = url
|
13
15
|
@api_key = api_key
|
14
16
|
@options = DEFAULT_OPTIONS.merge(options)
|
15
|
-
@
|
17
|
+
@features = {}
|
16
18
|
|
17
19
|
load_features
|
18
|
-
|
20
|
+
|
21
|
+
@thread = Thread.new do
|
19
22
|
loop do
|
20
23
|
sleep @options[:poll_interval]
|
21
24
|
load_features
|
@@ -23,14 +26,30 @@ module Featureflow
|
|
23
26
|
end
|
24
27
|
end
|
25
28
|
|
29
|
+
def finish
|
30
|
+
@thread.exit
|
31
|
+
end
|
32
|
+
|
33
|
+
def feature(key)
|
34
|
+
LOCK.synchronize { @features[key] }
|
35
|
+
end
|
36
|
+
|
26
37
|
def load_features
|
27
|
-
response = Excon.get(@url, headers: {
|
38
|
+
response = Excon.get(@url + + '/api/sdk/v1/features', headers: {
|
28
39
|
'Authorization' => "Bearer #{@api_key}",
|
29
|
-
'
|
40
|
+
'Accept' => 'Application/Json',
|
41
|
+
'If-None-Match' => @etag,
|
42
|
+
'X-Featureflow-Client' => 'RubyClient/' + Featureflow::VERSION
|
30
43
|
}, omit_default_port: true, read_timeout: @options[:timeout])
|
44
|
+
|
31
45
|
if response.status == 200
|
46
|
+
Featureflow.logger.debug "updating features"
|
47
|
+
|
32
48
|
@etag = response.headers['ETag']
|
33
|
-
|
49
|
+
|
50
|
+
features = JSON.parse(response.body)
|
51
|
+
|
52
|
+
LOCK.synchronize { @features = features }
|
34
53
|
elsif response.status >= 400
|
35
54
|
Featureflow.logger.error "request for features failed with response status #{response.status}"
|
36
55
|
Featureflow.logger.error response.to_s
|
@@ -5,15 +5,14 @@ module Featureflow
|
|
5
5
|
def initialize(request)
|
6
6
|
@request = request
|
7
7
|
end
|
8
|
+
|
8
9
|
def evaluate(key, context)
|
9
10
|
request_context_values = {
|
10
11
|
'featureflow.ip' => @request.remote_ip,
|
11
12
|
'featureflow.url' => @request.original_url
|
12
13
|
}
|
13
14
|
context = {key: context, values: {}} if context.is_a?(String)
|
14
|
-
Featureflow.
|
15
|
-
key: context[:key],
|
16
|
-
values: request_context_values.merge(context[:values]))
|
15
|
+
Featureflow.evaluate(key, key: context[:key], values: request_context_values.merge(context[:values]))
|
17
16
|
end
|
18
17
|
end
|
19
18
|
end
|
@@ -1,42 +1,12 @@
|
|
1
1
|
require 'featureflow/rails/rails_client'
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
configuration.merge!(config_hash)
|
9
|
-
end
|
10
|
-
|
11
|
-
yield(configuration) if block_given?
|
12
|
-
yield(featureflow) if block_given?
|
13
|
-
end
|
14
|
-
|
15
|
-
# Configuration getters
|
16
|
-
def configuration
|
17
|
-
@configuration = nil unless defined?(@configuration)
|
18
|
-
@configuration || LOCK.synchronize { @configuration ||= {} }
|
19
|
-
end
|
20
|
-
|
21
|
-
# Configuration getters
|
22
|
-
def featureflow
|
23
|
-
@featureflow = nil unless defined?(@featureflow)
|
24
|
-
@featureflow || LOCK.synchronize { @featureflow ||= Featureflow::Client.new(configuration) }
|
25
|
-
end
|
26
|
-
end
|
27
|
-
|
28
|
-
class FeatureflowRailtie < Rails::Railtie
|
29
|
-
|
30
|
-
config.before_initialize do
|
31
|
-
ActiveSupport.on_load(:action_controller) do
|
32
|
-
# @client =
|
33
|
-
ActionController::Base.class_eval do
|
34
|
-
def featureflow
|
35
|
-
Featureflow::RailsClient.new(request)
|
36
|
-
end
|
2
|
+
class Featureflow::FeatureflowRailtie < Rails::Railtie
|
3
|
+
config.before_initialize do
|
4
|
+
ActiveSupport.on_load(:action_controller) do
|
5
|
+
ActionController::Base.class_eval do
|
6
|
+
def featureflow
|
7
|
+
@featureflow ||= Featureflow::RailsClient.new(request)
|
37
8
|
end
|
38
9
|
end
|
39
10
|
end
|
40
|
-
|
41
11
|
end
|
42
12
|
end
|
@@ -1,32 +1,32 @@
|
|
1
1
|
module Featureflow
|
2
|
-
class
|
3
|
-
def initialize(
|
4
|
-
raise ArgumentError, 'Parameter
|
5
|
-
@
|
6
|
-
@
|
2
|
+
class UserBuilder
|
3
|
+
def initialize(id)
|
4
|
+
raise ArgumentError, 'Parameter id must be a String' unless id.is_a?(String) && !id.empty?
|
5
|
+
@user_id = id
|
6
|
+
@attributes = {}
|
7
7
|
self
|
8
8
|
end
|
9
9
|
|
10
|
-
def
|
10
|
+
def with_attributes(hash)
|
11
11
|
raise ArgumentError, 'Parameter hash must be a Hash' unless hash.is_a?(Hash)
|
12
12
|
hash = hash.dup
|
13
13
|
hash.each do |k, v|
|
14
14
|
raise ArgumentError, "Value for #{k} must be a valid 'primitive' JSON datatype" unless valid_value?(v)
|
15
15
|
hash[k.to_s] = h.delete(k) unless k.is_a?(String)
|
16
16
|
end
|
17
|
-
@
|
17
|
+
@attributes = @attributes.merge(hash)
|
18
18
|
self
|
19
19
|
end
|
20
20
|
|
21
21
|
def build
|
22
22
|
{
|
23
|
-
|
24
|
-
|
23
|
+
id: @user_id,
|
24
|
+
attributes: @attributes
|
25
25
|
}
|
26
26
|
end
|
27
27
|
|
28
|
-
private def
|
29
|
-
Array(
|
28
|
+
private def valid_attribute?(attributes)
|
29
|
+
Array(attributes).all? do |v|
|
30
30
|
[String, Numeric, TrueClass, FalseClass].any? { |type| v.is_a?(type) }
|
31
31
|
end
|
32
32
|
end
|
data/lib/featureflow/version.rb
CHANGED
data/publish-gem.sh
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
gem push
|
data/test.rb
CHANGED
@@ -2,20 +2,32 @@ $:.unshift(File.dirname(__FILE__) + "/lib/")
|
|
2
2
|
|
3
3
|
require 'featureflow'
|
4
4
|
with_features = [
|
5
|
-
Featureflow::Feature.create('default', 'variant')
|
5
|
+
Featureflow::Feature.create('default', 'variant'),
|
6
|
+
Featureflow::Feature.create('oli-f1', 'off')
|
6
7
|
]
|
7
8
|
|
8
9
|
|
9
10
|
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
11
|
+
api_key = 'srv-env-'
|
12
|
+
config = Featureflow::Configuration.new
|
13
|
+
config.api_key = api_key
|
14
|
+
#config.endpoint = 'http://localhost:8081'
|
15
|
+
#config.event_endpoint = 'http://localhost:8081'
|
16
|
+
config.with_features = with_features
|
17
|
+
|
18
|
+
featureflow_client = Featureflow::Client.new(config)
|
19
|
+
=begin
|
20
|
+
featureflow_client = Featureflow::Client.new(api_key: api_key, with_features: with_features, url: 'http://localhost:8081')
|
21
|
+
=end
|
22
|
+
user = Featureflow::UserBuilder.new('user1').build
|
23
|
+
puts('test-integration is on? ' + featureflow_client.evaluate('test-integration', user).on?.to_s)
|
24
|
+
featureflow_client.evaluate('nooooo', user).on?
|
25
|
+
featureflow_client.evaluate('default', user).on?
|
17
26
|
|
18
27
|
#
|
19
28
|
loop do
|
20
|
-
sleep
|
21
|
-
|
29
|
+
sleep 10
|
30
|
+
puts(featureflow_client.evaluate('oli-f1', user).value)
|
31
|
+
puts(featureflow_client.evaluate('oli-f1', user).is?('extended'))
|
32
|
+
puts(featureflow_client.evaluate('default', user).on?)
|
33
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: featureflow
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.6.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
|
-
-
|
7
|
+
- Oliver Oldfield-Hodge
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2018-12-17 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -82,7 +82,7 @@ dependencies:
|
|
82
82
|
version: 0.57.0
|
83
83
|
description: Connects your server with Featureflow. Create features at https://featureflow.io.
|
84
84
|
email:
|
85
|
-
-
|
85
|
+
- contact@featureflow.io
|
86
86
|
executables: []
|
87
87
|
extensions: []
|
88
88
|
extra_rdoc_files: []
|
@@ -99,7 +99,7 @@ files:
|
|
99
99
|
- lib/featureflow.rb
|
100
100
|
- lib/featureflow/client.rb
|
101
101
|
- lib/featureflow/conditions.rb
|
102
|
-
- lib/featureflow/
|
102
|
+
- lib/featureflow/configuration.rb
|
103
103
|
- lib/featureflow/evaluate.rb
|
104
104
|
- lib/featureflow/evaluate_helpers.rb
|
105
105
|
- lib/featureflow/events_client.rb
|
@@ -107,8 +107,10 @@ files:
|
|
107
107
|
- lib/featureflow/polling_client.rb
|
108
108
|
- lib/featureflow/rails/rails_client.rb
|
109
109
|
- lib/featureflow/rails/railtie.rb
|
110
|
+
- lib/featureflow/user_builder.rb
|
110
111
|
- lib/featureflow/version.rb
|
111
112
|
- lib/generators/featureflow_generator.rb
|
113
|
+
- publish-gem.sh
|
112
114
|
- test.rb
|
113
115
|
homepage: https://github.com/featureflow/featureflow-ruby-sdk
|
114
116
|
licenses:
|
@@ -130,7 +132,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
130
132
|
version: '0'
|
131
133
|
requirements: []
|
132
134
|
rubyforge_project:
|
133
|
-
rubygems_version: 2.
|
135
|
+
rubygems_version: 2.7.8
|
134
136
|
signing_key:
|
135
137
|
specification_version: 4
|
136
138
|
summary: Ruby SDK for Featureflow
|