setbit 0.1.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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +34 -0
- data/LICENSE +21 -0
- data/README.md +577 -0
- data/lib/setbit/client.rb +190 -0
- data/lib/setbit/errors.rb +12 -0
- data/lib/setbit/utils.rb +41 -0
- data/lib/setbit/version.rb +5 -0
- data/lib/setbit.rb +22 -0
- data/setbit.gemspec +44 -0
- metadata +128 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 9b8ccfb4587b58fc04c79c6557dafd02f05914742c58bd352e2e3786065b0446
|
|
4
|
+
data.tar.gz: cbee38616ee38877db945334e4ec36d2cd8a2c790880a52655600b37a8c523a9
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: aefc7d300599e61ac61e4591b6111eabc913fa9253d18f6d2f6d1681a99f6b638929d82ac166da9ef1749960c399accecd917ace25336895e72156374ebcd41b
|
|
7
|
+
data.tar.gz: 0bb36dac20e88bd51226a386755cffec66358b4a4f86deac5daa4275c1ab9fb750693263785cd8a75463f2f63742070eba4da7a7c7b70f9d0c48d9555d61b1c5
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to the SetBit Ruby SDK will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [0.1.0] - 2025-11-23
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- Initial release of SetBit Ruby SDK
|
|
12
|
+
- Boolean flag checking with `enabled?` method
|
|
13
|
+
- A/B test variant selection with `variant` method
|
|
14
|
+
- Conversion tracking with `track` method
|
|
15
|
+
- Tag-based targeting support
|
|
16
|
+
- Manual flag refresh with `refresh` method
|
|
17
|
+
- Comprehensive error handling with custom exceptions
|
|
18
|
+
- Fail-open design - returns defaults on errors
|
|
19
|
+
- Full test coverage with RSpec
|
|
20
|
+
- Rails and Sinatra integration examples
|
|
21
|
+
- Zero external runtime dependencies
|
|
22
|
+
|
|
23
|
+
### Features
|
|
24
|
+
- Boolean flag checking with consistent behavior
|
|
25
|
+
- Rollout flags with percentage-based gradual rollout (requires user_id)
|
|
26
|
+
- Weighted random distribution for A/B tests
|
|
27
|
+
- In-memory flag caching
|
|
28
|
+
- Silent failure for tracking (logs errors but doesn't crash)
|
|
29
|
+
- Support for custom base URL (self-hosted instances)
|
|
30
|
+
- Consistent user assignment using SHA-256 hashing
|
|
31
|
+
- Idiomatic Ruby API with proper naming conventions
|
|
32
|
+
- Thread-safe operations
|
|
33
|
+
|
|
34
|
+
[0.1.0]: https://github.com/setbit/setbit-ruby/releases/tag/v0.1.0
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 SetBit
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,577 @@
|
|
|
1
|
+
# SetBit Ruby SDK
|
|
2
|
+
|
|
3
|
+
Official Ruby SDK for [SetBit](https://setbit.io) - Simple feature flags and A/B testing.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- โ
**Boolean Flags** - Simple on/off feature toggles
|
|
8
|
+
- ๐งช **A/B Testing** - Weighted variant distribution for experiments
|
|
9
|
+
- ๐ **Conversion Tracking** - Track events and conversions
|
|
10
|
+
- ๐ท๏ธ **Tag-Based Targeting** - Target by environment, app, team, region, etc.
|
|
11
|
+
- ๐ **Fail-Open Design** - Returns defaults if API is unreachable
|
|
12
|
+
- ๐ชถ **Zero Dependencies** - Uses only Ruby standard library
|
|
13
|
+
- ๐ **Idiomatic Ruby** - Follows Ruby conventions and best practices
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
Add this line to your application's Gemfile:
|
|
18
|
+
|
|
19
|
+
```ruby
|
|
20
|
+
gem 'setbit'
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
And then execute:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
bundle install
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Or install it yourself as:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
gem install setbit
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Quick Start
|
|
36
|
+
|
|
37
|
+
```ruby
|
|
38
|
+
require 'setbit'
|
|
39
|
+
|
|
40
|
+
# Initialize the client
|
|
41
|
+
client = SetBit::Client.new(
|
|
42
|
+
api_key: "pk_your_api_key_here",
|
|
43
|
+
tags: { env: "production", app: "web" }
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
# Check a boolean flag (user_id required for analytics)
|
|
47
|
+
user_id = get_current_user_id
|
|
48
|
+
if client.enabled?("new-checkout", user_id: user_id)
|
|
49
|
+
show_new_checkout
|
|
50
|
+
else
|
|
51
|
+
show_old_checkout
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Rollout flag (gradual percentage-based rollout)
|
|
55
|
+
variant = client.variant("new-api", user_id: current_user.id)
|
|
56
|
+
if variant == "enabled"
|
|
57
|
+
use_new_api
|
|
58
|
+
else
|
|
59
|
+
use_old_api
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Get A/B test variant
|
|
63
|
+
variant = client.variant("pricing-experiment", user_id: current_user.id)
|
|
64
|
+
case variant
|
|
65
|
+
when "variant_a"
|
|
66
|
+
show_price_99
|
|
67
|
+
when "variant_b"
|
|
68
|
+
show_price_149
|
|
69
|
+
else # control
|
|
70
|
+
show_price_129
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Track conversions
|
|
74
|
+
client.track("purchase", user_id: current_user.id, metadata: { amount: 99.99 })
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## API Reference
|
|
78
|
+
|
|
79
|
+
### Initialization
|
|
80
|
+
|
|
81
|
+
```ruby
|
|
82
|
+
SetBit::Client.new(api_key:, tags: {}, base_url: "https://flags.setbit.io", logger: nil)
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
**Parameters:**
|
|
86
|
+
- `api_key` (String, required): Your SetBit API key
|
|
87
|
+
- `tags` (Hash, optional): Tags for targeting flags (e.g., `{ env: "production", app: "web" }`)
|
|
88
|
+
- `base_url` (String, optional): API endpoint URL (useful for self-hosted instances)
|
|
89
|
+
- `logger` (Logger, optional): Custom logger instance
|
|
90
|
+
|
|
91
|
+
**Raises:**
|
|
92
|
+
- `SetBit::AuthError`: If API key is invalid
|
|
93
|
+
- `SetBit::APIError`: If initial flag fetch fails
|
|
94
|
+
|
|
95
|
+
**Example:**
|
|
96
|
+
```ruby
|
|
97
|
+
client = SetBit::Client.new(
|
|
98
|
+
api_key: "pk_abc123",
|
|
99
|
+
tags: { env: "production", app: "web", region: "us-east" }
|
|
100
|
+
)
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
### `enabled?(flag_name, user_id:, default: false)`
|
|
106
|
+
|
|
107
|
+
Check if a flag is enabled. Returns `true` if the flag is globally enabled, `false` otherwise.
|
|
108
|
+
|
|
109
|
+
**Parameters:**
|
|
110
|
+
- `flag_name` (String): Name of the flag
|
|
111
|
+
- `user_id` (String, **required**): User identifier (required for analytics and billing)
|
|
112
|
+
- `default` (Boolean): Value to return if flag not found (default: `false`)
|
|
113
|
+
|
|
114
|
+
**Returns:** `Boolean` - `true` if enabled, `false` otherwise
|
|
115
|
+
|
|
116
|
+
**Note:** This method returns whether the flag is globally enabled. For rollout flags, use `variant()` to check which rollout group the user is in.
|
|
117
|
+
|
|
118
|
+
**Example:**
|
|
119
|
+
```ruby
|
|
120
|
+
# Check if flag is enabled (user_id required)
|
|
121
|
+
user_id = get_current_user_id
|
|
122
|
+
if client.enabled?("new-dashboard", user_id: user_id)
|
|
123
|
+
render_new_dashboard
|
|
124
|
+
else
|
|
125
|
+
render_old_dashboard
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# With custom default
|
|
129
|
+
if client.enabled?("beta-feature", user_id: user_id, default: true)
|
|
130
|
+
show_beta_feature
|
|
131
|
+
end
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
---
|
|
135
|
+
|
|
136
|
+
### `variant(flag_name, user_id:, default: "control")`
|
|
137
|
+
|
|
138
|
+
Get the variant for an A/B test experiment or rollout flag.
|
|
139
|
+
|
|
140
|
+
**Parameters:**
|
|
141
|
+
- `flag_name` (String): Name of the experiment or rollout flag
|
|
142
|
+
- `user_id` (String): User identifier (required)
|
|
143
|
+
- `default` (String): Variant to return if flag not found (default: `"control"`)
|
|
144
|
+
|
|
145
|
+
**Returns:** `String` - Variant name
|
|
146
|
+
|
|
147
|
+
For experiments: `"control"`, `"variant_a"`, `"variant_b"`, etc.
|
|
148
|
+
For rollout flags: `"enabled"` (user in rollout) or `"disabled"` (user not in rollout)
|
|
149
|
+
|
|
150
|
+
**Example:**
|
|
151
|
+
```ruby
|
|
152
|
+
# A/B test experiment
|
|
153
|
+
variant = client.variant("button-color-test", user_id: current_user.id)
|
|
154
|
+
|
|
155
|
+
button_color = case variant
|
|
156
|
+
when "variant_a" then "blue"
|
|
157
|
+
when "variant_b" then "green"
|
|
158
|
+
else "red" # control
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# Rollout flag (gradual percentage-based rollout)
|
|
162
|
+
variant = client.variant("new-api", user_id: current_user.id)
|
|
163
|
+
|
|
164
|
+
if variant == "enabled"
|
|
165
|
+
use_new_api # User is in the rollout group
|
|
166
|
+
else
|
|
167
|
+
use_old_api # User is not in the rollout group
|
|
168
|
+
end
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
---
|
|
172
|
+
|
|
173
|
+
### `track(event_name, user_id:, flag_name: nil, variant: nil, metadata: {})`
|
|
174
|
+
|
|
175
|
+
Track a conversion event.
|
|
176
|
+
|
|
177
|
+
**Parameters:**
|
|
178
|
+
- `event_name` (String): Name of the event (e.g., `"purchase"`, `"signup"`)
|
|
179
|
+
- `user_id` (String, **required**): User identifier (required for analytics and billing)
|
|
180
|
+
- `flag_name` (String, optional): Flag to associate with this conversion
|
|
181
|
+
- `variant` (String, optional): Variant the user was assigned to (for A/B test attribution)
|
|
182
|
+
- `metadata` (Hash, optional): Additional event data
|
|
183
|
+
|
|
184
|
+
**Returns:** `void`
|
|
185
|
+
|
|
186
|
+
**Note:** This method fails silently - errors are logged but not raised.
|
|
187
|
+
|
|
188
|
+
**Example:**
|
|
189
|
+
```ruby
|
|
190
|
+
user_id = current_user.id
|
|
191
|
+
|
|
192
|
+
# Track conversion with variant attribution (recommended for A/B tests)
|
|
193
|
+
variant = client.variant("pricing-test", user_id: user_id)
|
|
194
|
+
# ... later when user converts ...
|
|
195
|
+
client.track(
|
|
196
|
+
"purchase",
|
|
197
|
+
user_id: user_id,
|
|
198
|
+
flag_name: "pricing-test",
|
|
199
|
+
variant: variant,
|
|
200
|
+
metadata: { amount: 99.99, currency: "USD" }
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
# Track basic conversion
|
|
204
|
+
client.track("signup", user_id: user_id)
|
|
205
|
+
|
|
206
|
+
# Track with flag association only
|
|
207
|
+
client.track("purchase", user_id: user_id, flag_name: "checkout-experiment")
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
---
|
|
211
|
+
|
|
212
|
+
### `refresh`
|
|
213
|
+
|
|
214
|
+
Manually refresh flags from the API.
|
|
215
|
+
|
|
216
|
+
**Returns:** `void`
|
|
217
|
+
|
|
218
|
+
**Raises:**
|
|
219
|
+
- `SetBit::AuthError`: If API key is invalid
|
|
220
|
+
- `SetBit::APIError`: If API request fails
|
|
221
|
+
|
|
222
|
+
**Example:**
|
|
223
|
+
```ruby
|
|
224
|
+
# Refresh flags if you know they've changed
|
|
225
|
+
client.refresh
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
---
|
|
229
|
+
|
|
230
|
+
## Usage Examples
|
|
231
|
+
|
|
232
|
+
### Boolean Flags
|
|
233
|
+
|
|
234
|
+
```ruby
|
|
235
|
+
require 'setbit'
|
|
236
|
+
|
|
237
|
+
client = SetBit::Client.new(
|
|
238
|
+
api_key: "pk_abc123",
|
|
239
|
+
tags: { env: "production", app: "web" }
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
# Simple feature toggle (boolean flag)
|
|
243
|
+
enable_dark_mode if client.enabled?("dark-mode")
|
|
244
|
+
|
|
245
|
+
# With default value
|
|
246
|
+
debug_mode = client.enabled?("debug-logging", default: false)
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
### Rollout Flags
|
|
250
|
+
|
|
251
|
+
```ruby
|
|
252
|
+
# Gradual percentage-based rollout
|
|
253
|
+
# Use variant() to check which rollout group the user is in
|
|
254
|
+
variant = client.variant("new-api-v2", user_id: current_user.id)
|
|
255
|
+
|
|
256
|
+
if variant == "enabled"
|
|
257
|
+
use_api_v2 # User is in the rollout group
|
|
258
|
+
else
|
|
259
|
+
use_api_v1 # User is not in the rollout group
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
# Example: Rollout new checkout flow
|
|
263
|
+
checkout_variant = client.variant("new-checkout", user_id: current_user.id)
|
|
264
|
+
|
|
265
|
+
if checkout_variant == "enabled"
|
|
266
|
+
render_new_checkout
|
|
267
|
+
client.track("checkout_started", user_id: current_user.id, flag_name: "new-checkout")
|
|
268
|
+
else
|
|
269
|
+
render_old_checkout
|
|
270
|
+
end
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
### A/B Testing
|
|
274
|
+
|
|
275
|
+
```ruby
|
|
276
|
+
# Get variant for experiment
|
|
277
|
+
variant = client.variant("homepage-hero", user_id: current_user.id)
|
|
278
|
+
|
|
279
|
+
case variant
|
|
280
|
+
when "variant_a"
|
|
281
|
+
# Version A: Large hero image
|
|
282
|
+
render_hero(size: "large", style: "image")
|
|
283
|
+
when "variant_b"
|
|
284
|
+
# Version B: Video hero
|
|
285
|
+
render_hero(size: "large", style: "video")
|
|
286
|
+
when "variant_c"
|
|
287
|
+
# Version C: Minimal hero
|
|
288
|
+
render_hero(size: "small", style: "minimal")
|
|
289
|
+
else
|
|
290
|
+
# Control: Original hero
|
|
291
|
+
render_hero(size: "medium", style: "image")
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
# Track conversion for this experiment (pass variant for proper attribution)
|
|
295
|
+
client.track("signup", user_id: current_user.id, flag_name: "homepage-hero", variant: variant)
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
### Conversion Tracking
|
|
299
|
+
|
|
300
|
+
```ruby
|
|
301
|
+
# Track page view
|
|
302
|
+
client.track("page_view", metadata: { page: "/pricing" })
|
|
303
|
+
|
|
304
|
+
# Track user signup
|
|
305
|
+
client.track("signup", metadata: {
|
|
306
|
+
plan: "pro",
|
|
307
|
+
source: "landing_page"
|
|
308
|
+
})
|
|
309
|
+
|
|
310
|
+
# Track purchase with detailed metadata
|
|
311
|
+
client.track(
|
|
312
|
+
"purchase",
|
|
313
|
+
flag_name: "checkout-experiment",
|
|
314
|
+
metadata: {
|
|
315
|
+
amount: 149.99,
|
|
316
|
+
currency: "USD",
|
|
317
|
+
items: 3,
|
|
318
|
+
payment_method: "credit_card"
|
|
319
|
+
}
|
|
320
|
+
)
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
### Error Handling
|
|
324
|
+
|
|
325
|
+
```ruby
|
|
326
|
+
require 'setbit'
|
|
327
|
+
|
|
328
|
+
begin
|
|
329
|
+
client = SetBit::Client.new(api_key: "invalid_key")
|
|
330
|
+
rescue SetBit::AuthError => e
|
|
331
|
+
puts "Invalid API key: #{e.message}"
|
|
332
|
+
# Fall back to default behavior
|
|
333
|
+
client = nil
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
# Client returns safe defaults if initialization failed
|
|
337
|
+
if client&.enabled?("new-feature")
|
|
338
|
+
show_new_feature
|
|
339
|
+
else
|
|
340
|
+
show_old_feature
|
|
341
|
+
end
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
### Rails Integration
|
|
345
|
+
|
|
346
|
+
```ruby
|
|
347
|
+
# config/initializers/setbit.rb
|
|
348
|
+
Rails.application.config.setbit = SetBit::Client.new(
|
|
349
|
+
api_key: ENV['SETBIT_API_KEY'],
|
|
350
|
+
tags: {
|
|
351
|
+
env: Rails.env,
|
|
352
|
+
app: "rails-app"
|
|
353
|
+
},
|
|
354
|
+
logger: Rails.logger
|
|
355
|
+
)
|
|
356
|
+
|
|
357
|
+
# app/controllers/application_controller.rb
|
|
358
|
+
class ApplicationController < ActionController::Base
|
|
359
|
+
def setbit
|
|
360
|
+
Rails.application.config.setbit
|
|
361
|
+
end
|
|
362
|
+
helper_method :setbit
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
# app/controllers/checkout_controller.rb
|
|
366
|
+
class CheckoutController < ApplicationController
|
|
367
|
+
def new
|
|
368
|
+
variant = setbit.variant("checkout-flow")
|
|
369
|
+
|
|
370
|
+
if variant == "one_page"
|
|
371
|
+
render :one_page_checkout
|
|
372
|
+
else
|
|
373
|
+
render :multi_step_checkout
|
|
374
|
+
end
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
def complete
|
|
378
|
+
setbit.track(
|
|
379
|
+
"purchase",
|
|
380
|
+
flag_name: "checkout-flow",
|
|
381
|
+
metadata: { amount: params[:amount] }
|
|
382
|
+
)
|
|
383
|
+
redirect_to thank_you_path
|
|
384
|
+
end
|
|
385
|
+
end
|
|
386
|
+
|
|
387
|
+
# app/views/layouts/application.html.erb
|
|
388
|
+
<% if setbit.enabled?("new-header") %>
|
|
389
|
+
<%= render "shared/new_header" %>
|
|
390
|
+
<% else %>
|
|
391
|
+
<%= render "shared/old_header" %>
|
|
392
|
+
<% end %>
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
### Sinatra Integration
|
|
396
|
+
|
|
397
|
+
```ruby
|
|
398
|
+
require 'sinatra'
|
|
399
|
+
require 'setbit'
|
|
400
|
+
|
|
401
|
+
configure do
|
|
402
|
+
set :setbit, SetBit::Client.new(
|
|
403
|
+
api_key: ENV['SETBIT_API_KEY'],
|
|
404
|
+
tags: { env: ENV['RACK_ENV'], app: "sinatra-app" }
|
|
405
|
+
)
|
|
406
|
+
end
|
|
407
|
+
|
|
408
|
+
get '/' do
|
|
409
|
+
if settings.setbit.enabled?("new-homepage")
|
|
410
|
+
erb :homepage_v2
|
|
411
|
+
else
|
|
412
|
+
erb :homepage
|
|
413
|
+
end
|
|
414
|
+
end
|
|
415
|
+
|
|
416
|
+
post '/purchase' do
|
|
417
|
+
# Process purchase...
|
|
418
|
+
|
|
419
|
+
settings.setbit.track(
|
|
420
|
+
"purchase",
|
|
421
|
+
metadata: { amount: params[:amount] }
|
|
422
|
+
)
|
|
423
|
+
|
|
424
|
+
redirect '/thank-you'
|
|
425
|
+
end
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
### Background Jobs (Sidekiq)
|
|
429
|
+
|
|
430
|
+
```ruby
|
|
431
|
+
class ProcessOrderJob
|
|
432
|
+
include Sidekiq::Worker
|
|
433
|
+
|
|
434
|
+
def perform(order_id)
|
|
435
|
+
order = Order.find(order_id)
|
|
436
|
+
|
|
437
|
+
# Use SetBit in background job
|
|
438
|
+
client = SetBit::Client.new(
|
|
439
|
+
api_key: ENV['SETBIT_API_KEY'],
|
|
440
|
+
tags: { env: Rails.env, app: "worker" }
|
|
441
|
+
)
|
|
442
|
+
|
|
443
|
+
if client.enabled?("advanced-fraud-detection")
|
|
444
|
+
run_advanced_fraud_check(order)
|
|
445
|
+
else
|
|
446
|
+
run_basic_fraud_check(order)
|
|
447
|
+
end
|
|
448
|
+
|
|
449
|
+
client.track("order_processed", metadata: { order_id: order_id })
|
|
450
|
+
end
|
|
451
|
+
end
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
### Multi-Tenancy
|
|
455
|
+
|
|
456
|
+
```ruby
|
|
457
|
+
class ApplicationController < ActionController::Base
|
|
458
|
+
def setbit
|
|
459
|
+
@setbit ||= SetBit::Client.new(
|
|
460
|
+
api_key: ENV['SETBIT_API_KEY'],
|
|
461
|
+
tags: {
|
|
462
|
+
env: Rails.env,
|
|
463
|
+
app: "web",
|
|
464
|
+
tenant: current_tenant.slug
|
|
465
|
+
}
|
|
466
|
+
)
|
|
467
|
+
end
|
|
468
|
+
helper_method :setbit
|
|
469
|
+
end
|
|
470
|
+
|
|
471
|
+
# Now flags can be targeted per tenant
|
|
472
|
+
# tags: { env: "production", tenant: "acme-corp" } โ Show feature
|
|
473
|
+
# tags: { env: "production", tenant: "other-corp" } โ Hide feature
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
## Development
|
|
477
|
+
|
|
478
|
+
### Setup
|
|
479
|
+
|
|
480
|
+
```bash
|
|
481
|
+
git clone https://github.com/setbit/setbit-ruby.git
|
|
482
|
+
cd setbit-ruby
|
|
483
|
+
bundle install
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
### Running Tests
|
|
487
|
+
|
|
488
|
+
```bash
|
|
489
|
+
# Run all tests
|
|
490
|
+
bundle exec rspec
|
|
491
|
+
|
|
492
|
+
# Run specific test file
|
|
493
|
+
bundle exec rspec spec/client_spec.rb
|
|
494
|
+
|
|
495
|
+
# Run with coverage
|
|
496
|
+
bundle exec rake coverage
|
|
497
|
+
```
|
|
498
|
+
|
|
499
|
+
### Linting
|
|
500
|
+
|
|
501
|
+
```bash
|
|
502
|
+
# Run RuboCop
|
|
503
|
+
bundle exec rubocop
|
|
504
|
+
|
|
505
|
+
# Auto-fix issues
|
|
506
|
+
bundle exec rubocop -a
|
|
507
|
+
```
|
|
508
|
+
|
|
509
|
+
### Generate Documentation
|
|
510
|
+
|
|
511
|
+
```bash
|
|
512
|
+
bundle exec yard doc
|
|
513
|
+
```
|
|
514
|
+
|
|
515
|
+
## Error Handling
|
|
516
|
+
|
|
517
|
+
The SDK uses a **fail-open** philosophy - if something goes wrong, it returns safe default values rather than crashing your application.
|
|
518
|
+
|
|
519
|
+
### Exception Types
|
|
520
|
+
|
|
521
|
+
- `SetBit::Error`: Base exception for all SDK errors
|
|
522
|
+
- `SetBit::AuthError`: Invalid API key (raised during initialization/refresh)
|
|
523
|
+
- `SetBit::APIError`: API request failed (raised during initialization/refresh)
|
|
524
|
+
|
|
525
|
+
### Behavior
|
|
526
|
+
|
|
527
|
+
| Method | Error Behavior |
|
|
528
|
+
|--------|----------------|
|
|
529
|
+
| `new` | Raises exception if API key invalid or flags can't be fetched |
|
|
530
|
+
| `enabled?` | Returns `default` value if flag not found |
|
|
531
|
+
| `variant` | Returns `default` variant if flag not found |
|
|
532
|
+
| `track` | Logs error but does not raise exception |
|
|
533
|
+
| `refresh` | Raises exception if refresh fails |
|
|
534
|
+
|
|
535
|
+
## API Endpoints
|
|
536
|
+
|
|
537
|
+
The SDK communicates with these SetBit API endpoints:
|
|
538
|
+
|
|
539
|
+
- **GET** `/api/sdk/flags` - Fetch flags for given tags
|
|
540
|
+
- **POST** `/api/events` - Send conversion events
|
|
541
|
+
|
|
542
|
+
## Requirements
|
|
543
|
+
|
|
544
|
+
- Ruby >= 2.6.0
|
|
545
|
+
- No external runtime dependencies (uses standard library only!)
|
|
546
|
+
|
|
547
|
+
## Support
|
|
548
|
+
|
|
549
|
+
- ๐ [Documentation](https://docs.setbit.io)
|
|
550
|
+
- ๐ฌ [Discord Community](https://discord.gg/setbit)
|
|
551
|
+
- ๐ง Email: support@setbit.io
|
|
552
|
+
- ๐ [Report Issues](https://github.com/setbit/setbit-ruby/issues)
|
|
553
|
+
|
|
554
|
+
## License
|
|
555
|
+
|
|
556
|
+
MIT License - see [LICENSE](LICENSE) file for details.
|
|
557
|
+
|
|
558
|
+
## Contributing
|
|
559
|
+
|
|
560
|
+
Contributions are welcome! Please feel free to submit a Pull Request.
|
|
561
|
+
|
|
562
|
+
1. Fork the repository
|
|
563
|
+
2. Create your feature branch (`git checkout -b feature/amazing-feature`)
|
|
564
|
+
3. Commit your changes (`git commit -m 'Add some amazing feature'`)
|
|
565
|
+
4. Push to the branch (`git push origin feature/amazing-feature`)
|
|
566
|
+
5. Open a Pull Request
|
|
567
|
+
|
|
568
|
+
## Roadmap
|
|
569
|
+
|
|
570
|
+
- [ ] Add memoization options for variant selection
|
|
571
|
+
- [ ] Support for local flag evaluation
|
|
572
|
+
- [ ] Streaming flag updates via WebSocket
|
|
573
|
+
- [ ] Performance metrics and benchmarks
|
|
574
|
+
|
|
575
|
+
---
|
|
576
|
+
|
|
577
|
+
Made with โค๏ธ by the SetBit team
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "net/http"
|
|
4
|
+
require "json"
|
|
5
|
+
require "uri"
|
|
6
|
+
require "logger"
|
|
7
|
+
|
|
8
|
+
module SetBit
|
|
9
|
+
# SetBit feature flag client
|
|
10
|
+
#
|
|
11
|
+
# @example
|
|
12
|
+
# client = SetBit::Client.new(
|
|
13
|
+
# api_key: "pk_abc123",
|
|
14
|
+
# tags: { env: "production", app: "web" }
|
|
15
|
+
# )
|
|
16
|
+
#
|
|
17
|
+
# if client.enabled?("new-feature", user_id: "user_123")
|
|
18
|
+
# show_new_feature
|
|
19
|
+
# end
|
|
20
|
+
class Client
|
|
21
|
+
attr_reader :api_key, :tags, :base_url
|
|
22
|
+
attr_accessor :logger
|
|
23
|
+
|
|
24
|
+
# Initialize SetBit client
|
|
25
|
+
#
|
|
26
|
+
# @param api_key [String] SetBit API key (required)
|
|
27
|
+
# @param tags [Hash] Tags for targeting (env, app, team, region, etc.)
|
|
28
|
+
# @param base_url [String] API endpoint base URL
|
|
29
|
+
# @param logger [Logger] Optional logger instance
|
|
30
|
+
#
|
|
31
|
+
# @raise [SetBit::Error] if API key is missing
|
|
32
|
+
def initialize(api_key:, tags: {}, base_url: "https://flags.setbit.io", logger: nil)
|
|
33
|
+
raise SetBit::Error, "API key is required" if api_key.nil? || api_key.empty?
|
|
34
|
+
|
|
35
|
+
@api_key = api_key
|
|
36
|
+
@tags = tags || {}
|
|
37
|
+
@base_url = base_url.chomp("/")
|
|
38
|
+
@logger = logger || Logger.new(nil) # Silent logger by default
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Check if a flag is enabled
|
|
42
|
+
#
|
|
43
|
+
# @param flag_name [String] Name of the flag to check
|
|
44
|
+
# @param user_id [String] User identifier (required for analytics and billing)
|
|
45
|
+
# @param default [Boolean] Default value if flag not found or API fails
|
|
46
|
+
# @return [Boolean] true if flag is enabled, false otherwise
|
|
47
|
+
def enabled?(flag_name, user_id:, default: false)
|
|
48
|
+
url = URI("#{@base_url}/v1/evaluate")
|
|
49
|
+
|
|
50
|
+
payload = {
|
|
51
|
+
apiKey: @api_key,
|
|
52
|
+
userId: user_id,
|
|
53
|
+
tags: @tags,
|
|
54
|
+
flagName: flag_name
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
http = Net::HTTP.new(url.host, url.port)
|
|
58
|
+
http.use_ssl = url.scheme == "https"
|
|
59
|
+
http.open_timeout = 5
|
|
60
|
+
http.read_timeout = 5
|
|
61
|
+
|
|
62
|
+
request = Net::HTTP::Post.new(url, "Content-Type" => "application/json")
|
|
63
|
+
request.body = payload.to_json
|
|
64
|
+
|
|
65
|
+
response = http.request(request)
|
|
66
|
+
|
|
67
|
+
# Handle authentication errors
|
|
68
|
+
if response.code == "401"
|
|
69
|
+
@logger.error("Invalid API key")
|
|
70
|
+
return default
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Handle other errors - fail open
|
|
74
|
+
unless response.is_a?(Net::HTTPSuccess)
|
|
75
|
+
@logger.error("API error #{response.code}, returning default: #{default}")
|
|
76
|
+
return default
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
result = JSON.parse(response.body)
|
|
80
|
+
result["enabled"] || default
|
|
81
|
+
|
|
82
|
+
rescue JSON::ParserError => e
|
|
83
|
+
@logger.error("Failed to parse API response: #{e.message}, returning default: #{default}")
|
|
84
|
+
default
|
|
85
|
+
rescue StandardError => e
|
|
86
|
+
@logger.error("Failed to evaluate flag '#{flag_name}': #{e.message}, returning default: #{default}")
|
|
87
|
+
default
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Get the variant for an A/B test experiment
|
|
91
|
+
#
|
|
92
|
+
# @param flag_name [String] Name of the experiment flag
|
|
93
|
+
# @param user_id [String] User identifier (required)
|
|
94
|
+
# @param default [String] Default variant if flag not found or API fails
|
|
95
|
+
# @return [String] Variant name (e.g., "control", "variant_a", "variant_b")
|
|
96
|
+
def variant(flag_name, user_id:, default: "control")
|
|
97
|
+
url = URI("#{@base_url}/v1/evaluate")
|
|
98
|
+
|
|
99
|
+
payload = {
|
|
100
|
+
apiKey: @api_key,
|
|
101
|
+
userId: user_id,
|
|
102
|
+
tags: @tags,
|
|
103
|
+
flagName: flag_name
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
http = Net::HTTP.new(url.host, url.port)
|
|
107
|
+
http.use_ssl = url.scheme == "https"
|
|
108
|
+
http.open_timeout = 5
|
|
109
|
+
http.read_timeout = 5
|
|
110
|
+
|
|
111
|
+
request = Net::HTTP::Post.new(url, "Content-Type" => "application/json")
|
|
112
|
+
request.body = payload.to_json
|
|
113
|
+
|
|
114
|
+
response = http.request(request)
|
|
115
|
+
|
|
116
|
+
# Handle authentication errors
|
|
117
|
+
if response.code == "401"
|
|
118
|
+
@logger.error("Invalid API key")
|
|
119
|
+
return default
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Handle other errors - fail open
|
|
123
|
+
unless response.is_a?(Net::HTTPSuccess)
|
|
124
|
+
@logger.error("API error #{response.code}, returning default: #{default}")
|
|
125
|
+
return default
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
result = JSON.parse(response.body)
|
|
129
|
+
|
|
130
|
+
# If flag is disabled, return default
|
|
131
|
+
return default unless result["enabled"]
|
|
132
|
+
|
|
133
|
+
result["variant"] || default
|
|
134
|
+
|
|
135
|
+
rescue JSON::ParserError => e
|
|
136
|
+
@logger.error("Failed to parse API response: #{e.message}, returning default: #{default}")
|
|
137
|
+
default
|
|
138
|
+
rescue StandardError => e
|
|
139
|
+
@logger.error("Failed to get variant for '#{flag_name}': #{e.message}, returning default: #{default}")
|
|
140
|
+
default
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# Track a conversion event
|
|
144
|
+
#
|
|
145
|
+
# @param event_name [String] Name of the event (e.g., "purchase", "signup")
|
|
146
|
+
# @param user_id [String] User identifier (required)
|
|
147
|
+
# @param flag_name [String, nil] Optional flag name to associate with event
|
|
148
|
+
# @param variant [String, nil] Optional variant the user was assigned to (for A/B test attribution)
|
|
149
|
+
# @param metadata [Hash] Optional metadata hash
|
|
150
|
+
# @return [void]
|
|
151
|
+
#
|
|
152
|
+
# @note Fails silently if tracking request fails (logs error but doesn't raise)
|
|
153
|
+
#
|
|
154
|
+
# @example Track conversion with variant attribution
|
|
155
|
+
# variant = client.variant("pricing-test", user_id: user_id)
|
|
156
|
+
# # ... later when user converts ...
|
|
157
|
+
# client.track("purchase", user_id: user_id, flag_name: "pricing-test", variant: variant)
|
|
158
|
+
def track(event_name, user_id:, flag_name: nil, variant: nil, metadata: {})
|
|
159
|
+
url = URI("#{@base_url}/v1/track")
|
|
160
|
+
|
|
161
|
+
payload = {
|
|
162
|
+
apiKey: @api_key,
|
|
163
|
+
userId: user_id,
|
|
164
|
+
eventName: event_name
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
payload[:flagName] = flag_name if flag_name
|
|
168
|
+
payload[:variant] = variant if variant
|
|
169
|
+
payload[:metadata] = metadata if metadata && !metadata.empty?
|
|
170
|
+
|
|
171
|
+
http = Net::HTTP.new(url.host, url.port)
|
|
172
|
+
http.use_ssl = url.scheme == "https"
|
|
173
|
+
http.open_timeout = 5
|
|
174
|
+
http.read_timeout = 5
|
|
175
|
+
|
|
176
|
+
request = Net::HTTP::Post.new(url, "Content-Type" => "application/json")
|
|
177
|
+
request.body = payload.to_json
|
|
178
|
+
|
|
179
|
+
response = http.request(request)
|
|
180
|
+
|
|
181
|
+
if response.is_a?(Net::HTTPSuccess)
|
|
182
|
+
@logger.debug("Tracked event '#{event_name}' for user '#{user_id}'")
|
|
183
|
+
else
|
|
184
|
+
@logger.error("Failed to track event '#{event_name}': HTTP #{response.code}")
|
|
185
|
+
end
|
|
186
|
+
rescue StandardError => e
|
|
187
|
+
@logger.error("Failed to track event '#{event_name}': #{e.message}")
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SetBit
|
|
4
|
+
# Base error class for SetBit SDK
|
|
5
|
+
class Error < StandardError; end
|
|
6
|
+
|
|
7
|
+
# Raised when API key is invalid
|
|
8
|
+
class AuthError < Error; end
|
|
9
|
+
|
|
10
|
+
# Raised when API request fails
|
|
11
|
+
class APIError < Error; end
|
|
12
|
+
end
|
data/lib/setbit/utils.rb
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "digest"
|
|
4
|
+
|
|
5
|
+
module SetBit
|
|
6
|
+
module Utils
|
|
7
|
+
# Compute a consistent percentage (0-99) for a given identifier
|
|
8
|
+
# Uses SHA-256 hash to ensure consistent assignment
|
|
9
|
+
#
|
|
10
|
+
# @param identifier [String] User ID or other unique identifier
|
|
11
|
+
# @return [Integer] Integer between 0 and 99 (inclusive)
|
|
12
|
+
def self.compute_rollout_percentage(identifier)
|
|
13
|
+
hash_bytes = Digest::SHA256.digest(identifier)
|
|
14
|
+
# Use first 4 bytes to get a number
|
|
15
|
+
hash_int = hash_bytes[0, 4].unpack1("N")
|
|
16
|
+
hash_int % 100
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Select a variant using weighted random distribution
|
|
20
|
+
#
|
|
21
|
+
# @param variants [Hash] Hash mapping variant names to config with 'weight' key
|
|
22
|
+
# @return [String] Selected variant name
|
|
23
|
+
def self.select_variant(variants)
|
|
24
|
+
return "control" if variants.nil? || variants.empty?
|
|
25
|
+
|
|
26
|
+
total_weight = variants.values.sum { |v| v["weight"] || 0 }
|
|
27
|
+
|
|
28
|
+
return variants.keys.first || "control" if total_weight.zero?
|
|
29
|
+
|
|
30
|
+
rand_value = rand(total_weight)
|
|
31
|
+
|
|
32
|
+
cumulative = 0
|
|
33
|
+
variants.each do |name, config|
|
|
34
|
+
cumulative += config["weight"] || 0
|
|
35
|
+
return name if rand_value < cumulative
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
"control" # fallback
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
data/lib/setbit.rb
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "setbit/version"
|
|
4
|
+
require_relative "setbit/errors"
|
|
5
|
+
require_relative "setbit/utils"
|
|
6
|
+
require_relative "setbit/client"
|
|
7
|
+
|
|
8
|
+
# SetBit Ruby SDK
|
|
9
|
+
#
|
|
10
|
+
# Simple feature flag and A/B testing client for SetBit.
|
|
11
|
+
#
|
|
12
|
+
# @example
|
|
13
|
+
# client = SetBit::Client.new(
|
|
14
|
+
# api_key: "pk_abc123",
|
|
15
|
+
# tags: { env: "production", app: "web" }
|
|
16
|
+
# )
|
|
17
|
+
#
|
|
18
|
+
# if client.enabled?("new-feature")
|
|
19
|
+
# show_new_feature
|
|
20
|
+
# end
|
|
21
|
+
module SetBit
|
|
22
|
+
end
|
data/setbit.gemspec
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "lib/setbit/version"
|
|
4
|
+
|
|
5
|
+
Gem::Specification.new do |spec|
|
|
6
|
+
spec.name = "setbit"
|
|
7
|
+
spec.version = SetBit::VERSION
|
|
8
|
+
spec.authors = ["SetBit"]
|
|
9
|
+
spec.email = ["support@setbit.io"]
|
|
10
|
+
|
|
11
|
+
spec.summary = "Ruby SDK for SetBit feature flags and A/B testing"
|
|
12
|
+
spec.description = "Simple and powerful feature flag and A/B testing client for SetBit. " \
|
|
13
|
+
"Provides boolean flags, weighted variant distribution, and conversion tracking."
|
|
14
|
+
spec.homepage = "https://setbit.io"
|
|
15
|
+
spec.license = "MIT"
|
|
16
|
+
spec.required_ruby_version = ">= 2.6.0"
|
|
17
|
+
|
|
18
|
+
spec.metadata["homepage_uri"] = spec.homepage
|
|
19
|
+
spec.metadata["source_code_uri"] = "https://github.com/setbit/setbit-ruby"
|
|
20
|
+
spec.metadata["changelog_uri"] = "https://github.com/setbit/setbit-ruby/blob/main/CHANGELOG.md"
|
|
21
|
+
spec.metadata["bug_tracker_uri"] = "https://github.com/setbit/setbit-ruby/issues"
|
|
22
|
+
spec.metadata["documentation_uri"] = "https://docs.setbit.io"
|
|
23
|
+
|
|
24
|
+
# Specify which files should be added to the gem when it is released.
|
|
25
|
+
spec.files = Dir[
|
|
26
|
+
"lib/**/*.rb",
|
|
27
|
+
"README.md",
|
|
28
|
+
"CHANGELOG.md",
|
|
29
|
+
"LICENSE",
|
|
30
|
+
"setbit.gemspec"
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
spec.require_paths = ["lib"]
|
|
34
|
+
|
|
35
|
+
# Runtime dependencies
|
|
36
|
+
# Using only standard library for HTTP - no external dependencies!
|
|
37
|
+
|
|
38
|
+
# Development dependencies
|
|
39
|
+
spec.add_development_dependency "rspec", "~> 3.12"
|
|
40
|
+
spec.add_development_dependency "webmock", "~> 3.18"
|
|
41
|
+
spec.add_development_dependency "rake", "~> 13.0"
|
|
42
|
+
spec.add_development_dependency "rubocop", "~> 1.50"
|
|
43
|
+
spec.add_development_dependency "yard", "~> 0.9"
|
|
44
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: setbit
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.1
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- SetBit
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2025-11-26 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: rspec
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - "~>"
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '3.12'
|
|
20
|
+
type: :development
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - "~>"
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '3.12'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: webmock
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - "~>"
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '3.18'
|
|
34
|
+
type: :development
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - "~>"
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '3.18'
|
|
41
|
+
- !ruby/object:Gem::Dependency
|
|
42
|
+
name: rake
|
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - "~>"
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: '13.0'
|
|
48
|
+
type: :development
|
|
49
|
+
prerelease: false
|
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
51
|
+
requirements:
|
|
52
|
+
- - "~>"
|
|
53
|
+
- !ruby/object:Gem::Version
|
|
54
|
+
version: '13.0'
|
|
55
|
+
- !ruby/object:Gem::Dependency
|
|
56
|
+
name: rubocop
|
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
|
58
|
+
requirements:
|
|
59
|
+
- - "~>"
|
|
60
|
+
- !ruby/object:Gem::Version
|
|
61
|
+
version: '1.50'
|
|
62
|
+
type: :development
|
|
63
|
+
prerelease: false
|
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
65
|
+
requirements:
|
|
66
|
+
- - "~>"
|
|
67
|
+
- !ruby/object:Gem::Version
|
|
68
|
+
version: '1.50'
|
|
69
|
+
- !ruby/object:Gem::Dependency
|
|
70
|
+
name: yard
|
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
|
72
|
+
requirements:
|
|
73
|
+
- - "~>"
|
|
74
|
+
- !ruby/object:Gem::Version
|
|
75
|
+
version: '0.9'
|
|
76
|
+
type: :development
|
|
77
|
+
prerelease: false
|
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
79
|
+
requirements:
|
|
80
|
+
- - "~>"
|
|
81
|
+
- !ruby/object:Gem::Version
|
|
82
|
+
version: '0.9'
|
|
83
|
+
description: Simple and powerful feature flag and A/B testing client for SetBit. Provides
|
|
84
|
+
boolean flags, weighted variant distribution, and conversion tracking.
|
|
85
|
+
email:
|
|
86
|
+
- support@setbit.io
|
|
87
|
+
executables: []
|
|
88
|
+
extensions: []
|
|
89
|
+
extra_rdoc_files: []
|
|
90
|
+
files:
|
|
91
|
+
- CHANGELOG.md
|
|
92
|
+
- LICENSE
|
|
93
|
+
- README.md
|
|
94
|
+
- lib/setbit.rb
|
|
95
|
+
- lib/setbit/client.rb
|
|
96
|
+
- lib/setbit/errors.rb
|
|
97
|
+
- lib/setbit/utils.rb
|
|
98
|
+
- lib/setbit/version.rb
|
|
99
|
+
- setbit.gemspec
|
|
100
|
+
homepage: https://setbit.io
|
|
101
|
+
licenses:
|
|
102
|
+
- MIT
|
|
103
|
+
metadata:
|
|
104
|
+
homepage_uri: https://setbit.io
|
|
105
|
+
source_code_uri: https://github.com/setbit/setbit-ruby
|
|
106
|
+
changelog_uri: https://github.com/setbit/setbit-ruby/blob/main/CHANGELOG.md
|
|
107
|
+
bug_tracker_uri: https://github.com/setbit/setbit-ruby/issues
|
|
108
|
+
documentation_uri: https://docs.setbit.io
|
|
109
|
+
post_install_message:
|
|
110
|
+
rdoc_options: []
|
|
111
|
+
require_paths:
|
|
112
|
+
- lib
|
|
113
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
114
|
+
requirements:
|
|
115
|
+
- - ">="
|
|
116
|
+
- !ruby/object:Gem::Version
|
|
117
|
+
version: 2.6.0
|
|
118
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
119
|
+
requirements:
|
|
120
|
+
- - ">="
|
|
121
|
+
- !ruby/object:Gem::Version
|
|
122
|
+
version: '0'
|
|
123
|
+
requirements: []
|
|
124
|
+
rubygems_version: 3.4.10
|
|
125
|
+
signing_key:
|
|
126
|
+
specification_version: 4
|
|
127
|
+
summary: Ruby SDK for SetBit feature flags and A/B testing
|
|
128
|
+
test_files: []
|