featureflow 0.5.1 → 0.7.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/CHANGELOG.md +26 -0
- data/README.md +43 -22
- data/lib/featureflow/client.rb +72 -48
- data/lib/featureflow/conditions.rb +81 -10
- 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 +25 -7
- data/lib/featureflow/rails/railtie.rb +6 -36
- data/lib/featureflow/user_builder.rb +34 -0
- data/lib/featureflow/version.rb +1 -1
- data/lib/featureflow.rb +27 -5
- data/lib/generators/featureflow_generator.rb +1 -1
- metadata +45 -34
- data/.gitignore +0 -1
- data/.rubocop.yml +0 -5
- data/Gemfile +0 -4
- data/Gemfile.lock +0 -53
- data/Rakefile +0 -6
- data/featureflow.gemspec +0 -38
- data/lib/featureflow/context_builder.rb +0 -34
- data/test.rb +0 -21
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
|
-
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 2865ebc3fed1dfffb613a9a459ce6e0ac8d6e5cd05d758b4e93944cbc8d8d372
|
|
4
|
+
data.tar.gz: 1b5a7d4b01a2ff98501ca2818e3d00a4d4523525e9e71cd4cda1a847ef009867
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8c1fb252dcd6beac50457f7d8328198b7512d4bad761824d665490dc0a8fa59026aa55901adf0555dbfce12599ecbf1b611695801d150b29a76ee747abb1aadd
|
|
7
|
+
data.tar.gz: 712e342d59b58327b4ec1bf98770946ef255f39dbecc2093dc50fa786252b82f6aed33b06a1faacbd5c7e9f60aa65f750a37e454c4ec6f03f8a8cb9f96bb87c2
|
data/CHANGELOG.md
CHANGED
|
@@ -1,4 +1,30 @@
|
|
|
1
1
|
# Change log
|
|
2
|
+
## [0.7.0] - 2026-08-11
|
|
3
|
+
### Fixed
|
|
4
|
+
- Condition operators fail closed instead of raising into the host application: an
|
|
5
|
+
invalid `matches` regex, a non-string attribute against a string operator, or a
|
|
6
|
+
string/number type mismatch on a comparison operator is now a no-match, never an
|
|
7
|
+
exception.
|
|
8
|
+
- `before`/`after` compare parsed instants rather than strings, so rule values with
|
|
9
|
+
arbitrary UTC offsets compare correctly; date-only values are read as UTC midnight
|
|
10
|
+
on every host, not local midnight.
|
|
11
|
+
- Rollout bucketing hashed every user to the same bucket (`user['id']` vs
|
|
12
|
+
`user[:id]`); percentage splits now distribute correctly.
|
|
13
|
+
- `RailsClient#evaluate` and `UserBuilder#with_attributes` were broken; both work
|
|
14
|
+
again, and the Rails generator accepts current `sdk-srv-env-` keys.
|
|
15
|
+
### Added
|
|
16
|
+
- `Featureflow::Client.new(api_key: '...')` accepts a configuration Hash; unknown
|
|
17
|
+
options raise rather than being silently dropped.
|
|
18
|
+
- Implicit `featureflow.date` / `featureflow.hourofday` attributes are injected at
|
|
19
|
+
evaluation time, matching the other server SDKs.
|
|
20
|
+
### Changed
|
|
21
|
+
- Requires Ruby >= 3.1 (excon ~> 1.5).
|
|
22
|
+
- The packaged gem now contains only `lib/`, README, CHANGELOG and LICENSE.
|
|
23
|
+
|
|
24
|
+
## [0.6.0] - 2018-12-17
|
|
25
|
+
### Fixed
|
|
26
|
+
- Use User over Context matching all other SDKs
|
|
27
|
+
- Use events.featureflow.io endpoint
|
|
2
28
|
## [0.5.1] - 2017-07-17
|
|
3
29
|
### Fixed
|
|
4
30
|
- Feature registration now compatible with Ruby 2.4
|
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,25 @@ 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
|
+
```
|
|
185
|
+
|
|
186
|
+
## Manual test harness (example server)
|
|
187
|
+
|
|
188
|
+
To try the SDK against a real Featureflow environment without writing any code, run the small local harness in [examples/harness](examples/harness):
|
|
189
|
+
|
|
190
|
+
```
|
|
191
|
+
FEATUREFLOW_SERVER_KEY=sdk-srv-env-<your_key> bundle exec ruby examples/harness/server.rb
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
Then open `http://127.0.0.1:3456/` to evaluate features in the browser, or hit `GET /api/evaluate?feature=<key>&userId=<id>` directly. See [examples/harness/README.md](examples/harness/README.md) for all endpoints and environment variables (including pointing at staging via `FEATUREFLOW_BASE_URL`).
|
data/lib/featureflow/client.rb
CHANGED
|
@@ -1,83 +1,107 @@
|
|
|
1
|
-
require 'logger'
|
|
2
1
|
require 'time'
|
|
3
|
-
require 'pp'
|
|
4
2
|
require 'featureflow/evaluate'
|
|
5
3
|
require 'featureflow/polling_client'
|
|
6
4
|
require 'featureflow/events_client'
|
|
7
5
|
|
|
8
6
|
module Featureflow
|
|
9
7
|
class Client
|
|
10
|
-
def initialize(
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
Featureflow.logger.info 'initializing client'
|
|
8
|
+
def initialize(configuration = nil)
|
|
9
|
+
@configuration = build_configuration(configuration)
|
|
10
|
+
@configuration.validate!
|
|
14
11
|
@features = {}
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
url: 'https://app.featureflow.io',
|
|
18
|
-
path: '/api/sdk/v1/features',
|
|
19
|
-
with_features: [],
|
|
20
|
-
disable_events: false
|
|
21
|
-
}.merge(config)
|
|
22
|
-
|
|
23
|
-
unless with_features_valid? @config[:with_features]
|
|
24
|
-
raise ArgumentError, 'config[:with_features] must be an array of Feature hashes. Use Featureflow::Feature.create(key, failover_variant)'
|
|
25
|
-
end
|
|
12
|
+
|
|
13
|
+
Featureflow.logger.info 'initializing client'
|
|
26
14
|
|
|
27
15
|
@failover_variants = {}
|
|
28
|
-
@
|
|
16
|
+
@configuration.with_features.each do |feature|
|
|
29
17
|
Featureflow.logger.info "Registering feature with key #{feature[:key]}"
|
|
30
18
|
failover = feature[:failover_variant];
|
|
31
19
|
@failover_variants[feature[:key]] = failover if failover.is_a?(String) && !failover.empty?
|
|
32
20
|
end
|
|
33
21
|
|
|
34
|
-
unless @
|
|
35
|
-
@events_client = EventsClient.new @
|
|
36
|
-
@events_client.register_features @
|
|
22
|
+
unless @configuration.disable_events
|
|
23
|
+
@events_client = EventsClient.new @configuration.event_endpoint, @configuration.api_key
|
|
24
|
+
@events_client.register_features @configuration.with_features
|
|
37
25
|
end
|
|
38
26
|
|
|
39
|
-
|
|
40
|
-
@config[:api_key],
|
|
41
|
-
delay: 30,
|
|
42
|
-
timeout: 30) {|features| update_features(features)}
|
|
27
|
+
reload
|
|
43
28
|
|
|
44
29
|
Featureflow.logger.info 'client initialized'
|
|
45
30
|
end
|
|
46
31
|
|
|
32
|
+
def reload
|
|
33
|
+
@polling_client.finish if @polling_client
|
|
34
|
+
@polling_client = build_polling_client
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def build_polling_client
|
|
38
|
+
PollingClient.new(
|
|
39
|
+
@configuration.endpoint,
|
|
40
|
+
@configuration.api_key,
|
|
41
|
+
poll_interval: 10,
|
|
42
|
+
timeout: 30
|
|
43
|
+
)
|
|
44
|
+
end
|
|
45
|
+
|
|
47
46
|
def feature(key)
|
|
48
|
-
@
|
|
47
|
+
@polling_client.feature(key)
|
|
49
48
|
end
|
|
50
49
|
|
|
51
|
-
def
|
|
50
|
+
def failover_variant(key)
|
|
51
|
+
@failover_variants[key]
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def evaluate(key, user)
|
|
52
55
|
raise ArgumentError, 'key must be a string' unless key.is_a?(String)
|
|
53
|
-
raise ArgumentError, '
|
|
54
|
-
unless
|
|
55
|
-
raise ArgumentError, '
|
|
56
|
-
' or a Hash built using Featureflow::
|
|
56
|
+
raise ArgumentError, 'user is required' unless user
|
|
57
|
+
unless user.is_a?(String) || user.is_a?(Hash)
|
|
58
|
+
raise ArgumentError, 'user must be either a string user id,' + \
|
|
59
|
+
' or a Hash built using Featureflow::UserBuilder)'
|
|
57
60
|
end
|
|
58
61
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
context = context.dup
|
|
62
|
-
context[:values] = context[:values].merge('featureflow.key' => context[:key],
|
|
63
|
-
'featureflow.date' => Time.now.iso8601)
|
|
62
|
+
user = UserBuilder.new(user).build if user.is_a?(String)
|
|
64
63
|
|
|
65
|
-
|
|
66
|
-
|
|
64
|
+
user = user.dup
|
|
65
|
+
# Implicit attributes injected at evaluation time (matching the Node SDK's
|
|
66
|
+
# EvaluateHelpers): featureflow.date/hourofday let rules target the current
|
|
67
|
+
# moment; sdk-server's partial evaluation defers these to the JS client, but
|
|
68
|
+
# server SDKs evaluate them locally against "now".
|
|
69
|
+
now = Time.now
|
|
70
|
+
user[:attributes] = user[:attributes].merge(
|
|
71
|
+
'featureflow.user.id' => user[:id],
|
|
72
|
+
'featureflow.date' => now.utc.iso8601,
|
|
73
|
+
'featureflow.hourofday' => now.hour
|
|
74
|
+
)
|
|
67
75
|
|
|
68
|
-
|
|
69
|
-
|
|
76
|
+
Evaluate.new(
|
|
77
|
+
feature_key: key,
|
|
78
|
+
feature: feature(key),
|
|
79
|
+
failover_variant: failover_variant(key),
|
|
80
|
+
user: user,
|
|
81
|
+
salt: '1',
|
|
82
|
+
events_client: @events_client
|
|
83
|
+
)
|
|
70
84
|
end
|
|
71
85
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
86
|
+
# Accepts either a Featureflow::Configuration, or a Hash of configuration
|
|
87
|
+
# options — `Featureflow::Client.new(api_key: '...')` is the form the README
|
|
88
|
+
# leads with and the natural Ruby idiom. Passing nothing keeps using the
|
|
89
|
+
# process-wide Featureflow.configuration (which reads FEATUREFLOW_SERVER_KEY).
|
|
90
|
+
private def build_configuration(configuration)
|
|
91
|
+
return Featureflow.configuration if configuration.nil?
|
|
92
|
+
return configuration unless configuration.is_a?(Hash)
|
|
76
93
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
94
|
+
config = Configuration.new
|
|
95
|
+
configuration.each do |option, value|
|
|
96
|
+
setter = "#{option}="
|
|
97
|
+
# Fail loudly rather than silently dropping a mistyped option — a
|
|
98
|
+
# misspelt `endpoint:` would otherwise quietly poll production.
|
|
99
|
+
unless config.respond_to?(setter)
|
|
100
|
+
raise ArgumentError, "Unknown Featureflow configuration option: #{option}"
|
|
101
|
+
end
|
|
102
|
+
config.public_send(setter, value)
|
|
103
|
+
end
|
|
104
|
+
config
|
|
81
105
|
end
|
|
82
106
|
end
|
|
83
107
|
end
|
|
@@ -1,33 +1,104 @@
|
|
|
1
|
+
require 'date'
|
|
2
|
+
require 'time'
|
|
3
|
+
|
|
1
4
|
module Featureflow
|
|
2
5
|
class Conditions
|
|
6
|
+
# Mirrors the Node SDK's dateParse: dates compare by instant, not by string,
|
|
7
|
+
# so ISO values with different UTC offsets (e.g. "+04:00" vs "Z") compare correctly.
|
|
8
|
+
#
|
|
9
|
+
# A value carrying no timezone information is read as UTC, per the SDK contract
|
|
10
|
+
# ("A date-only value denotes UTC midnight"): the dashboard's date picker emits
|
|
11
|
+
# date-only values like "2026-07-03", and reading those as *local* midnight would
|
|
12
|
+
# make a scheduled rollout fire at a different instant on every host in a fleet.
|
|
13
|
+
# Values with an explicit offset or "Z" keep their instant, unshifted.
|
|
14
|
+
def self.to_epoch(value)
|
|
15
|
+
case value
|
|
16
|
+
when Time then value.to_f
|
|
17
|
+
when Numeric then value.to_f
|
|
18
|
+
when String
|
|
19
|
+
begin
|
|
20
|
+
parsed = Time.parse(value)
|
|
21
|
+
# Time.parse silently falls back to the process's timezone when the value
|
|
22
|
+
# carries no zone; adding back the offset it applied re-reads the same
|
|
23
|
+
# wall-clock components as UTC. Date._parse only reports :offset when the
|
|
24
|
+
# value actually stated a zone.
|
|
25
|
+
parsed += parsed.utc_offset unless Date._parse(value).key?(:offset)
|
|
26
|
+
parsed.to_f
|
|
27
|
+
rescue ArgumentError, TypeError, RangeError
|
|
28
|
+
nil
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Both operands must be strings for a string operator to mean anything.
|
|
34
|
+
def self.both_strings?(a, b)
|
|
35
|
+
a.is_a?(String) && b.is_a?(String)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Numeric-to-numeric or string-to-string only. Mixing the two raises in Ruby, and
|
|
39
|
+
# coercing would answer version comparisons wrongly.
|
|
40
|
+
def self.comparable?(a, b)
|
|
41
|
+
(a.is_a?(Numeric) && b.is_a?(Numeric)) || (a.is_a?(String) && b.is_a?(String))
|
|
42
|
+
end
|
|
43
|
+
|
|
3
44
|
def self.test(op, a, b)
|
|
4
45
|
case op
|
|
5
46
|
when 'equals'
|
|
6
47
|
a.eql? b[0]
|
|
48
|
+
# The string operators assume both sides are strings. A numeric attribute against
|
|
49
|
+
# `contains` raised NoMethodError straight into the host application — the same
|
|
50
|
+
# fail-closed violation as the invalid regex below, and reachable the same way,
|
|
51
|
+
# from a rule somebody typed into the dashboard. Python already guards these with
|
|
52
|
+
# an explicit type check; this brings Ruby into line. A type mismatch is a
|
|
53
|
+
# no-match, not an exception.
|
|
7
54
|
when 'contains'
|
|
8
|
-
a.include?
|
|
55
|
+
both_strings?(a, b[0]) && a.include?(b[0])
|
|
9
56
|
when 'startsWith'
|
|
10
|
-
a.start_with?
|
|
57
|
+
both_strings?(a, b[0]) && a.start_with?(b[0])
|
|
11
58
|
when 'endsWith'
|
|
12
|
-
a.end_with?
|
|
59
|
+
both_strings?(a, b[0]) && a.end_with?(b[0])
|
|
13
60
|
when 'matches'
|
|
14
|
-
a
|
|
61
|
+
# The pattern is untrusted configuration, not code: it comes from a targeting
|
|
62
|
+
# rule somebody typed into the dashboard, so a malformed one like "[unclosed"
|
|
63
|
+
# is a user's mistake rather than a bug in the SDK. The contract
|
|
64
|
+
# (featureflow-client-sdk-testbed/CONTRACT.md, "Operators") therefore requires
|
|
65
|
+
# an invalid regex to return false — a flag SDK must degrade rather than take
|
|
66
|
+
# the host application down with it.
|
|
67
|
+
#
|
|
68
|
+
# Only the regex failure is rescued, never everything: RegexpError for a
|
|
69
|
+
# pattern that will not compile, TypeError for a value that is not a string at
|
|
70
|
+
# all. A genuine bug anywhere else still surfaces.
|
|
71
|
+
begin
|
|
72
|
+
a.match? Regexp.new(b[0])
|
|
73
|
+
rescue RegexpError, TypeError
|
|
74
|
+
false
|
|
75
|
+
end
|
|
15
76
|
when 'in'
|
|
16
77
|
b.include? a
|
|
17
78
|
when 'notIn'
|
|
18
79
|
!b.include? a
|
|
80
|
+
# Comparing a String with a Numeric raises ArgumentError in Ruby, so an attribute
|
|
81
|
+
# whose type does not match the rule's took the host application down. The
|
|
82
|
+
# contract requires a numeric operator to return false on non-numeric input
|
|
83
|
+
# rather than coercing — "1.10.0" > "1.9.0" must not silently succeed — so a
|
|
84
|
+
# mismatch is simply a no-match. String-to-string comparison is kept, since Ruby
|
|
85
|
+
# orders strings meaningfully and the server behaves the same way.
|
|
19
86
|
when 'greaterThan'
|
|
20
|
-
a > b[0]
|
|
87
|
+
comparable?(a, b[0]) && a > b[0]
|
|
21
88
|
when 'greaterThanOrEqual'
|
|
22
|
-
a >= b[0]
|
|
89
|
+
comparable?(a, b[0]) && a >= b[0]
|
|
23
90
|
when 'lessThan'
|
|
24
|
-
a < b[0]
|
|
91
|
+
comparable?(a, b[0]) && a < b[0]
|
|
25
92
|
when 'lessThanOrEqual'
|
|
26
|
-
a <= b[0]
|
|
93
|
+
comparable?(a, b[0]) && a <= b[0]
|
|
27
94
|
when 'before'
|
|
28
|
-
|
|
95
|
+
a_time = to_epoch(a)
|
|
96
|
+
b_time = to_epoch(b[0])
|
|
97
|
+
!!(a_time && b_time && a_time < b_time)
|
|
29
98
|
when 'after'
|
|
30
|
-
|
|
99
|
+
a_time = to_epoch(a)
|
|
100
|
+
b_time = to_epoch(b[0])
|
|
101
|
+
!!(a_time && b_time && a_time > b_time)
|
|
31
102
|
else
|
|
32
103
|
false
|
|
33
104
|
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
|
|
@@ -1,19 +1,37 @@
|
|
|
1
1
|
require 'featureflow/client'
|
|
2
|
+
require 'featureflow/user_builder'
|
|
2
3
|
|
|
3
4
|
module Featureflow
|
|
4
5
|
class RailsClient
|
|
5
6
|
def initialize(request)
|
|
6
7
|
@request = request
|
|
7
8
|
end
|
|
8
|
-
|
|
9
|
-
|
|
9
|
+
|
|
10
|
+
# `user` is whatever Client#evaluate accepts: a String user id, or a user
|
|
11
|
+
# Hash built with Featureflow::UserBuilder ({id:, attributes:}). The current
|
|
12
|
+
# request's ip and url are merged in as implicit attributes so targeting
|
|
13
|
+
# rules can match on them; anything the caller set explicitly wins.
|
|
14
|
+
def evaluate(key, user)
|
|
15
|
+
Featureflow.evaluate(key, with_request_attributes(user))
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
def with_request_attributes(user)
|
|
21
|
+
user = UserBuilder.new(user).build if user.is_a?(String)
|
|
22
|
+
# Anything else is left alone so Client#evaluate raises its own ArgumentError.
|
|
23
|
+
return user unless user.is_a?(Hash)
|
|
24
|
+
|
|
25
|
+
user.merge(attributes: request_attributes.merge(user[:attributes] || {}))
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Attribute keys are Strings throughout the evaluation path (UserBuilder
|
|
29
|
+
# normalises them), so the request attributes are keyed as Strings too.
|
|
30
|
+
def request_attributes
|
|
31
|
+
{
|
|
10
32
|
'featureflow.ip' => @request.remote_ip,
|
|
11
33
|
'featureflow.url' => @request.original_url
|
|
12
34
|
}
|
|
13
|
-
context = {key: context, values: {}} if context.is_a?(String)
|
|
14
|
-
Featureflow.featureflow.evaluate(key,
|
|
15
|
-
key: context[:key],
|
|
16
|
-
values: request_context_values.merge(context[:values]))
|
|
17
35
|
end
|
|
18
36
|
end
|
|
19
|
-
end
|
|
37
|
+
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
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
module Featureflow
|
|
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
|
+
self
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def with_attributes(hash)
|
|
11
|
+
raise ArgumentError, 'Parameter hash must be a Hash' unless hash.is_a?(Hash)
|
|
12
|
+
normalized = {}
|
|
13
|
+
hash.each do |k, v|
|
|
14
|
+
raise ArgumentError, "Value for #{k} must be a valid 'primitive' JSON datatype" unless valid_attribute?(v)
|
|
15
|
+
normalized[k.to_s] = v
|
|
16
|
+
end
|
|
17
|
+
@attributes = @attributes.merge(normalized)
|
|
18
|
+
self
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def build
|
|
22
|
+
{
|
|
23
|
+
id: @user_id,
|
|
24
|
+
attributes: @attributes
|
|
25
|
+
}
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private def valid_attribute?(attributes)
|
|
29
|
+
Array(attributes).all? do |v|
|
|
30
|
+
[String, Numeric, TrueClass, FalseClass].any? { |type| v.is_a?(type) }
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
data/lib/featureflow/version.rb
CHANGED
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
|
|
@@ -9,7 +9,7 @@ class FeatureflowGenerator < Rails::Generators::Base
|
|
|
9
9
|
desc "Configures the featureflow with your API key"
|
|
10
10
|
|
|
11
11
|
def create_initializer_file
|
|
12
|
-
unless /^srv-env-[a-f0-9]{32}$/ =~ api_key
|
|
12
|
+
unless /^sdk-srv-env-[a-f0-9]{32}$/ =~ api_key
|
|
13
13
|
raise Thor::Error, "Invalid featureflow environment api key #{api_key.inspect}\nYou can find your environment api key on your featureflow dashboard at https://[APP-NAME].featureflow.io/"
|
|
14
14
|
end
|
|
15
15
|
|
metadata
CHANGED
|
@@ -1,105 +1,113 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: featureflow
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.7.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
|
-
-
|
|
8
|
-
autorequire:
|
|
7
|
+
- Oliver Oldfield-Hodge
|
|
8
|
+
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date:
|
|
11
|
+
date: 2026-08-11 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
|
-
name:
|
|
14
|
+
name: rake
|
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
|
16
16
|
requirements:
|
|
17
17
|
- - "~>"
|
|
18
18
|
- !ruby/object:Gem::Version
|
|
19
|
-
version: '
|
|
19
|
+
version: '13.0'
|
|
20
20
|
type: :development
|
|
21
21
|
prerelease: false
|
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
|
23
23
|
requirements:
|
|
24
24
|
- - "~>"
|
|
25
25
|
- !ruby/object:Gem::Version
|
|
26
|
-
version: '
|
|
26
|
+
version: '13.0'
|
|
27
27
|
- !ruby/object:Gem::Dependency
|
|
28
|
-
name:
|
|
28
|
+
name: rspec
|
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
|
30
30
|
requirements:
|
|
31
31
|
- - "~>"
|
|
32
32
|
- !ruby/object:Gem::Version
|
|
33
|
-
version: '
|
|
33
|
+
version: '3.0'
|
|
34
34
|
type: :development
|
|
35
35
|
prerelease: false
|
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
|
37
37
|
requirements:
|
|
38
38
|
- - "~>"
|
|
39
39
|
- !ruby/object:Gem::Version
|
|
40
|
-
version: '
|
|
40
|
+
version: '3.0'
|
|
41
41
|
- !ruby/object:Gem::Dependency
|
|
42
|
-
name:
|
|
42
|
+
name: cucumber
|
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
|
44
44
|
requirements:
|
|
45
45
|
- - "~>"
|
|
46
46
|
- !ruby/object:Gem::Version
|
|
47
|
-
version: '
|
|
47
|
+
version: '11.0'
|
|
48
48
|
type: :development
|
|
49
49
|
prerelease: false
|
|
50
50
|
version_requirements: !ruby/object:Gem::Requirement
|
|
51
51
|
requirements:
|
|
52
52
|
- - "~>"
|
|
53
53
|
- !ruby/object:Gem::Version
|
|
54
|
-
version: '
|
|
54
|
+
version: '11.0'
|
|
55
55
|
- !ruby/object:Gem::Dependency
|
|
56
|
-
name:
|
|
56
|
+
name: webrick
|
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
|
58
|
+
requirements:
|
|
59
|
+
- - "~>"
|
|
60
|
+
- !ruby/object:Gem::Version
|
|
61
|
+
version: '1.8'
|
|
62
|
+
type: :development
|
|
63
|
+
prerelease: false
|
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
65
|
+
requirements:
|
|
66
|
+
- - "~>"
|
|
67
|
+
- !ruby/object:Gem::Version
|
|
68
|
+
version: '1.8'
|
|
69
|
+
- !ruby/object:Gem::Dependency
|
|
70
|
+
name: debug
|
|
57
71
|
requirement: !ruby/object:Gem::Requirement
|
|
58
72
|
requirements:
|
|
59
73
|
- - "~>"
|
|
60
74
|
- !ruby/object:Gem::Version
|
|
61
|
-
version:
|
|
75
|
+
version: '1.9'
|
|
62
76
|
type: :development
|
|
63
77
|
prerelease: false
|
|
64
78
|
version_requirements: !ruby/object:Gem::Requirement
|
|
65
79
|
requirements:
|
|
66
80
|
- - "~>"
|
|
67
81
|
- !ruby/object:Gem::Version
|
|
68
|
-
version:
|
|
82
|
+
version: '1.9'
|
|
69
83
|
- !ruby/object:Gem::Dependency
|
|
70
84
|
name: excon
|
|
71
85
|
requirement: !ruby/object:Gem::Requirement
|
|
72
86
|
requirements:
|
|
73
87
|
- - "~>"
|
|
74
88
|
- !ruby/object:Gem::Version
|
|
75
|
-
version:
|
|
89
|
+
version: '1.5'
|
|
76
90
|
type: :runtime
|
|
77
91
|
prerelease: false
|
|
78
92
|
version_requirements: !ruby/object:Gem::Requirement
|
|
79
93
|
requirements:
|
|
80
94
|
- - "~>"
|
|
81
95
|
- !ruby/object:Gem::Version
|
|
82
|
-
version:
|
|
96
|
+
version: '1.5'
|
|
83
97
|
description: Connects your server with Featureflow. Create features at https://featureflow.io.
|
|
84
98
|
email:
|
|
85
|
-
-
|
|
99
|
+
- contact@featureflow.io
|
|
86
100
|
executables: []
|
|
87
101
|
extensions: []
|
|
88
102
|
extra_rdoc_files: []
|
|
89
103
|
files:
|
|
90
|
-
- ".gitignore"
|
|
91
|
-
- ".rubocop.yml"
|
|
92
104
|
- CHANGELOG.md
|
|
93
|
-
- Gemfile
|
|
94
|
-
- Gemfile.lock
|
|
95
105
|
- LICENSE
|
|
96
106
|
- README.md
|
|
97
|
-
- Rakefile
|
|
98
|
-
- featureflow.gemspec
|
|
99
107
|
- lib/featureflow.rb
|
|
100
108
|
- lib/featureflow/client.rb
|
|
101
109
|
- lib/featureflow/conditions.rb
|
|
102
|
-
- lib/featureflow/
|
|
110
|
+
- lib/featureflow/configuration.rb
|
|
103
111
|
- lib/featureflow/evaluate.rb
|
|
104
112
|
- lib/featureflow/evaluate_helpers.rb
|
|
105
113
|
- lib/featureflow/events_client.rb
|
|
@@ -107,14 +115,18 @@ files:
|
|
|
107
115
|
- lib/featureflow/polling_client.rb
|
|
108
116
|
- lib/featureflow/rails/rails_client.rb
|
|
109
117
|
- lib/featureflow/rails/railtie.rb
|
|
118
|
+
- lib/featureflow/user_builder.rb
|
|
110
119
|
- lib/featureflow/version.rb
|
|
111
120
|
- lib/generators/featureflow_generator.rb
|
|
112
|
-
- test.rb
|
|
113
121
|
homepage: https://github.com/featureflow/featureflow-ruby-sdk
|
|
114
122
|
licenses:
|
|
115
|
-
-
|
|
116
|
-
metadata:
|
|
117
|
-
|
|
123
|
+
- Apache-2.0
|
|
124
|
+
metadata:
|
|
125
|
+
homepage_uri: https://github.com/featureflow/featureflow-ruby-sdk
|
|
126
|
+
source_code_uri: https://github.com/featureflow/featureflow-ruby-sdk
|
|
127
|
+
changelog_uri: https://github.com/featureflow/featureflow-ruby-sdk/blob/master/CHANGELOG.md
|
|
128
|
+
bug_tracker_uri: https://github.com/featureflow/featureflow-ruby-sdk/issues
|
|
129
|
+
post_install_message:
|
|
118
130
|
rdoc_options: []
|
|
119
131
|
require_paths:
|
|
120
132
|
- lib
|
|
@@ -122,16 +134,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
122
134
|
requirements:
|
|
123
135
|
- - ">="
|
|
124
136
|
- !ruby/object:Gem::Version
|
|
125
|
-
version: '
|
|
137
|
+
version: '3.1'
|
|
126
138
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
127
139
|
requirements:
|
|
128
140
|
- - ">="
|
|
129
141
|
- !ruby/object:Gem::Version
|
|
130
142
|
version: '0'
|
|
131
143
|
requirements: []
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
signing_key:
|
|
144
|
+
rubygems_version: 3.5.22
|
|
145
|
+
signing_key:
|
|
135
146
|
specification_version: 4
|
|
136
147
|
summary: Ruby SDK for Featureflow
|
|
137
148
|
test_files: []
|
data/.gitignore
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
pkg
|
data/.rubocop.yml
DELETED
data/Gemfile
DELETED
data/Gemfile.lock
DELETED
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
PATH
|
|
2
|
-
remote: .
|
|
3
|
-
specs:
|
|
4
|
-
featureflow (0.5.0)
|
|
5
|
-
excon (~> 0.57.0)
|
|
6
|
-
|
|
7
|
-
GEM
|
|
8
|
-
remote: https://rubygems.org/
|
|
9
|
-
specs:
|
|
10
|
-
builder (3.2.3)
|
|
11
|
-
cucumber (2.4.0)
|
|
12
|
-
builder (>= 2.1.2)
|
|
13
|
-
cucumber-core (~> 1.5.0)
|
|
14
|
-
cucumber-wire (~> 0.0.1)
|
|
15
|
-
diff-lcs (>= 1.1.3)
|
|
16
|
-
gherkin (~> 4.0)
|
|
17
|
-
multi_json (>= 1.7.5, < 2.0)
|
|
18
|
-
multi_test (>= 0.1.2)
|
|
19
|
-
cucumber-core (1.5.0)
|
|
20
|
-
gherkin (~> 4.0)
|
|
21
|
-
cucumber-wire (0.0.1)
|
|
22
|
-
diff-lcs (1.3)
|
|
23
|
-
excon (0.57.0)
|
|
24
|
-
gherkin (4.1.3)
|
|
25
|
-
multi_json (1.12.1)
|
|
26
|
-
multi_test (0.1.2)
|
|
27
|
-
rake (10.5.0)
|
|
28
|
-
rspec (3.6.0)
|
|
29
|
-
rspec-core (~> 3.6.0)
|
|
30
|
-
rspec-expectations (~> 3.6.0)
|
|
31
|
-
rspec-mocks (~> 3.6.0)
|
|
32
|
-
rspec-core (3.6.0)
|
|
33
|
-
rspec-support (~> 3.6.0)
|
|
34
|
-
rspec-expectations (3.6.0)
|
|
35
|
-
diff-lcs (>= 1.2.0, < 2.0)
|
|
36
|
-
rspec-support (~> 3.6.0)
|
|
37
|
-
rspec-mocks (3.6.0)
|
|
38
|
-
diff-lcs (>= 1.2.0, < 2.0)
|
|
39
|
-
rspec-support (~> 3.6.0)
|
|
40
|
-
rspec-support (3.6.0)
|
|
41
|
-
|
|
42
|
-
PLATFORMS
|
|
43
|
-
ruby
|
|
44
|
-
|
|
45
|
-
DEPENDENCIES
|
|
46
|
-
bundler (~> 1.15)
|
|
47
|
-
cucumber (~> 2.4.0)
|
|
48
|
-
featureflow!
|
|
49
|
-
rake (~> 10.0)
|
|
50
|
-
rspec (~> 3.0)
|
|
51
|
-
|
|
52
|
-
BUNDLED WITH
|
|
53
|
-
1.15.1
|
data/Rakefile
DELETED
data/featureflow.gemspec
DELETED
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
# coding: utf-8
|
|
2
|
-
lib = File.expand_path('../lib', __FILE__)
|
|
3
|
-
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
|
4
|
-
require 'featureflow/version'
|
|
5
|
-
|
|
6
|
-
Gem::Specification.new do |spec|
|
|
7
|
-
spec.name = "featureflow"
|
|
8
|
-
spec.version = Featureflow::VERSION
|
|
9
|
-
spec.authors = ["Henry Young"]
|
|
10
|
-
spec.email = ["henry@featureflow.io"]
|
|
11
|
-
|
|
12
|
-
spec.summary = "Ruby SDK for Featureflow"
|
|
13
|
-
spec.description = "Connects your server with Featureflow. Create features at https://featureflow.io."
|
|
14
|
-
spec.homepage = "https://github.com/featureflow/featureflow-ruby-sdk"
|
|
15
|
-
spec.license = "MIT"
|
|
16
|
-
|
|
17
|
-
# Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host'
|
|
18
|
-
# to allow pushing to a single host or delete this section to allow pushing to any host.
|
|
19
|
-
# if spec.respond_to?(:metadata)
|
|
20
|
-
# spec.metadata["allowed_push_host"] = "TODO: Set to 'http://mygemserver.com'"
|
|
21
|
-
# else
|
|
22
|
-
# raise "RubyGems 2.0 or newer is required to protect against " \
|
|
23
|
-
# "public gem pushes."
|
|
24
|
-
# end
|
|
25
|
-
|
|
26
|
-
spec.files = `git ls-files -z`.split("\x0").reject do |f|
|
|
27
|
-
f.match(%r{^(test|spec|features)/})
|
|
28
|
-
end
|
|
29
|
-
spec.bindir = "exe"
|
|
30
|
-
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
|
31
|
-
spec.require_paths = ["lib"]
|
|
32
|
-
|
|
33
|
-
spec.add_development_dependency "bundler", "~> 1.15"
|
|
34
|
-
spec.add_development_dependency "rake", "~> 10.0"
|
|
35
|
-
spec.add_development_dependency "rspec", "~> 3.0"
|
|
36
|
-
spec.add_development_dependency "cucumber", "~> 2.4.0"
|
|
37
|
-
spec.add_dependency "excon", "~> 0.57.0"
|
|
38
|
-
end
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
module Featureflow
|
|
2
|
-
class ContextBuilder
|
|
3
|
-
def initialize(key)
|
|
4
|
-
raise ArgumentError, 'Parameter key must be a String' unless key.is_a?(String) && !key.empty?
|
|
5
|
-
@context_key = key
|
|
6
|
-
@values = {}
|
|
7
|
-
self
|
|
8
|
-
end
|
|
9
|
-
|
|
10
|
-
def with_values(hash)
|
|
11
|
-
raise ArgumentError, 'Parameter hash must be a Hash' unless hash.is_a?(Hash)
|
|
12
|
-
hash = hash.dup
|
|
13
|
-
hash.each do |k, v|
|
|
14
|
-
raise ArgumentError, "Value for #{k} must be a valid 'primitive' JSON datatype" unless valid_value?(v)
|
|
15
|
-
hash[k.to_s] = h.delete(k) unless k.is_a?(String)
|
|
16
|
-
end
|
|
17
|
-
@values = @values.merge(hash)
|
|
18
|
-
self
|
|
19
|
-
end
|
|
20
|
-
|
|
21
|
-
def build
|
|
22
|
-
{
|
|
23
|
-
key: @context_key,
|
|
24
|
-
values: @values
|
|
25
|
-
}
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
private def valid_value?(values)
|
|
29
|
-
Array(values).all? do |v|
|
|
30
|
-
[String, Numeric, TrueClass, FalseClass].any? { |type| v.is_a?(type) }
|
|
31
|
-
end
|
|
32
|
-
end
|
|
33
|
-
end
|
|
34
|
-
end
|
data/test.rb
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
$:.unshift(File.dirname(__FILE__) + "/lib/")
|
|
2
|
-
|
|
3
|
-
require 'featureflow'
|
|
4
|
-
with_features = [
|
|
5
|
-
Featureflow::Feature.create('default', 'variant')
|
|
6
|
-
]
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
# api_key = 'srv-env-9b5fff890c724d119a334a64ed4d2eb2'
|
|
11
|
-
api_key = 'srv-env-f472cfa8c2774ea2b3678fc5a3dbfe13'
|
|
12
|
-
featureflow_client = Featureflow::Client.new(api_key: api_key, with_features: with_features, url: 'http://10.10.2.163:8081')
|
|
13
|
-
context = Featureflow::ContextBuilder.new('user1').build
|
|
14
|
-
puts('test-integration is on? ' + featureflow_client.evaluate('test-integration', context).on?.to_s)
|
|
15
|
-
featureflow_client.evaluate('nooooo', context).on?
|
|
16
|
-
featureflow_client.evaluate('default', context).on?
|
|
17
|
-
|
|
18
|
-
#
|
|
19
|
-
loop do
|
|
20
|
-
sleep 1
|
|
21
|
-
end
|