molasses 0.1.1 → 0.4.2
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 +4 -4
- data/README.md +37 -7
- data/lib/molasses.rb +143 -46
- data/spec/molasses_spec.rb +541 -232
- metadata +17 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ffa4e080550ec76be2ebafd0b43402f30757c3dfb99b04dd513cca321333e3e8
|
4
|
+
data.tar.gz: f07d34cadfc75714479ade49dba636b79cf55ee099b8355f2a9160656499be1a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e3964e86375dcec32653fc56de635e4e60e2c0beaf5f5d2fd9893d4ca8c17423bb9b0bd90bc23e114dea2b25dd1aad6e4616756a804e6a1d2b277ea158902d85
|
7
|
+
data.tar.gz: d4361b370982a5f56b847e8b0823712995b94481829e77c74c586aaa2ddc1d264f3dea12fffadfeffc5cf131b8304bcaef566a8bb2d4ba52dd217c01773e0675
|
data/README.md
CHANGED
@@ -28,10 +28,12 @@ client = Molasses::Client.new("test_api_key")
|
|
28
28
|
|
29
29
|
```
|
30
30
|
|
31
|
-
If you decide
|
31
|
+
If you decide you want to auto track experiments being viewed (experiment started events) you can turn that on by setting the `:auto_send_events` field to `true`
|
32
32
|
|
33
33
|
```go
|
34
|
-
client = Molasses::Client.new("test_api_key",
|
34
|
+
client = Molasses::Client.new("test_api_key", {
|
35
|
+
:auto_send_events => true
|
36
|
+
})
|
35
37
|
|
36
38
|
```
|
37
39
|
|
@@ -56,20 +58,48 @@ You can check if a feature is active for a user who is anonymous by just calling
|
|
56
58
|
client.is_active("TEST_FEATURE_FOR_USER")
|
57
59
|
```
|
58
60
|
|
59
|
-
### Experiments
|
61
|
+
### Tracking and Experiments
|
60
62
|
|
61
|
-
To track
|
63
|
+
To track any analytics event you can call `track`. experiment_success takes the event's name,the user, and any additional parameters for the event.
|
62
64
|
|
63
65
|
```ruby
|
64
|
-
client.
|
66
|
+
client.track("Button Clicked", {
|
67
|
+
"id"=>"foo",
|
68
|
+
"params"=>{
|
69
|
+
"isBetaUser"=>"false",
|
70
|
+
"isScaredUser"=>"false"
|
71
|
+
}
|
72
|
+
}, {
|
65
73
|
"version": "v2.3.0"
|
66
|
-
},
|
74
|
+
},)
|
75
|
+
```
|
76
|
+
|
77
|
+
To track whether an experiment was started you can call `experiment_started`. experiment_started takes the feature's name,the user, and any additional parameters for the event.
|
78
|
+
|
79
|
+
```ruby
|
80
|
+
client.experiment_started("GOOGLE_SSO", {
|
67
81
|
"id"=>"foo",
|
68
82
|
"params"=>{
|
69
83
|
"isBetaUser"=>"false",
|
70
84
|
"isScaredUser"=>"false"
|
71
85
|
}
|
72
|
-
}
|
86
|
+
}, {
|
87
|
+
"version": "v2.3.0"
|
88
|
+
},)
|
89
|
+
```
|
90
|
+
|
91
|
+
To track whether an experiment was successful you can call `experiment_success`. experiment_success takes the feature's name,the user, and any additional parameters for the event.
|
92
|
+
|
93
|
+
```ruby
|
94
|
+
client.experiment_success("GOOGLE_SSO", {
|
95
|
+
"id"=>"foo",
|
96
|
+
"params"=>{
|
97
|
+
"isBetaUser"=>"false",
|
98
|
+
"isScaredUser"=>"false"
|
99
|
+
}
|
100
|
+
}, {
|
101
|
+
"version": "v2.3.0"
|
102
|
+
},)
|
73
103
|
```
|
74
104
|
|
75
105
|
## Example
|
data/lib/molasses.rb
CHANGED
@@ -1,33 +1,34 @@
|
|
1
|
-
require
|
2
|
-
require
|
3
|
-
require
|
4
|
-
require
|
1
|
+
require "logger"
|
2
|
+
require "workers"
|
3
|
+
require "faraday"
|
4
|
+
require "json"
|
5
|
+
require "zlib"
|
6
|
+
require "semantic"
|
7
|
+
|
5
8
|
module Molasses
|
6
9
|
class Client
|
7
|
-
def initialize(api_key,
|
10
|
+
def initialize(api_key, opts = {})
|
8
11
|
@api_key = api_key
|
9
|
-
@send_events =
|
10
|
-
|
11
|
-
|
12
|
-
else
|
13
|
-
@base_url = 'https://us-central1-molasses-36bff.cloudfunctions.net'
|
14
|
-
end
|
12
|
+
@send_events = opts[:auto_send_events] || false
|
13
|
+
@base_url = opts[:base_url] || "https://sdk.molasses.app/v1"
|
14
|
+
@logger = opts[:logger] || get_default_logger
|
15
15
|
@conn = Faraday.new(
|
16
16
|
url: @base_url,
|
17
17
|
headers: {
|
18
|
-
|
19
|
-
|
20
|
-
}
|
18
|
+
"Content-Type" => "application/json",
|
19
|
+
"Authorization" => "Bearer #{@api_key}",
|
20
|
+
},
|
21
21
|
)
|
22
22
|
@feature_cache = {}
|
23
23
|
@initialized = {}
|
24
|
+
|
24
25
|
fetch_features
|
25
26
|
@timer = Workers::PeriodicTimer.new(15) do
|
26
27
|
fetch_features
|
27
28
|
end
|
28
29
|
end
|
29
30
|
|
30
|
-
def is_active(key, user={})
|
31
|
+
def is_active(key, user = {})
|
31
32
|
unless @initialized
|
32
33
|
return false
|
33
34
|
end
|
@@ -37,16 +38,17 @@ module Molasses
|
|
37
38
|
result = user_active(feature, user)
|
38
39
|
if @send_events && user && user.include?("id")
|
39
40
|
send_event({
|
40
|
-
"event"=> "experiment_started",
|
41
|
-
"tags"=> user["params"],
|
42
|
-
"userId"=> user["id"],
|
43
|
-
"featureId"=> feature["id"],
|
44
|
-
"featureName"=> key,
|
45
|
-
"testType"=> result ? "experiment" : "control"
|
41
|
+
"event" => "experiment_started",
|
42
|
+
"tags" => user["params"],
|
43
|
+
"userId" => user["id"],
|
44
|
+
"featureId" => feature["id"],
|
45
|
+
"featureName" => key,
|
46
|
+
"testType" => result ? "experiment" : "control",
|
46
47
|
})
|
47
48
|
end
|
48
49
|
return result
|
49
50
|
else
|
51
|
+
@logger.info "Warning - feature flag #{key} not set in environment"
|
50
52
|
return false
|
51
53
|
end
|
52
54
|
end
|
@@ -55,21 +57,57 @@ module Molasses
|
|
55
57
|
@timer.cancel
|
56
58
|
end
|
57
59
|
|
58
|
-
def
|
59
|
-
if !@initialized ||
|
60
|
-
|
60
|
+
def experiment_started(key, user = nil, additional_details = {})
|
61
|
+
if !@initialized || user == nil || !user.include?("id")
|
62
|
+
return false
|
63
|
+
end
|
64
|
+
unless @feature_cache.include?(key)
|
65
|
+
@logger.info "Warning - feature flag #{key} not set in environment"
|
66
|
+
return false
|
61
67
|
end
|
62
68
|
feature = @feature_cache[key]
|
63
69
|
result = is_active(feature, user)
|
64
70
|
send_event({
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
+
"event" => "experiment_started",
|
72
|
+
"tags" => user.include?("params") ? user["params"].merge(additional_details) : additional_details,
|
73
|
+
"userId" => user["id"],
|
74
|
+
"featureId" => feature["id"],
|
75
|
+
"featureName" => key,
|
76
|
+
"testType" => result ? "experiment" : "control",
|
77
|
+
})
|
78
|
+
end
|
79
|
+
|
80
|
+
def experiment_success(key, user = nil, additional_details = {})
|
81
|
+
if !@initialized || user == nil || !user.include?("id")
|
82
|
+
return false
|
83
|
+
end
|
84
|
+
unless @feature_cache.include?(key)
|
85
|
+
@logger.info "Warning - feature flag #{key} not set in environment"
|
86
|
+
return false
|
87
|
+
end
|
88
|
+
feature = @feature_cache[key]
|
89
|
+
result = is_active(feature, user)
|
90
|
+
send_event({
|
91
|
+
"event" => "experiment_success",
|
92
|
+
"tags" => user.include?("params") ? user["params"].merge(additional_details) : additional_details,
|
93
|
+
"userId" => user["id"],
|
94
|
+
"featureId" => feature["id"],
|
95
|
+
"featureName" => key,
|
96
|
+
"testType" => result ? "experiment" : "control",
|
97
|
+
})
|
98
|
+
end
|
99
|
+
|
100
|
+
def track(key, user = nil, additional_details = {})
|
101
|
+
if user == nil || !user.include?("id")
|
102
|
+
return false
|
103
|
+
end
|
104
|
+
send_event({
|
105
|
+
"event" => key,
|
106
|
+
"tags" => user.include?("params") ? user["params"].merge(additional_details) : additional_details,
|
107
|
+
"userId" => user["id"],
|
71
108
|
})
|
72
109
|
end
|
110
|
+
|
73
111
|
private
|
74
112
|
|
75
113
|
def user_active(feature, user)
|
@@ -78,13 +116,13 @@ module Molasses
|
|
78
116
|
end
|
79
117
|
|
80
118
|
unless user && user.include?("id")
|
81
|
-
|
119
|
+
return true
|
82
120
|
end
|
83
121
|
|
84
122
|
segment_map = {}
|
85
123
|
for feature_segment in feature["segments"]
|
86
|
-
|
87
|
-
|
124
|
+
segment_type = feature_segment["segmentType"]
|
125
|
+
segment_map[segment_type] = feature_segment
|
88
126
|
end
|
89
127
|
|
90
128
|
if segment_map.include?("alwaysControl") and in_segment(user, segment_map["alwaysControl"])
|
@@ -97,7 +135,7 @@ module Molasses
|
|
97
135
|
return user_percentage(user["id"], segment_map["everyoneElse"]["percentage"])
|
98
136
|
end
|
99
137
|
|
100
|
-
def user_percentage(id="", percentage = 0)
|
138
|
+
def user_percentage(id = "", percentage = 0)
|
101
139
|
if percentage == 100
|
102
140
|
return true
|
103
141
|
end
|
@@ -114,7 +152,7 @@ module Molasses
|
|
114
152
|
def in_segment(user, segment_map)
|
115
153
|
user_constraints = segment_map["userConstraints"]
|
116
154
|
constraints_length = user_constraints.length
|
117
|
-
constraints_to_be_met = segment_map["constraint"] ==
|
155
|
+
constraints_to_be_met = segment_map["constraint"] == "any" ? 1 : constraints_length
|
118
156
|
constraints_met = 0
|
119
157
|
|
120
158
|
for constraint in user_constraints
|
@@ -137,51 +175,110 @@ module Molasses
|
|
137
175
|
return constraints_met >= constraints_to_be_met
|
138
176
|
end
|
139
177
|
|
178
|
+
def parse_number(user_value)
|
179
|
+
case user_value
|
180
|
+
when Numeric
|
181
|
+
return user_value
|
182
|
+
when TrueClass
|
183
|
+
return 1
|
184
|
+
when FalseClass
|
185
|
+
return 0
|
186
|
+
when String
|
187
|
+
return user_value.to_f
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
def parse_bool(user_value)
|
192
|
+
case user_value
|
193
|
+
when Numeric
|
194
|
+
return user_value == 1
|
195
|
+
when TrueClass, FalseClass
|
196
|
+
return user_value
|
197
|
+
when String
|
198
|
+
return user_value == "true"
|
199
|
+
end
|
200
|
+
end
|
201
|
+
|
140
202
|
def meets_constraint(user_value, param_exists, constraint)
|
141
203
|
operator = constraint["operator"]
|
142
204
|
if param_exists == false
|
143
205
|
return false
|
144
206
|
end
|
145
207
|
|
208
|
+
constraint_value = constraint["values"]
|
209
|
+
case constraint["userParamType"]
|
210
|
+
when "number"
|
211
|
+
user_value = parse_number(user_value)
|
212
|
+
constraint_value = parse_number(constraint_value)
|
213
|
+
when "boolean"
|
214
|
+
user_value = parse_bool(user_value)
|
215
|
+
constraint_value = parse_bool(constraint_value)
|
216
|
+
when "semver"
|
217
|
+
user_value = Semantic::Version.new user_value
|
218
|
+
constraint_value = Semantic::Version.new constraint_value
|
219
|
+
else
|
220
|
+
user_value = user_value.to_s
|
221
|
+
end
|
222
|
+
|
146
223
|
case operator
|
147
224
|
when "in"
|
148
|
-
list_values =
|
225
|
+
list_values = constraint_value.split(",")
|
149
226
|
return list_values.include? user_value
|
150
227
|
when "nin"
|
151
|
-
list_values =
|
228
|
+
list_values = constraint_value.split(",")
|
152
229
|
return !list_values.include?(user_value)
|
230
|
+
when "lt"
|
231
|
+
return user_value < constraint_value
|
232
|
+
when "lte"
|
233
|
+
return user_value <= constraint_value
|
234
|
+
when "gt"
|
235
|
+
return user_value > constraint_value
|
236
|
+
when "gte"
|
237
|
+
return user_value >= constraint_value
|
153
238
|
when "equals"
|
154
|
-
return user_value ==
|
239
|
+
return user_value == constraint_value
|
155
240
|
when "doesNotEqual"
|
156
|
-
return user_value !=
|
241
|
+
return user_value != constraint_value
|
157
242
|
when "contains"
|
158
|
-
return
|
243
|
+
return constraint_value.include?(user_value)
|
159
244
|
when "doesNotContain"
|
160
|
-
return !
|
245
|
+
return !constraint_value.include?(user_value)
|
161
246
|
else
|
162
247
|
return false
|
163
248
|
end
|
164
249
|
end
|
165
250
|
|
166
251
|
def send_event(event_options)
|
167
|
-
@conn.post(
|
252
|
+
@conn.post("analytics", event_options.to_json)
|
168
253
|
end
|
169
254
|
|
170
255
|
def fetch_features()
|
171
|
-
response = @conn.get(
|
256
|
+
response = @conn.get("features")
|
172
257
|
if response.status == 200
|
173
258
|
data = JSON.parse(response.body)
|
174
259
|
if data.include?("data") and data["data"].include?("features")
|
175
260
|
features = data["data"]["features"]
|
176
|
-
for feature in features
|
261
|
+
for feature in features
|
177
262
|
@feature_cache[feature["key"]] = feature
|
178
263
|
end
|
264
|
+
unless @initialized
|
265
|
+
@logger.info "Molasses - connected and initialized"
|
266
|
+
end
|
179
267
|
@initialized = true
|
180
268
|
end
|
181
269
|
else
|
182
|
-
|
270
|
+
@logger.info "Molasses - #{response.status} - #{response.body}"
|
183
271
|
end
|
184
272
|
end
|
185
273
|
|
274
|
+
def get_default_logger
|
275
|
+
if defined?(Rails) && Rails.respond_to?(:logger)
|
276
|
+
Rails.logger
|
277
|
+
else
|
278
|
+
log = ::Logger.new(STDOUT)
|
279
|
+
log.level = ::Logger::WARN
|
280
|
+
log
|
281
|
+
end
|
282
|
+
end
|
186
283
|
end
|
187
|
-
end
|
284
|
+
end
|
data/spec/molasses_spec.rb
CHANGED
@@ -1,225 +1,358 @@
|
|
1
|
-
require
|
2
|
-
|
3
|
-
require
|
1
|
+
require "simplecov"
|
2
|
+
SimpleCov.start
|
3
|
+
require "faraday"
|
4
|
+
require "molasses"
|
5
|
+
require "webmock/rspec"
|
4
6
|
responseA = {
|
5
7
|
"data": {
|
6
|
-
|
8
|
+
"features": [
|
9
|
+
{
|
10
|
+
"active": true,
|
11
|
+
"description": "foo",
|
12
|
+
"key": "FOO_TEST",
|
13
|
+
"segments": [
|
7
14
|
{
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
"userParam": "isScaredUser",
|
19
|
-
"operator": "in",
|
20
|
-
"values": "true,maybe",
|
21
|
-
},
|
22
|
-
],
|
23
|
-
},
|
24
|
-
{
|
25
|
-
"constraint": "all",
|
26
|
-
"percentage": 100,
|
27
|
-
"segmentType": "alwaysExperiment",
|
28
|
-
"userConstraints": [
|
29
|
-
{
|
30
|
-
"userParam": "isBetaUser",
|
31
|
-
"operator": "equals",
|
32
|
-
"values": "true",
|
33
|
-
},
|
34
|
-
],
|
35
|
-
},
|
36
|
-
{
|
37
|
-
"constraint": "all",
|
38
|
-
"percentage": 100,
|
39
|
-
"segmentType": "everyoneElse",
|
40
|
-
"userConstraints": [],
|
41
|
-
},
|
42
|
-
],
|
15
|
+
"constraint": "all",
|
16
|
+
"percentage": 100,
|
17
|
+
"segmentType": "alwaysControl",
|
18
|
+
"userConstraints": [
|
19
|
+
{
|
20
|
+
"userParam": "isScaredUser",
|
21
|
+
"operator": "in",
|
22
|
+
"values": "true,maybe",
|
23
|
+
},
|
24
|
+
],
|
43
25
|
},
|
44
|
-
|
45
|
-
|
26
|
+
{
|
27
|
+
"constraint": "all",
|
28
|
+
"percentage": 100,
|
29
|
+
"segmentType": "alwaysExperiment",
|
30
|
+
"userConstraints": [
|
31
|
+
{
|
32
|
+
"userParam": "isBetaUser",
|
33
|
+
"operator": "equals",
|
34
|
+
"values": "true",
|
35
|
+
},
|
36
|
+
],
|
37
|
+
},
|
38
|
+
{
|
39
|
+
"constraint": "all",
|
40
|
+
"percentage": 100,
|
41
|
+
"segmentType": "everyoneElse",
|
42
|
+
"userConstraints": [],
|
43
|
+
},
|
44
|
+
],
|
45
|
+
},
|
46
|
+
],
|
47
|
+
},
|
48
|
+
}
|
46
49
|
responseB = {
|
47
50
|
"data": {
|
48
|
-
|
51
|
+
"features": [
|
52
|
+
{
|
53
|
+
"id": "1",
|
54
|
+
"active": true,
|
55
|
+
"description": "foo",
|
56
|
+
"key": "FOO_TEST",
|
57
|
+
"segments": [
|
49
58
|
{
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
{
|
61
|
-
"userParam": "isScaredUser",
|
62
|
-
"operator": "nin",
|
63
|
-
"values": "false,maybe",
|
64
|
-
},
|
65
|
-
],
|
66
|
-
},
|
67
|
-
{
|
68
|
-
"constraint": "all",
|
69
|
-
"percentage": 100,
|
70
|
-
"segmentType": "alwaysExperiment",
|
71
|
-
"userConstraints": [
|
72
|
-
{
|
73
|
-
"userParam": "isBetaUser",
|
74
|
-
"operator": "doesNotEqual",
|
75
|
-
"values": "false",
|
76
|
-
},
|
77
|
-
],
|
78
|
-
},
|
79
|
-
{
|
80
|
-
"constraint": "all",
|
81
|
-
"percentage": 100,
|
82
|
-
"segmentType": "everyoneElse",
|
83
|
-
"userConstraints": [],
|
84
|
-
},
|
85
|
-
],
|
59
|
+
"constraint": "all",
|
60
|
+
"percentage": 100,
|
61
|
+
"segmentType": "alwaysControl",
|
62
|
+
"userConstraints": [
|
63
|
+
{
|
64
|
+
"userParam": "isScaredUser",
|
65
|
+
"operator": "nin",
|
66
|
+
"values": "false,maybe",
|
67
|
+
},
|
68
|
+
],
|
86
69
|
},
|
87
|
-
|
70
|
+
{
|
71
|
+
"constraint": "all",
|
72
|
+
"percentage": 100,
|
73
|
+
"segmentType": "alwaysExperiment",
|
74
|
+
"userConstraints": [
|
75
|
+
{
|
76
|
+
"userParam": "isBetaUser",
|
77
|
+
"operator": "doesNotEqual",
|
78
|
+
"values": "false",
|
79
|
+
},
|
80
|
+
],
|
81
|
+
},
|
82
|
+
{
|
83
|
+
"constraint": "all",
|
84
|
+
"percentage": 100,
|
85
|
+
"segmentType": "everyoneElse",
|
86
|
+
"userConstraints": [],
|
87
|
+
},
|
88
|
+
],
|
89
|
+
},
|
90
|
+
],
|
88
91
|
},
|
89
92
|
}
|
90
93
|
|
91
94
|
responseC = {
|
92
95
|
"data": {
|
93
|
-
|
96
|
+
"features": [
|
97
|
+
{
|
98
|
+
"id": "2",
|
99
|
+
"active": true,
|
100
|
+
"description": "bar",
|
101
|
+
"key": "NUMBERS_BOOLS",
|
102
|
+
"segments": [
|
103
|
+
{
|
104
|
+
"percentage": 100,
|
105
|
+
"segmentType": "alwaysControl",
|
106
|
+
"constraint": "all",
|
107
|
+
"userConstraints": [
|
108
|
+
{
|
109
|
+
"userParam": "lt",
|
110
|
+
"userParamType": "number",
|
111
|
+
"operator": "lt",
|
112
|
+
"values": 12,
|
113
|
+
},
|
114
|
+
{
|
115
|
+
"userParam": "lte",
|
116
|
+
"userParamType": "number",
|
117
|
+
"operator": "lte",
|
118
|
+
"values": 12,
|
119
|
+
},
|
120
|
+
{
|
121
|
+
"userParam": "gt",
|
122
|
+
"userParamType": "number",
|
123
|
+
"operator": "gt",
|
124
|
+
"values": 12,
|
125
|
+
},
|
126
|
+
{
|
127
|
+
"userParam": "gte",
|
128
|
+
"userParamType": "number",
|
129
|
+
"operator": "gte",
|
130
|
+
"values": 12,
|
131
|
+
},
|
132
|
+
{
|
133
|
+
"userParam": "equals",
|
134
|
+
"userParamType": "number",
|
135
|
+
"operator": "equals",
|
136
|
+
"values": 12,
|
137
|
+
},
|
138
|
+
{
|
139
|
+
"userParam": "doesNotEqual",
|
140
|
+
"userParamType": "number",
|
141
|
+
"operator": "doesNotEqual",
|
142
|
+
"values": 12,
|
143
|
+
},
|
144
|
+
{
|
145
|
+
"userParam": "equalsBool",
|
146
|
+
"userParamType": "boolean",
|
147
|
+
"operator": "equals",
|
148
|
+
"values": true,
|
149
|
+
},
|
150
|
+
{
|
151
|
+
"userParam": "doesNotEqualBool",
|
152
|
+
"userParamType": "boolean",
|
153
|
+
"operator": "doesNotEqual",
|
154
|
+
"values": false,
|
155
|
+
},
|
156
|
+
|
157
|
+
],
|
158
|
+
|
159
|
+
},
|
160
|
+
{
|
161
|
+
"constraint": "all",
|
162
|
+
"percentage": 50,
|
163
|
+
"segmentType": "everyoneElse",
|
164
|
+
"userConstraints": [],
|
165
|
+
},
|
166
|
+
],
|
167
|
+
},
|
168
|
+
{
|
169
|
+
"id": "4",
|
170
|
+
"active": true,
|
171
|
+
"description": "bar",
|
172
|
+
"key": "semver",
|
173
|
+
"segments": [
|
174
|
+
{
|
175
|
+
"percentage": 100,
|
176
|
+
"segmentType": "alwaysExperiment",
|
177
|
+
"constraint": "any",
|
178
|
+
"userConstraints": [
|
179
|
+
{
|
180
|
+
"userParam": "lt",
|
181
|
+
"userParamType": "semver",
|
182
|
+
"operator": "lt",
|
183
|
+
"values": "1.2.0",
|
184
|
+
},
|
185
|
+
{
|
186
|
+
"userParam": "lte",
|
187
|
+
"userParamType": "semver",
|
188
|
+
"operator": "lte",
|
189
|
+
"values": "1.2.0",
|
190
|
+
},
|
191
|
+
{
|
192
|
+
"userParam": "gt",
|
193
|
+
"userParamType": "semver",
|
194
|
+
"operator": "gt",
|
195
|
+
"values": "1.2.0",
|
196
|
+
},
|
197
|
+
{
|
198
|
+
"userParam": "gte",
|
199
|
+
"userParamType": "semver",
|
200
|
+
"operator": "gte",
|
201
|
+
"values": "1.2.0",
|
202
|
+
},
|
203
|
+
{
|
204
|
+
"userParam": "equals",
|
205
|
+
"userParamType": "semver",
|
206
|
+
"operator": "equals",
|
207
|
+
"values": "1.2.0",
|
208
|
+
},
|
209
|
+
{
|
210
|
+
"userParam": "doesNotEqual",
|
211
|
+
"userParamType": "semver",
|
212
|
+
"operator": "doesNotEqual",
|
213
|
+
"values": "1.2.0",
|
214
|
+
},
|
215
|
+
|
216
|
+
],
|
217
|
+
|
218
|
+
},
|
219
|
+
{
|
220
|
+
"constraint": "all",
|
221
|
+
"percentage": 0,
|
222
|
+
"segmentType": "everyoneElse",
|
223
|
+
"userConstraints": [],
|
224
|
+
},
|
225
|
+
],
|
226
|
+
},
|
227
|
+
{
|
228
|
+
"id": "1",
|
229
|
+
"active": true,
|
230
|
+
"description": "foo",
|
231
|
+
"key": "FOO_TEST",
|
232
|
+
"segments": [
|
94
233
|
{
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
{
|
116
|
-
"userParam": "isMostDefinitelyScaredUser",
|
117
|
-
"operator": "contains",
|
118
|
-
"values": "scared",
|
119
|
-
},
|
120
|
-
],
|
121
|
-
},
|
122
|
-
{
|
123
|
-
"percentage": 100,
|
124
|
-
"segmentType": "alwaysExperiment",
|
125
|
-
"constraint": "any",
|
126
|
-
"userConstraints": [
|
127
|
-
{
|
128
|
-
"userParam": "isBetaUser",
|
129
|
-
"operator": "doesNotContain",
|
130
|
-
"values": "fal",
|
131
|
-
},
|
132
|
-
{
|
133
|
-
"userParam": "isDefinitelyBetaUser",
|
134
|
-
"operator": "doesNotContain",
|
135
|
-
"values": "fal",
|
136
|
-
},
|
137
|
-
],
|
138
|
-
},
|
139
|
-
{
|
140
|
-
"constraint": "all",
|
141
|
-
"percentage": 100,
|
142
|
-
"segmentType": "everyoneElse",
|
143
|
-
"userConstraints": [],
|
144
|
-
},
|
145
|
-
],
|
234
|
+
"percentage": 100,
|
235
|
+
"segmentType": "alwaysControl",
|
236
|
+
"constraint": "all",
|
237
|
+
"userConstraints": [
|
238
|
+
{
|
239
|
+
"userParam": "isScaredUser",
|
240
|
+
"operator": "contains",
|
241
|
+
"values": "scared",
|
242
|
+
},
|
243
|
+
{
|
244
|
+
"userParam": "isDefinitelyScaredUser",
|
245
|
+
"operator": "contains",
|
246
|
+
"values": "scared",
|
247
|
+
},
|
248
|
+
{
|
249
|
+
"userParam": "isMostDefinitelyScaredUser",
|
250
|
+
"operator": "contains",
|
251
|
+
"values": "scared",
|
252
|
+
},
|
253
|
+
],
|
146
254
|
},
|
147
|
-
|
255
|
+
{
|
256
|
+
"percentage": 100,
|
257
|
+
"segmentType": "alwaysExperiment",
|
258
|
+
"constraint": "any",
|
259
|
+
"userConstraints": [
|
260
|
+
{
|
261
|
+
"userParam": "isBetaUser",
|
262
|
+
"operator": "doesNotContain",
|
263
|
+
"values": "fal",
|
264
|
+
},
|
265
|
+
{
|
266
|
+
"userParam": "isDefinitelyBetaUser",
|
267
|
+
"operator": "doesNotContain",
|
268
|
+
"values": "fal",
|
269
|
+
},
|
270
|
+
],
|
271
|
+
},
|
272
|
+
{
|
273
|
+
"constraint": "all",
|
274
|
+
"percentage": 100,
|
275
|
+
"segmentType": "everyoneElse",
|
276
|
+
"userConstraints": [],
|
277
|
+
},
|
278
|
+
],
|
279
|
+
},
|
280
|
+
],
|
148
281
|
},
|
149
282
|
}
|
150
283
|
|
151
284
|
responseD = {
|
152
285
|
"data": {
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
286
|
+
"features": [
|
287
|
+
{
|
288
|
+
"id": "1",
|
289
|
+
"active": true,
|
290
|
+
"description": "foo",
|
291
|
+
"key": "FOO_TEST",
|
292
|
+
"segments": [],
|
293
|
+
},
|
294
|
+
{
|
295
|
+
"id": "2",
|
296
|
+
"active": false,
|
297
|
+
"description": "foo",
|
298
|
+
"key": "FOO_FALSE_TEST",
|
299
|
+
"segments": [],
|
300
|
+
},
|
301
|
+
{
|
302
|
+
"id": "3",
|
303
|
+
"active": true,
|
304
|
+
"description": "foo",
|
305
|
+
"key": "FOO_50_PERCENT_TEST",
|
306
|
+
"segments": [
|
161
307
|
{
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
"segments": [],
|
308
|
+
"constraint": "all",
|
309
|
+
"segmentType": "everyoneElse",
|
310
|
+
"percentage": 50,
|
311
|
+
"userConstraints": [],
|
167
312
|
},
|
313
|
+
],
|
314
|
+
},
|
315
|
+
{
|
316
|
+
"id": "4",
|
317
|
+
"active": true,
|
318
|
+
"description": "foo",
|
319
|
+
"key": "FOO_0_PERCENT_TEST",
|
320
|
+
"segments": [
|
168
321
|
{
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
"segments": [
|
174
|
-
{
|
175
|
-
"constraint": "all",
|
176
|
-
"segmentType": "everyoneElse",
|
177
|
-
"percentage": 50,
|
178
|
-
"userConstraints": [],
|
179
|
-
},
|
180
|
-
],
|
322
|
+
"constraint": "all",
|
323
|
+
"segmentType": "everyoneElse",
|
324
|
+
"percentage": 0,
|
325
|
+
"userConstraints": [],
|
181
326
|
},
|
327
|
+
],
|
328
|
+
},
|
329
|
+
{
|
330
|
+
"id": "5",
|
331
|
+
"active": true,
|
332
|
+
"description": "foo",
|
333
|
+
"key": "FOO_ID_TEST",
|
334
|
+
"segments": [
|
182
335
|
{
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
},
|
194
|
-
],
|
336
|
+
"constraint": "all",
|
337
|
+
"percentage": 100,
|
338
|
+
"segmentType": "alwaysControl",
|
339
|
+
"userConstraints": [
|
340
|
+
{
|
341
|
+
"userParam": "id",
|
342
|
+
"operator": "equals",
|
343
|
+
"values": "123",
|
344
|
+
},
|
345
|
+
],
|
195
346
|
},
|
196
347
|
{
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
"segments": [
|
202
|
-
{
|
203
|
-
"constraint": "all",
|
204
|
-
"percentage": 100,
|
205
|
-
"segmentType": "alwaysControl",
|
206
|
-
"userConstraints": [
|
207
|
-
{
|
208
|
-
"userParam": "id",
|
209
|
-
"operator": "equals",
|
210
|
-
"values": "123",
|
211
|
-
},
|
212
|
-
],
|
213
|
-
},
|
214
|
-
{
|
215
|
-
"constraint": "all",
|
216
|
-
"segmentType": "everyoneElse",
|
217
|
-
"percentage": 100,
|
218
|
-
"userConstraints": [],
|
219
|
-
},
|
220
|
-
],
|
348
|
+
"constraint": "all",
|
349
|
+
"segmentType": "everyoneElse",
|
350
|
+
"percentage": 100,
|
351
|
+
"userConstraints": [],
|
221
352
|
},
|
222
|
-
|
353
|
+
],
|
354
|
+
},
|
355
|
+
],
|
223
356
|
},
|
224
357
|
}
|
225
358
|
|
@@ -227,53 +360,229 @@ $client = nil
|
|
227
360
|
$conn = nil
|
228
361
|
$stubs = nil
|
229
362
|
RSpec.describe Molasses::Client do
|
230
|
-
let(:stubs)
|
231
|
-
let(:conn)
|
232
|
-
let(:client) { Molasses::Client.new("test_api_key"
|
363
|
+
let(:stubs) { Faraday::Adapter::Test::Stubs.new }
|
364
|
+
let(:conn) { Faraday.new { |b| b.adapter(:test, stubs) } }
|
365
|
+
let(:client) { Molasses::Client.new("test_api_key") }
|
233
366
|
it "can do basic molasses" do
|
234
|
-
stub_request(:get,
|
235
|
-
to_return(body:responseA.to_json, headers:
|
236
|
-
|
237
|
-
|
367
|
+
stub_request(:get, "https://sdk.molasses.app/v1/features").
|
368
|
+
to_return(body: responseA.to_json, headers: {
|
369
|
+
"Content-Type" => "application/json",
|
370
|
+
})
|
238
371
|
expect(client.is_active("FOO_TEST")).to be_truthy
|
239
|
-
expect(client.is_active("FOO_TEST", {"foo"=>"foo"})).to be_truthy
|
372
|
+
expect(client.is_active("FOO_TEST", { "foo" => "foo" })).to be_truthy
|
240
373
|
expect(client.is_active("NOT_CHECKOUT")).to be_falsy
|
241
|
-
expect(client.is_active("FOO_TEST", {"id"=>"foo", "params"=>{}})).to be_truthy
|
242
|
-
expect(client.is_active("FOO_TEST", {"id"=>"food", "params"=>{
|
243
|
-
|
244
|
-
|
245
|
-
|
374
|
+
expect(client.is_active("FOO_TEST", { "id" => "foo", "params" => {} })).to be_truthy
|
375
|
+
expect(client.is_active("FOO_TEST", { "id" => "food", "params" => {
|
376
|
+
"isScaredUser" => "true",
|
377
|
+
} })).to be_falsy
|
378
|
+
expect(client.is_active("FOO_TEST", { "id" => "foodie", "params" => {
|
379
|
+
"isBetaUser" => "true",
|
380
|
+
} })).to be_truthy
|
246
381
|
end
|
247
382
|
it "can do advanced molasses" do
|
248
|
-
stub_request(:get,
|
249
|
-
to_return(body:responseB.to_json, headers:
|
250
|
-
|
251
|
-
|
383
|
+
stub_request(:get, "https://sdk.molasses.app/v1/features").
|
384
|
+
to_return(body: responseB.to_json, headers: {
|
385
|
+
"Content-Type" => "application/json",
|
386
|
+
})
|
252
387
|
|
253
388
|
expect(client.is_active("FOO_TEST")).to be_truthy
|
254
|
-
expect(client.is_active("FOO_TEST", {"foo"=>"foo"})).to be_truthy
|
389
|
+
expect(client.is_active("FOO_TEST", { "foo" => "foo" })).to be_truthy
|
255
390
|
expect(client.is_active("NOT_CHECKOUT")).to be_falsy
|
256
|
-
expect(client.is_active("FOO_TEST", {"id"=>"foo", "params"=>{
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
391
|
+
expect(client.is_active("FOO_TEST", { "id" => "foo", "params" => {
|
392
|
+
"isBetaUser" => "false", "isScaredUser" => "false",
|
393
|
+
} })).to be_truthy
|
394
|
+
expect(client.is_active("FOO_TEST", { "id" => "food", "params" => {
|
395
|
+
"isScaredUser" => "true",
|
396
|
+
} })).to be_falsy
|
397
|
+
expect(client.is_active("FOO_TEST", { "id" => "foodie", "params" => {
|
398
|
+
"isBetaUser" => "true",
|
399
|
+
} })).to be_truthy
|
262
400
|
end
|
263
401
|
it "can do even more advanced molasses" do
|
264
|
-
stub_request(:get,
|
265
|
-
to_return(body:responseC.to_json, headers:
|
266
|
-
|
267
|
-
|
402
|
+
stub_request(:get, "https://sdk.molasses.app/v1/features").
|
403
|
+
to_return(body: responseC.to_json, headers: {
|
404
|
+
"Content-Type" => "application/json",
|
405
|
+
})
|
406
|
+
stub_request(:post, "https://sdk.molasses.app/v1/analytics").
|
407
|
+
to_return(status: 200, body: "", headers: {})
|
408
|
+
expect(client.is_active("FOO_TEST", { "id" => "foo", "params" => {
|
409
|
+
"isScaredUser" => "scared",
|
410
|
+
"isDefinitelyScaredUser" => "scared",
|
411
|
+
"isMostDefinitelyScaredUser" => "scared",
|
412
|
+
} })).to be_falsy
|
413
|
+
expect(client.is_active("FOO_TEST", { "id" => "food", "params" => {
|
414
|
+
"isDefinitelyBetaUser" => "true", "isBetaUser" => "true",
|
415
|
+
} })).to be_truthy
|
416
|
+
expect(client.is_active("FOO_TEST", { "id" => "foodie", "params" => {
|
417
|
+
"isBetaUser" => "true",
|
418
|
+
} })).to be_truthy
|
419
|
+
|
420
|
+
expect(client.is_active("NUMBERS_BOOLS", {
|
421
|
+
"id" => "12341",
|
422
|
+
"params" => {
|
423
|
+
"lt" => true,
|
424
|
+
"lte" => "12",
|
425
|
+
"gt" => 14,
|
426
|
+
"gte" => 12,
|
427
|
+
"equals" => 12,
|
428
|
+
"doesNotEqual" => false,
|
429
|
+
"equalsBool" => true,
|
430
|
+
"doesNotEqualBool" => "true",
|
431
|
+
},
|
432
|
+
})).to be_falsy
|
433
|
+
|
434
|
+
expect(client.is_active("semver", {
|
435
|
+
"id" => "123444", # valid crc32 percentage
|
436
|
+
"params" => {
|
437
|
+
"lt" => "1.1.0",
|
438
|
+
},
|
439
|
+
})).to be_truthy
|
440
|
+
|
441
|
+
expect(client.is_active("semver", {
|
442
|
+
"id" => "123444", # valid crc32 percentage
|
443
|
+
"params" => {
|
444
|
+
"lt" => "1.2.0",
|
445
|
+
},
|
446
|
+
})).to be_falsy
|
447
|
+
|
448
|
+
expect(client.is_active("semver", {
|
449
|
+
"id" => "123444", # valid crc32 percentage
|
450
|
+
"params" => {
|
451
|
+
"gt" => "1.3.0",
|
452
|
+
},
|
453
|
+
})).to be_truthy
|
454
|
+
|
455
|
+
expect(client.is_active("semver", {
|
456
|
+
"id" => "123444", # valid crc32 percentage
|
457
|
+
"params" => {
|
458
|
+
"gt" => "1.2.0",
|
459
|
+
},
|
460
|
+
})).to be_falsy
|
461
|
+
|
462
|
+
expect(client.is_active("semver", {
|
463
|
+
"id" => "123444", # valid crc32 percentage
|
464
|
+
"params" => {
|
465
|
+
"lte" => "1.1.0",
|
466
|
+
},
|
467
|
+
})).to be_truthy
|
468
|
+
|
469
|
+
expect(client.is_active("semver", {
|
470
|
+
"id" => "123444", # valid crc32 percentage
|
471
|
+
"params" => {
|
472
|
+
"lte" => "1.2.0",
|
473
|
+
},
|
474
|
+
})).to be_truthy
|
475
|
+
|
476
|
+
expect(client.is_active("semver", {
|
477
|
+
"id" => "123444", # valid crc32 percentage
|
478
|
+
"params" => {
|
479
|
+
"lte" => "1.3.0",
|
480
|
+
},
|
481
|
+
})).to be_falsy
|
482
|
+
|
483
|
+
expect(client.is_active("semver", {
|
484
|
+
"id" => "123444", # valid crc32 percentage
|
485
|
+
"params" => {
|
486
|
+
"gte" => "1.3.0",
|
487
|
+
},
|
488
|
+
})).to be_truthy
|
489
|
+
|
490
|
+
expect(client.is_active("semver", {
|
491
|
+
"id" => "123444", # valid crc32 percentage
|
492
|
+
"params" => {
|
493
|
+
"gte" => "1.2.0",
|
494
|
+
},
|
495
|
+
})).to be_truthy
|
496
|
+
|
497
|
+
expect(client.is_active("semver", {
|
498
|
+
"id" => "123444", # valid crc32 percentage
|
499
|
+
"params" => {
|
500
|
+
"gte" => "0.1.0",
|
501
|
+
},
|
502
|
+
})).to be_falsy
|
503
|
+
|
504
|
+
expect(client.is_active("semver", {
|
505
|
+
"id" => "123444", # valid crc32 percentage
|
506
|
+
"params" => {
|
507
|
+
"equals" => "1.2.0",
|
508
|
+
},
|
509
|
+
})).to be_truthy
|
510
|
+
|
511
|
+
expect(client.is_active("semver", {
|
512
|
+
"id" => "123444", # valid crc32 percentage
|
513
|
+
"params" => {
|
514
|
+
"equals" => "1.3.0",
|
515
|
+
},
|
516
|
+
})).to be_falsy
|
517
|
+
|
518
|
+
expect(client.is_active("semver", {
|
519
|
+
"id" => "123444", # valid crc32 percentage
|
520
|
+
"params" => {
|
521
|
+
"doesNotEqual" => "1.3.0",
|
522
|
+
},
|
523
|
+
})).to be_truthy
|
524
|
+
|
525
|
+
expect(client.is_active("semver", {
|
526
|
+
"id" => "123444", # valid crc32 percentage
|
527
|
+
"params" => {
|
528
|
+
"doesNotEqual" => "1.2.0",
|
529
|
+
},
|
530
|
+
})).to be_falsy
|
268
531
|
|
269
|
-
expect(client.is_active("
|
270
|
-
|
271
|
-
|
272
|
-
"
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
532
|
+
expect(client.is_active("NUMBERS_BOOLS", {
|
533
|
+
"id" => "123444", # valid crc32 percentage
|
534
|
+
"params" => {
|
535
|
+
"lt" => true,
|
536
|
+
"lte" => "12",
|
537
|
+
"gt" => 14,
|
538
|
+
"gte" => 12,
|
539
|
+
"equals" => 12,
|
540
|
+
"doesNotEqual" => false,
|
541
|
+
"equalsBool" => 0,
|
542
|
+
"doesNotEqualBool" => "true",
|
543
|
+
},
|
544
|
+
})).to be_truthy
|
545
|
+
expect(client.experiment_started("NUMBERS_BOOLS", {
|
546
|
+
"id" => "123444", # valid crc32 percentage
|
547
|
+
"params" => {
|
548
|
+
"lt" => true,
|
549
|
+
"lte" => "12",
|
550
|
+
"gt" => 14,
|
551
|
+
"gte" => 12,
|
552
|
+
"equals" => 12,
|
553
|
+
"doesNotEqual" => false,
|
554
|
+
"equalsBool" => 0,
|
555
|
+
"doesNotEqualBool" => "true",
|
556
|
+
},
|
557
|
+
}))
|
558
|
+
expect(client.experiment_started("Clicked button", {
|
559
|
+
"id" => "123444", # v
|
560
|
+
}))
|
561
|
+
expect(client.track("Clicked button", {
|
562
|
+
"id" => "123444", # valid crc32 percentage
|
563
|
+
"params" => {
|
564
|
+
"lt" => true,
|
565
|
+
"lte" => "12",
|
566
|
+
"gt" => 14,
|
567
|
+
"gte" => 12,
|
568
|
+
"equals" => 12,
|
569
|
+
"doesNotEqual" => false,
|
570
|
+
"equalsBool" => 0,
|
571
|
+
"doesNotEqualBool" => "true",
|
572
|
+
},
|
573
|
+
}))
|
574
|
+
expect(client.experiment_success("NUMBERS_BOOLS", {
|
575
|
+
"id" => "123444", # valid crc32 percentage
|
576
|
+
"params" => {
|
577
|
+
"lt" => true,
|
578
|
+
"lte" => "12",
|
579
|
+
"gt" => 14,
|
580
|
+
"gte" => 12,
|
581
|
+
"equals" => 12,
|
582
|
+
"doesNotEqual" => false,
|
583
|
+
"equalsBool" => 0,
|
584
|
+
"doesNotEqualBool" => "true",
|
585
|
+
},
|
586
|
+
}))
|
278
587
|
end
|
279
|
-
end
|
588
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: molasses
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.4.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- James Hrisho
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2021-07-10 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: faraday
|
@@ -38,6 +38,20 @@ dependencies:
|
|
38
38
|
- - "~>"
|
39
39
|
- !ruby/object:Gem::Version
|
40
40
|
version: 0.6.1
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: semantic
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: 1.6.1
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: 1.6.1
|
41
55
|
description: Ruby SDK for Molasses. Feature flags as a service
|
42
56
|
email: james.hrisho@gmail.com
|
43
57
|
executables: []
|
@@ -67,7 +81,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
67
81
|
- !ruby/object:Gem::Version
|
68
82
|
version: '0'
|
69
83
|
requirements: []
|
70
|
-
rubygems_version: 3.0.3
|
84
|
+
rubygems_version: 3.0.3.1
|
71
85
|
signing_key:
|
72
86
|
specification_version: 4
|
73
87
|
summary: Ruby SDK for Molasses. Feature flags as a service
|