molasses 0.1.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (5) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +37 -7
  3. data/lib/molasses.rb +154 -52
  4. data/spec/molasses_spec.rb +541 -232
  5. metadata +33 -5
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 65c1760e6753f23f0b5029971c67d0bd637ec58abe376d8a1a4f151eaa05bb3c
4
- data.tar.gz: d1c94afb5eb0be2190b7898599375aa9395a0b986d0c3281e497b9243dfed927
3
+ metadata.gz: d508ad9b8da82f0847404e2952c7da584498472b479946cf493f5f30e8c5bff3
4
+ data.tar.gz: d72c43aac2273436539a840194c2725528a5752fb724cfddecbd86c7b23ac121
5
5
  SHA512:
6
- metadata.gz: 88459445d1b41d504aba89102b05d602587fde046c79a92c305e80097da3601c97fae412c8c2f394a68324aaacc887ce43cd9b5361e0b4d6f93466f1cf917b2d
7
- data.tar.gz: 364dbb84b69da102d920599d2de04a4371a1e6935b9f8968bc2126b669a25b05294b5db4748f65f28650ad1307717732c4b699ced27ab41a86b7f7f9018f2ef7
6
+ metadata.gz: 1153ff848c5a16bb18dde2fe87099310838a69800a9406dbdacf000e84c5067d6e2d573e0cb55830b7c37284115a534b206aeaf95f179a103026fe2998154599
7
+ data.tar.gz: 43a7de947daac266594e545afca20df5ee3bf9e51b1d059a28d67b270257ac1d461725a275627818bc8b0c9275cfe2784cfdef69168ffbca3ce2f900397140ae
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 not to track analytics events (experiment started, experiment success) you can turn them off by setting the `send_events` field to `false`
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", false)
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 whether an experiment was successful you can call `experiment_success`. experiment_success takes the feature's name, any additional parameters for the event and the user.
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.experiment_success("GOOGLE_SSO",{
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 'workers'
2
- require 'faraday'
3
- require 'json'
4
- require 'zlib'
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, send_events=true, base_url="")
10
+ def initialize(api_key, opts = {})
8
11
  @api_key = api_key
9
- @send_events = send_events
10
- if base_url != ""
11
- @base_url = base_url
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
- 'Content-Type' => 'application/json',
19
- 'Authorization' => "Bearer #{@api_key}"
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,20 +38,76 @@ 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
53
55
 
56
+ def stop
57
+ @timer.cancel
58
+ end
59
+
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
67
+ end
68
+ feature = @feature_cache[key]
69
+ result = is_active(feature, user)
70
+ send_event({
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"],
108
+ })
109
+ end
110
+
54
111
  private
55
112
 
56
113
  def user_active(feature, user)
@@ -59,13 +116,13 @@ module Molasses
59
116
  end
60
117
 
61
118
  unless user && user.include?("id")
62
- return true
119
+ return true
63
120
  end
64
121
 
65
122
  segment_map = {}
66
123
  for feature_segment in feature["segments"]
67
- segment_type = feature_segment["segmentType"]
68
- segment_map[segment_type] = feature_segment
124
+ segment_type = feature_segment["segmentType"]
125
+ segment_map[segment_type] = feature_segment
69
126
  end
70
127
 
71
128
  if segment_map.include?("alwaysControl") and in_segment(user, segment_map["alwaysControl"])
@@ -78,7 +135,7 @@ module Molasses
78
135
  return user_percentage(user["id"], segment_map["everyoneElse"]["percentage"])
79
136
  end
80
137
 
81
- def user_percentage(id="", percentage = 0)
138
+ def user_percentage(id = "", percentage = 0)
82
139
  if percentage == 100
83
140
  return true
84
141
  end
@@ -95,7 +152,7 @@ module Molasses
95
152
  def in_segment(user, segment_map)
96
153
  user_constraints = segment_map["userConstraints"]
97
154
  constraints_length = user_constraints.length
98
- constraints_to_be_met = segment_map["constraint"] == 'any' ? 1 : constraints_length
155
+ constraints_to_be_met = segment_map["constraint"] == "any" ? 1 : constraints_length
99
156
  constraints_met = 0
100
157
 
101
158
  for constraint in user_constraints
@@ -118,65 +175,110 @@ module Molasses
118
175
  return constraints_met >= constraints_to_be_met
119
176
  end
120
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
+
121
202
  def meets_constraint(user_value, param_exists, constraint)
122
203
  operator = constraint["operator"]
123
204
  if param_exists == false
124
205
  return false
125
206
  end
126
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
+
127
223
  case operator
128
224
  when "in"
129
- list_values = constraint["values"].split(',')
225
+ list_values = constraint_value.split(",")
130
226
  return list_values.include? user_value
131
227
  when "nin"
132
- list_values = constraint["values"].split(',')
228
+ list_values = constraint_value.split(",")
133
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
134
238
  when "equals"
135
- return user_value == constraint["values"]
239
+ return user_value == constraint_value
136
240
  when "doesNotEqual"
137
- return user_value != constraint["values"]
241
+ return user_value != constraint_value
138
242
  when "contains"
139
- return constraint["values"].include?(user_value)
243
+ return constraint_value.include?(user_value)
140
244
  when "doesNotContain"
141
- return !constraint["values"].include?(user_value)
245
+ return !constraint_value.include?(user_value)
142
246
  else
143
247
  return false
144
248
  end
145
249
  end
146
- def experiment_success(key, additional_details={}, user=nil)
147
- if !@initialized || !@send_events || user == nil || user.include?("id")
148
- return false
149
- end
150
- feature = @feature_cache[key]
151
- result = is_active(feature, user)
152
- send_event({
153
- "event"=> "experiment_success",
154
- "tags"=> user["params"].merge(additional_details),
155
- "userId"=> user["id"],
156
- "featureId"=> feature["id"],
157
- "featureName"=> key,
158
- "testType"=> result ? "experiment" : "control"
159
- })
160
- end
250
+
161
251
  def send_event(event_options)
162
- @conn.post('analytics', event_options.to_json)
252
+ @conn.post("analytics", event_options.to_json)
163
253
  end
164
254
 
165
255
  def fetch_features()
166
- response = @conn.get('get-features')
256
+ response = @conn.get("features")
167
257
  if response.status == 200
168
258
  data = JSON.parse(response.body)
169
259
  if data.include?("data") and data["data"].include?("features")
170
260
  features = data["data"]["features"]
171
- for feature in features do
261
+ for feature in features
172
262
  @feature_cache[feature["key"]] = feature
173
263
  end
264
+ unless @initialized
265
+ @logger.info "Molasses - connected and initialized"
266
+ end
174
267
  @initialized = true
175
268
  end
176
269
  else
177
- puts "Molasses - #{response.status} - #{response.body}"
270
+ @logger.info "Molasses - #{response.status} - #{response.body}"
178
271
  end
179
272
  end
180
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
181
283
  end
182
- end
284
+ end
@@ -1,225 +1,358 @@
1
- require 'faraday'
2
- require 'molasses'
3
- require 'webmock/rspec'
1
+ require "simplecov"
2
+ SimpleCov.start
3
+ require "faraday"
4
+ require "molasses"
5
+ require "webmock/rspec"
4
6
  responseA = {
5
7
  "data": {
6
- "features": [
8
+ "features": [
9
+ {
10
+ "active": true,
11
+ "description": "foo",
12
+ "key": "FOO_TEST",
13
+ "segments": [
7
14
  {
8
- "active": true,
9
- "description": "foo",
10
- "key": "FOO_TEST",
11
- "segments": [
12
- {
13
- "constraint": "all",
14
- "percentage": 100,
15
- "segmentType": "alwaysControl",
16
- "userConstraints": [
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
- "features": [
51
+ "features": [
52
+ {
53
+ "id": "1",
54
+ "active": true,
55
+ "description": "foo",
56
+ "key": "FOO_TEST",
57
+ "segments": [
49
58
  {
50
- "id": "1",
51
- "active": true,
52
- "description": "foo",
53
- "key": "FOO_TEST",
54
- "segments": [
55
- {
56
- "constraint": "all",
57
- "percentage": 100,
58
- "segmentType": "alwaysControl",
59
- "userConstraints": [
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
- "features": [
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
- "id": "1",
96
- "active": true,
97
- "description": "foo",
98
- "key": "FOO_TEST",
99
- "segments": [
100
- {
101
- "percentage": 100,
102
- "segmentType": "alwaysControl",
103
- "constraint": "all",
104
- "userConstraints": [
105
- {
106
- "userParam": "isScaredUser",
107
- "operator": "contains",
108
- "values": "scared",
109
- },
110
- {
111
- "userParam": "isDefinitelyScaredUser",
112
- "operator": "contains",
113
- "values": "scared",
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
- "features": [
154
- {
155
- "id": "1",
156
- "active": true,
157
- "description": "foo",
158
- "key": "FOO_TEST",
159
- "segments": [],
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
- "id": "2",
163
- "active": false,
164
- "description": "foo",
165
- "key": "FOO_FALSE_TEST",
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
- "id": "3",
170
- "active": true,
171
- "description": "foo",
172
- "key": "FOO_50_PERCENT_TEST",
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
- "id": "4",
184
- "active": true,
185
- "description": "foo",
186
- "key": "FOO_0_PERCENT_TEST",
187
- "segments": [
188
- {
189
- "constraint": "all",
190
- "segmentType": "everyoneElse",
191
- "percentage": 0,
192
- "userConstraints": [],
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
- "id": "5",
198
- "active": true,
199
- "description": "foo",
200
- "key": "FOO_ID_TEST",
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) { Faraday::Adapter::Test::Stubs.new }
231
- let(:conn) { Faraday.new { |b| b.adapter(:test, stubs) } }
232
- let(:client) { Molasses::Client.new("test_api_key", false) }
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, 'https://us-central1-molasses-36bff.cloudfunctions.net/get-features').
235
- to_return(body:responseA.to_json, headers: {
236
- 'Content-Type'=>'application/json',
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
- "isScaredUser"=>"true"}})).to be_falsy
244
- expect(client.is_active("FOO_TEST", {"id"=>"foodie", "params"=>{
245
- "isBetaUser"=>"true"}})).to be_truthy
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, 'https://us-central1-molasses-36bff.cloudfunctions.net/get-features').
249
- to_return(body:responseB.to_json, headers: {
250
- 'Content-Type'=>'application/json',
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
- "isBetaUser"=>"false", "isScaredUser"=>"false"}})).to be_truthy
258
- expect(client.is_active("FOO_TEST", {"id"=>"food", "params"=>{
259
- "isScaredUser"=>"true"}})).to be_falsy
260
- expect(client.is_active("FOO_TEST", {"id"=>"foodie", "params"=>{
261
- "isBetaUser"=>"true"}})).to be_truthy
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, 'https://us-central1-molasses-36bff.cloudfunctions.net/get-features').
265
- to_return(body:responseC.to_json, headers: {
266
- 'Content-Type'=>'application/json',
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("FOO_TEST", {"id"=> "foo", "params" => {
270
- "isScaredUser"=> "scared",
271
- "isDefinitelyScaredUser"=> "scared",
272
- "isMostDefinitelyScaredUser"=> "scared",
273
- }})).to be_falsy
274
- expect(client.is_active("FOO_TEST", {"id"=>"food", "params"=>{
275
- "isDefinitelyBetaUser"=>"true", "isBetaUser"=>"true"}})).to be_truthy
276
- expect(client.is_active("FOO_TEST", {"id"=>"foodie", "params"=>{
277
- "isBetaUser"=>"true"}})).to be_truthy
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,29 +1,57 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: molasses
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.4.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - James Hrisho
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-09-30 00:00:00.000000000 Z
11
+ date: 2021-07-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: faraday
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
- - - ">="
17
+ - - "~>"
18
18
  - !ruby/object:Gem::Version
19
19
  version: '1.0'
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
- - - ">="
24
+ - - "~>"
25
25
  - !ruby/object:Gem::Version
26
26
  version: '1.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: workers
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: 0.6.1
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
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
27
55
  description: Ruby SDK for Molasses. Feature flags as a service
28
56
  email: james.hrisho@gmail.com
29
57
  executables: []
@@ -53,7 +81,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
53
81
  - !ruby/object:Gem::Version
54
82
  version: '0'
55
83
  requirements: []
56
- rubygems_version: 3.0.3
84
+ rubygems_version: 3.0.3.1
57
85
  signing_key:
58
86
  specification_version: 4
59
87
  summary: Ruby SDK for Molasses. Feature flags as a service