featurehub-sdk 1.3.0 → 2.0.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 (37) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/CLAUDE.md +85 -0
  3. data/.ruby-version +1 -1
  4. data/CHANGELOG.md +18 -1
  5. data/Gemfile +1 -1
  6. data/Gemfile.lock +20 -8
  7. data/README.md +306 -119
  8. data/examples/rails_example/.ruby-version +1 -1
  9. data/examples/rails_example/Dockerfile +1 -1
  10. data/examples/sinatra/.dockerignore +7 -0
  11. data/examples/sinatra/.ruby-version +1 -1
  12. data/examples/sinatra/Dockerfile +14 -25
  13. data/examples/sinatra/Gemfile +5 -4
  14. data/examples/sinatra/Gemfile.lock +40 -32
  15. data/examples/sinatra/app/application.rb +21 -9
  16. data/examples/sinatra/docker-compose.yaml +24 -0
  17. data/examples/sinatra/feature-flags.yaml +6 -0
  18. data/examples/sinatra/sinatra.iml +35 -14
  19. data/examples/sinatra/start.sh +2 -0
  20. data/featurehub-sdk.gemspec +4 -1
  21. data/lib/feature_hub/sdk/context.rb +28 -7
  22. data/lib/feature_hub/sdk/feature_hub_config.rb +68 -12
  23. data/lib/feature_hub/sdk/feature_repository.rb +52 -13
  24. data/lib/feature_hub/sdk/{feature_state.rb → feature_state_holder.rb} +13 -9
  25. data/lib/feature_hub/sdk/interceptors.rb +10 -6
  26. data/lib/feature_hub/sdk/internal_feature_repository.rb +7 -3
  27. data/lib/feature_hub/sdk/local_yaml_interceptor.rb +99 -0
  28. data/lib/feature_hub/sdk/local_yaml_store.rb +71 -0
  29. data/lib/feature_hub/sdk/poll_edge_service.rb +10 -15
  30. data/lib/feature_hub/sdk/raw_update_feature_listener.rb +19 -0
  31. data/lib/feature_hub/sdk/redis_session_store.rb +130 -0
  32. data/lib/feature_hub/sdk/strategy_attributes.rb +7 -0
  33. data/lib/feature_hub/sdk/streaming_edge_service.rb +5 -7
  34. data/lib/feature_hub/sdk/version.rb +1 -10
  35. data/lib/featurehub-sdk.rb +5 -1
  36. data/sig/feature_hub/featurehub.rbs +127 -28
  37. metadata +27 -5
data/README.md CHANGED
@@ -1,18 +1,18 @@
1
- # Official FeatureHub Ruby SDK.
1
+ # Official FeatureHub Ruby SDK
2
2
 
3
3
  ## Overview
4
- To control the feature flags from the FeatureHub Admin console, either use our [demo](https://demo.featurehub.io) version for evaluation or install the app using our guide [here](https://docs.featurehub.io/featurehub/latest/installation.html)
4
+
5
+ To control the feature flags from the FeatureHub Admin console, either use our [demo](https://demo.featurehub.io) version for evaluation or install the app using our guide [here](https://docs.featurehub.io/featurehub/latest/installation.html).
5
6
 
6
7
  ## SDK installation
7
8
 
8
- Add the featurehub sdk gem to your Gemfile and/or gemspec if you are creating a library:
9
+ Add the featurehub-sdk gem to your Gemfile:
9
10
 
10
- ```
11
- gem `featurehub-sdk`
11
+ ```ruby
12
+ gem 'featurehub-sdk'
12
13
  ```
13
14
 
14
-
15
- To use it in your code, use:
15
+ To use it in your code:
16
16
 
17
17
  ```ruby
18
18
  require 'featurehub-sdk'
@@ -20,216 +20,403 @@ require 'featurehub-sdk'
20
20
 
21
21
  ## Options to get feature updates
22
22
 
23
- There are 2 ways to request for feature updates via this SDK:
24
-
25
- - **SSE (Server Sent Events) realtime updates mechanism**
23
+ There are 2 ways to request feature updates via this SDK:
26
24
 
27
- In this mode, you will make a connection to the FeatureHub Edge server using using Server Sent Events, any updates to any features will come through in _near realtime_, automatically updating the feature values in the repository. This method is recommended for server applications.
25
+ - **SSE (Server Sent Events) realtime updates**
28
26
 
29
- - **FeatureHub polling client (GET request updates)**
27
+ Makes a persistent connection to the FeatureHub Edge server. Any updates to features come through in near-realtime, automatically updating the repository. Recommended for long-running server applications.
30
28
 
31
- In this mode you can set an interval (from 0 - just once) to any number of seconds between polling. This is more useful for when you have short term single threaded
32
- processes like command line tools. Batch tools that iterate over data sets and wish to control when updates happen can also benefit from this method.
29
+ - **Polling client (GET request)**
33
30
 
34
- This SDK uses concurrent ruby to ensure whichever option you choose stays open and continually updates your data.
31
+ Requests updates at a configurable interval (0 = once only). Useful for short-lived processes such as CLI tools or batch jobs.
35
32
 
36
- ## Example
37
-
38
- Check our example Sinatra app [here](https://github.com/featurehub-io/featurehub-ruby-sdk/tree/main/example/sinatra)
33
+ Both options use `concurrent-ruby` to keep the connection open and update state in the background.
39
34
 
40
35
  ## Quick start
41
36
 
42
- ### Connecting to FeatureHub
43
- There are 3 steps to connecting:
44
- 1) Copy FeatureHub API Key from the FeatureHub Admin Console
45
- 2) Create FeatureHub config
46
- 3) Check FeatureHub Repository readiness and request feature state
37
+ ### 1. Copy your API Key
47
38
 
48
- #### 1. API Key from the FeatureHub Admin Console
49
- Find and copy your API Key from the FeatureHub Admin Console on the API Keys page -
50
- you will use this in your code to configure feature updates for your environments.
51
- It should look similar to this: ```default/71ed3c04-122b-4312-9ea8-06b2b8d6ceac/fsTmCrcZZoGyl56kPHxfKAkbHrJ7xZMKO3dlBiab5IqUXjgKvqpjxYdI8zdXiJqYCpv92Jrki0jY5taE```.
52
- There are two options - a Server Evaluated API Key and a Client Evaluated API Key. More on this [here](https://docs.featurehub.io/#_client_and_server_api_keys)
39
+ Find and copy your API Key from the FeatureHub Admin Console on the API Keys page. It will look similar to:
53
40
 
54
- Client Side evaluation is intended for use in secure environments (such as microservices) and is intended for rapid client side evaluation, per request for example.
41
+ ```
42
+ default/71ed3c04-122b-4312-9ea8-06b2b8d6ceac/fsTmCrcZZoGyl56kPHxfKAkbHrJ7xZMKO3dlBiab5IqUXjgKvqpjxYdI8zdXiJqYCpv92Jrki0jY5taE
43
+ ```
55
44
 
56
- Server Side evaluation is more suitable when you are using an _insecure client_. (e.g. command line tool). This also means you evaluate one user per client.
45
+ There are two key types Server Evaluated and Client Evaluated. More detail [here](https://docs.featurehub.io/#_client_and_server_api_keys).
57
46
 
58
- #### 2. Create FeatureHub config:
47
+ - **Client Evaluated** keys (contain `*`) send full rollout strategy data to the SDK and evaluate strategies locally, per request. Intended for secure server-side environments such as microservices.
48
+ - **Server Evaluated** keys evaluate on the server side. Suitable for insecure clients or environments where you evaluate one user per connection.
59
49
 
60
- Create `FeatureHubConfig`. You need to provide the API Key and the URL of the FeatureHub Edge server.
50
+ ### 2. Create FeatureHub config
61
51
 
62
52
  ```ruby
63
- config = FeatureHub::Sdk::FeatureHubConfig.new(ENV.fetch("FEATUREHUB_EDGE_URL"),
64
- [ENV.fetch("FEATUREHUB_CLIENT_API_KEY")])
53
+ config = FeatureHub::Sdk::FeatureHubConfig.new(
54
+ ENV.fetch("FEATUREHUB_EDGE_URL"),
55
+ [ENV.fetch("FEATUREHUB_CLIENT_API_KEY")]
56
+ )
65
57
  config.init
66
-
67
58
  ```
68
-
69
- Note, you only ever need to do this once, a Config consists of a Repository
70
- (which holds state) and an Edge Server (which gets the updates and passes them
71
- on to the Repository). You can have many of them if you wish, but you don't need
72
- to.
73
-
74
- to in Rails, you might create an initializer that does this:
59
+
60
+ You only ever need to do this once. A `FeatureHubConfig` holds a `FeatureHubRepository` (state) and an edge service (updates). In Rails, create an initializer:
75
61
 
76
62
  ```ruby
77
- Rails.configuration.fh_client = FeatureHub::Sdk::FeatureHubConfig.new(ENV.fetch("FEATUREHUB_EDGE_URL"),
78
- [ENV.fetch("FEATUREHUB_CLIENT_API_KEY")]).init
63
+ Rails.configuration.fh_client = FeatureHub::Sdk::FeatureHubConfig.new(
64
+ ENV.fetch("FEATUREHUB_EDGE_URL"),
65
+ [ENV.fetch("FEATUREHUB_CLIENT_API_KEY")]
66
+ ).init
79
67
  ```
80
68
 
81
- in Sinatra (our example), it might do this:
69
+ In Sinatra:
82
70
 
83
71
  ```ruby
84
72
  class App < Sinatra::Base
85
- configure do
86
- set :fh_config, FeatureHub::Sdk::FeatureHubConfig.new(ENV.fetch("FEATUREHUB_EDGE_URL"),
87
- [ENV.fetch("FEATUREHUB_CLIENT_API_KEY")])
88
- end
73
+ configure do
74
+ set :fh_config, FeatureHub::Sdk::FeatureHubConfig.new(
75
+ ENV.fetch("FEATUREHUB_EDGE_URL"),
76
+ [ENV.fetch("FEATUREHUB_CLIENT_API_KEY")]
77
+ )
78
+ end
89
79
  end
90
80
  ```
91
81
 
92
-
93
- By default, this SDK will use SSE client. If you decide to use FeatureHub polling client, after initialising the config, you can add this:
82
+ To use the polling client instead of SSE:
94
83
 
95
84
  ```ruby
96
85
  config.use_polling_edge_service(30)
97
- # OR
98
- config.use_polling_edge_service # uses environment variable FEATUREHUB_POLL_INTERVAL or default of 30
86
+ # OR — reads FEATUREHUB_POLL_INTERVAL env var, defaults to 30 seconds
87
+ config.use_polling_edge_service
99
88
  ```
100
89
 
101
- in this case it is configured for requesting an update every 30 seconds.
102
-
103
- #### 3. Check FeatureHub Repository readiness and request feature state
90
+ ### 3. Check readiness and request feature state
104
91
 
105
- Check for FeatureHub Repository readiness:
106
92
  ```ruby
107
93
  if config.repository.ready?
108
- # do something
94
+ # safe to evaluate features
109
95
  end
110
96
  ```
111
97
 
112
- If you are not intending to use rollout strategies, you can pass empty context to the SDK:
98
+ See [Readiness](#readiness) below for details on incorporating this into health checks.
99
+
100
+ ## Evaluating features
101
+
102
+ ### Without a context (no rollout strategies)
113
103
 
114
104
  ```ruby
115
- def name_arg(name)
116
- if config.new_context.build.feature("FEATURE_TITLE_TO_UPPERCASE").flag
117
- "HELLO WORLD"
118
- else
119
- "hello world"
120
- end
105
+ if config.new_context.build.feature("FEATURE_TITLE_TO_UPPERCASE").flag
106
+ "HELLO WORLD"
107
+ else
108
+ "hello world"
121
109
  end
122
110
  ```
123
111
 
112
+ ### With a context (rollout strategies)
124
113
 
125
- If you are using rollout strategies and targeting rules they are all determined by the active _user context_. In this example we pass `user_key` to the context :
114
+ Build a context with the attributes you want to use for strategy evaluation, then call `build` to push them to the server (server-evaluated keys) or trigger a poll (client-evaluated keys):
126
115
 
127
116
  ```ruby
128
- def name_arg(name)
129
- if config.new_context.user_key(name).build.feature("FEATURE_TITLE_TO_UPPERCASE").flag
130
- "HELLO WORLD"
131
- else
132
- "hello world"
133
- end
117
+ ctx = config.new_context
118
+ .user_key(current_user.id)
119
+ .country("australia")
120
+ .platform("ios")
121
+ .version("2.3.1")
122
+ .attribute_value("plan", "premium")
123
+ .build
124
+
125
+ if ctx.feature("FEATURE_TITLE_TO_UPPERCASE").flag
126
+ # ...
134
127
  end
135
128
  ```
136
129
 
137
- Well known fields have their own methods, or you can add custom
138
- values for fields using `attribute_value(key, [values])`. For
139
- example if you wish to trigger on specific contract ids and each
140
- user could have a different set of contract ids, you can add those
141
- `attribute_value("contract_values", [2,17,45])` and have configured
142
- your strategy with a list of contract values which trigger the feature.
130
+ #### Well-known context attributes
131
+
132
+ | Method | ContextKey |
133
+ |---|---|
134
+ | `user_key(value)` | `:userkey` |
135
+ | `session_key(value)` | `:session` |
136
+ | `country(value)` | `:country` |
137
+ | `platform(value)` | `:platform` |
138
+ | `device(value)` | `:device` |
139
+ | `version(value)` | `:version` |
140
+
141
+ #### Custom attributes
142
+
143
+ ```ruby
144
+ ctx.attribute_value("contract_ids", [2, 17, 45])
145
+ ```
146
+
147
+ #### `assign` — bulk-set attributes from a hash
148
+
149
+ `assign` accepts a hash, maps well-known keys to their dedicated setters, and merges anything else as a custom attribute:
150
+
151
+ ```ruby
152
+ ctx.assign(
153
+ userkey: current_user.id,
154
+ country: "nz",
155
+ plan: "enterprise"
156
+ )
157
+ ```
158
+
159
+ String keys are also accepted (`"userkey"` and `:userkey` are equivalent).
160
+
161
+ #### Construct a context with initial attributes
162
+
163
+ Pass a hash directly to `new_context` via the repository, or pre-populate at construction time:
164
+
165
+ ```ruby
166
+ ctx = FeatureHub::Sdk::ClientContext.new(repository, { userkey: "u1", country: "nz" })
167
+ ```
168
+
169
+ #### One-off feature evaluation with inline attributes
170
+
171
+ If you only need to check one feature and do not want to build a context, you can pass attributes directly to `feature`:
172
+
173
+ ```ruby
174
+ # On the config (delegates to the repository)
175
+ config.feature("SUBMIT_COLOR_BUTTON", { country: "nz" }).string
176
+
177
+ # Or directly on the repository
178
+ config.repository.feature("SUBMIT_COLOR_BUTTON", { country: "nz", userkey: "u1" }).string
179
+ ```
180
+
181
+ This creates a temporary `ClientContext` internally and evaluates the feature through it.
182
+
183
+ #### `value` — get a raw value with a default
184
+
185
+ `value(key, default_value = nil, attrs = nil)` returns the feature's value directly, or `default_value` if the feature does not exist:
186
+
187
+ ```ruby
188
+ # Simple lookup with a fallback
189
+ color = config.value("SUBMIT_COLOR_BUTTON", "blue")
190
+
191
+ # With inline attributes for strategy evaluation
192
+ color = config.value("SUBMIT_COLOR_BUTTON", "blue", { country: "nz" })
193
+
194
+ # Also available on the repository directly
195
+ color = config.repository.value("SUBMIT_COLOR_BUTTON", "blue")
196
+ ```
197
+
198
+ #### Feature value accessors
199
+
200
+ | Method | Returns |
201
+ |---|---|
202
+ | `.flag` / `.boolean` | `bool?` |
203
+ | `.string` | `String?` |
204
+ | `.number` | `Float?` |
205
+ | `.raw_json` | `String?` (raw JSON string) |
206
+ | `.json` | `Hash?` (parsed JSON) |
207
+ | `.enabled?` | `bool` (true if flag is on) |
208
+ | `.set?` | `bool` (true if a value has been set) |
209
+ | `.exists?` | `bool` (true if the feature exists in the repository) |
210
+ | `.present?` | `bool` (alias for `exists?`) |
211
+
212
+ ## Feature interceptors
213
+
214
+ Interceptors let you override feature values at runtime without changing the repository. They are evaluated before rollout strategies.
215
+
216
+ ### Environment variable interceptor
217
+
218
+ Override any feature at runtime using environment variables:
219
+
220
+ ```
221
+ FEATUREHUB_OVERRIDE_FEATURES=true
222
+ FEATUREHUB_MY_FEATURE=true
223
+ FEATUREHUB_SUBMIT_COLOR_BUTTON=green
224
+ ```
225
+
226
+ ```ruby
227
+ config.repository.register_interceptor(FeatureHub::Sdk::EnvironmentInterceptor.new)
228
+ ```
229
+
230
+ ### Local YAML interceptor
231
+
232
+ Override features from a YAML file. Useful during development or testing:
233
+
234
+ ```yaml
235
+ # featurehub-overrides.yaml
236
+ flagValues:
237
+ MY_FEATURE: true
238
+ SUBMIT_COLOR_BUTTON: green
239
+ MAX_RETRIES: 3
240
+ ```
241
+
242
+ All options are passed as a single hash:
243
+
244
+ ```ruby
245
+ # Default file path (featurehub-features.yaml or FEATUREHUB_LOCAL_YAML env var)
246
+ config.repository.register_interceptor(FeatureHub::Sdk::LocalYamlValueInterceptor.new)
247
+
248
+ # Explicit file path
249
+ config.repository.register_interceptor(
250
+ FeatureHub::Sdk::LocalYamlValueInterceptor.new(filename: "path/to/overrides.yaml")
251
+ )
252
+
253
+ # Watch for file changes and reload automatically
254
+ config.repository.register_interceptor(
255
+ FeatureHub::Sdk::LocalYamlValueInterceptor.new(watch: true, watch_interval: 5)
256
+ )
257
+
258
+ # With a custom logger
259
+ config.repository.register_interceptor(
260
+ FeatureHub::Sdk::LocalYamlValueInterceptor.new(filename: "overrides.yaml", logger: my_logger)
261
+ )
262
+ ```
263
+
264
+ Supported options: `:filename`, `:watch` (default: `false`), `:watch_interval` (seconds, default: `5`), `:logger`.
265
+
266
+ ## Offline / local-only mode with LocalYamlStore
143
267
 
268
+ `LocalYamlStore` loads features from a YAML file directly into the repository, with no Edge connection required. It uses the same file format as `LocalYamlValueInterceptor`. This is useful for tests, CI environments, or services that manage their own feature state.
144
269
 
145
- See more options to request feature states [here](https://github.com/featurehub-io/featurehub-ruby-sdk/blob/main/featurehub-sdk/lib/feature_hub/sdk/context.rb)
270
+ ```yaml
271
+ # features.yaml
272
+ flagValues:
273
+ MY_FLAG: true
274
+ SUBMIT_COLOR_BUTTON: green
275
+ MAX_RETRIES: 3
276
+ PRICING_CONFIG:
277
+ base: 9.99
278
+ tiers: [19.99, 49.99]
279
+ ```
146
280
 
147
- ### Using inside popular web servers
281
+ ```ruby
282
+ repository = FeatureHub::Sdk::FeatureHubRepository.new
148
283
 
149
- Because most of the popular webservers use a process per request distributed request distribution model, they
150
- will generally fork the process when they need more processes to handle the incoming traffic, and this will naturally
151
- kill the connection to FeatureHub. It does not however reset the cached repository. To ensure your fork is back
152
- up in running, for various frameworks you will need to ensure the Edge connection is restarted. This consists of
284
+ # Default file path (featurehub-features.yaml or FEATUREHUB_LOCAL_YAML env var)
285
+ store = FeatureHub::Sdk::LocalYamlStore.new(repository)
286
+
287
+ # Explicit file path
288
+ store = FeatureHub::Sdk::LocalYamlStore.new(repository, filename: "features.yaml")
289
+
290
+ repository.feature("MY_FLAG").flag # => true
291
+ repository.value("SUBMIT_COLOR_BUTTON") # => "green"
292
+ ```
293
+
294
+ The file path defaults to `featurehub-overrides.yaml` or the `FEATUREHUB_LOCAL_YAML` environment variable. Complex values (hashes, arrays) are serialised to a JSON string and stored as a `JSON` feature type.
295
+
296
+ ## Caching feature state in Redis
297
+
298
+ `RedisSessionStore` persists feature values from a `FeatureHubRepository` to Redis. On startup it replays cached features into the repository, then listens for live updates and writes newer versions back. A background timer re-reads all features periodically so updates from other processes are picked up automatically.
299
+
300
+ > **Warning:** Do not use `RedisSessionStore` with server-evaluated features. Each server-evaluated context resolves to different values; sharing a single Redis key across processes will cause them to overwrite each other's state.
301
+
302
+ ```ruby
303
+ # Requires the 'redis' gem: gem 'redis', '~> 5'
304
+ store = FeatureHub::Sdk::RedisSessionStore.new(
305
+ "redis://localhost:6379",
306
+ config.repository,
307
+ {
308
+ prefix: "myapp", # Redis key prefix (default: "featurehub")
309
+ namespace: 0, # Redis DB index (default: 0)
310
+ timeout: 60, # Seconds between periodic reloads (default: 30)
311
+ password: "secret", # Optional Redis password
312
+ logger: my_logger # Optional logger (default: SDK default logger)
313
+ }
314
+ )
315
+
316
+ # Register it so it also receives live updates
317
+ config.register_raw_update_listener(store)
318
+
319
+ # Shut down cleanly
320
+ store.close
321
+ ```
322
+
323
+ Redis keys used:
324
+ - `{prefix}_ids` — a Redis SET of feature IDs
325
+ - `{prefix}_{id}` — the JSON-encoded feature state for each feature
326
+
327
+ ## Custom raw update listeners
328
+
329
+ `RawUpdateFeatureListener` is a base class you can subclass to observe every raw feature update that flows through the repository, regardless of source. Register an instance with the repository (or config) and override only the callbacks you need:
330
+
331
+ ```ruby
332
+ class MyAuditListener < FeatureHub::Sdk::RawUpdateFeatureListener
333
+ def process_updates(features, source)
334
+ features.each { |f| Rails.logger.info("bulk update from #{source}: #{f["key"]}") }
335
+ end
336
+
337
+ def process_update(feature, source)
338
+ Rails.logger.info("single update from #{source}: #{feature["key"]}")
339
+ end
340
+
341
+ def delete_feature(feature, source)
342
+ Rails.logger.warn("deleted from #{source}: #{feature["key"]}")
343
+ end
344
+ end
345
+
346
+ config.register_raw_update_listener(MyAuditListener.new)
347
+ ```
348
+
349
+ Callbacks are dispatched asynchronously via `Concurrent::Future`. The `source` parameter will be `"streaming"`, `"polling"`, `"local-yaml"`, `"redis-store"`, or `"unknown"`.
350
+
351
+ All listeners are closed automatically when `config.close` or `repository.close` is called.
352
+
353
+ ## Using inside popular web servers
354
+
355
+ Most popular web servers fork processes to handle traffic. Forking kills the Edge connection but preserves the cached repository. Call `force_new_edge_service` in your framework's post-fork hook to restart the connection:
153
356
 
154
357
  ```ruby
155
358
  config.force_new_edge_service
156
359
  ```
157
360
 
158
- #### Resetting in Passenger
361
+ #### Passenger
159
362
 
160
- In your `config.ru`
363
+ In `config.ru`:
161
364
 
162
365
  ```ruby
163
366
  if defined?(PhusionPassenger)
164
367
  PhusionPassenger.on_event(:starting_worker_process) do |forked|
165
- if forked
166
- # e.g.
167
- # App.settings.fh_config.force_new_edge_service
168
- end
368
+ App.settings.fh_config.force_new_edge_service if forked
169
369
  end
170
370
  end
171
-
172
371
  ```
173
372
 
174
- #### Resetting in Puma
373
+ #### Puma
175
374
 
176
375
  ```ruby
177
376
  on_worker_boot do
178
- # e.g.
179
- # App.settings.fh_config.force_new_edge_service
377
+ App.settings.fh_config.force_new_edge_service
180
378
  end
181
-
182
379
  ```
183
380
 
184
- #### Resetting in Unicorn
381
+ #### Unicorn
185
382
 
186
383
  ```ruby
187
384
  after_fork do |_server, _worker|
188
- # e.g.
189
- # App.settings.fh_config.force_new_edge_service
385
+ App.settings.fh_config.force_new_edge_service
190
386
  end
191
-
192
387
  ```
193
388
 
194
- #### Resetting in Spring
389
+ #### Spring
195
390
 
196
391
  ```ruby
197
- Spring.after_fork do
198
- # e.g.
199
- # App.settings.fh_config.force_new_edge_service
392
+ Spring.after_fork do
393
+ App.settings.fh_config.force_new_edge_service
200
394
  end
201
-
202
395
  ```
203
-
204
396
 
205
- ### Extracting the state
397
+ ## Extracting and restoring state
206
398
 
207
- You can extract the state from a repository and store it somewhere and reload
208
- it, but it should be done so using the JSON mechanism so it parses correctly.
399
+ You can snapshot the repository state and reload it later (e.g. as a warm-start cache):
209
400
 
210
401
  ```ruby
211
402
  require 'json'
212
403
 
404
+ # Snapshot
213
405
  state = config.repository.extract_feature_state
214
-
215
- # somehow save it
216
406
  save(state.to_json)
217
407
 
218
- # some later stage, reload it or use it as a cache
408
+ # Restore
219
409
  config.repository.notify(:features, JSON.parse(read_state))
220
410
  ```
221
411
 
222
- ### Readyness
223
-
224
- It is encourage that you include the ready state of the repository in your
225
- readyness check. If your server cannot connect to your FeatureHub repository
226
- and cannot sensibly operate without it, it is not ready. Once it has received
227
- initial state it will remain ready even when it temporarily loses connections.
412
+ ## Readiness
228
413
 
229
- It is only if the key is invalid, or if the repository has never received state,
230
- that the repository is marked not ready. To determine readyness:
414
+ It is recommended to include the repository's ready state in your health/readiness check. The repository becomes ready once it has received its first successful update, and stays ready even through temporary connection loss. It is only not ready if the API key is invalid or no state has ever been received:
231
415
 
232
416
  ```ruby
233
417
  config.repository.ready?
234
418
  ```
235
419
 
420
+ ## Examples
421
+
422
+ Check our example Sinatra app [here](https://github.com/featurehub-io/featurehub-ruby-sdk/tree/main/example/sinatra).
@@ -1 +1 @@
1
- 3.0.6
1
+ 3.3.10
@@ -1,4 +1,4 @@
1
- FROM ruby:3.1.2
1
+ FROM ruby:3.3.10-slim
2
2
 
3
3
  LABEL Author="info@featurehub.io"
4
4
 
@@ -0,0 +1,7 @@
1
+ /examples/rails_example
2
+ /.github
3
+ /bin
4
+ /featurehub-sdk
5
+ /spec
6
+ /sig
7
+ /*.md
@@ -1 +1 @@
1
- 3.0.6
1
+ 3.3.10
@@ -1,42 +1,31 @@
1
- FROM ruby:2.7-bullseye
1
+ FROM ruby:3.3.10-bookworm
2
2
 
3
3
  MAINTAINER info@featurehub.io
4
4
  ENV BUNDLER_VERSION 2.3.15
5
5
  ARG DEBIAN_FRONTEND=noninteractive
6
6
 
7
7
  RUN apt update && \
8
- apt install -y gnupg
8
+ apt install -y gnupg wget tzdata apt-transport-https dirmngr gnupg curl && \
9
+ curl https://oss-binaries.phusionpassenger.com/auto-software-signing-gpg-key-2025.txt | gpg --dearmor | tee /etc/apt/trusted.gpg.d/phusion.gpg >/dev/null && \
10
+ sh -c 'echo deb https://oss-binaries.phusionpassenger.com/apt/passenger bookworm main > /etc/apt/sources.list.d/passenger.list' && \
11
+ apt-get update && \
12
+ apt-get install -y nginx passenger
9
13
 
10
- RUN apt-get update && \
11
- apt-get upgrade -y && \
12
- apt-get install -y wget tzdata apt-transport-https && \
13
- apt-get remove -y mysql-common
14
-
15
- # set up nsswitch
16
- COPY conf/nsswitch.conf /etc/nsswitch.conf
14
+ #ENV BUNDLE_PATH /bundle
15
+ RUN passenger-config build-native-support
17
16
 
18
17
  RUN echo 'gem: --no-document' >> ~/.gemrc && \
19
- gem update --system 3.0.6 && \
18
+ gem update --system && \
20
19
  gem install bundler -v ${BUNDLER_VERSION} --force
21
20
 
22
- RUN echo "deb https://oss-binaries.phusionpassenger.com/apt/passenger bullseye main" > /etc/apt/sources.list.d/passenger.list
23
-
24
- RUN apt-key adv --recv-keys --keyserver hkp://keyserver.ubuntu.com:80 0xcbcb082a1bb943db 561F9B9CAC40B2F7 && \
25
- apt-get update && \
26
- apt-get install -y libnginx-mod-http-passenger=1:6.0.13-1~bullseye1 \
27
- passenger=1:6.0.13-1~bullseye1 nginx && \
28
- apt-get clean -y && \
29
- rm -rf /var/cache/apt/archives/* /var/lib/apt/lists/*
30
- # set up passenger
21
+ # set up nsswitch
22
+ COPY examples/sinatra/conf/nsswitch.conf /etc/nsswitch.conf
31
23
 
32
- #ENV BUNDLE_PATH /bundle
33
- RUN passenger-config build-native-support
34
- RUN gem update --system
35
24
  RUN mkdir -p /app/featurehub
36
- COPY conf/nginx.conf /etc/nginx/nginx.conf
37
- COPY Gemfile Gemfile.lock /app/featurehub/
25
+ COPY examples/sinatra/conf/nginx.conf /etc/nginx/nginx.conf
26
+ COPY . /app/
38
27
  WORKDIR /app/featurehub
39
28
  RUN cd /app/featurehub && bundle install
40
- ADD . /app/featurehub
29
+ ADD examples/sinatra/ /app/featurehub
41
30
 
42
31
  CMD /usr/sbin/nginx -g \'daemon off;\'
@@ -2,13 +2,14 @@
2
2
 
3
3
  source "https://rubygems.org"
4
4
 
5
- ruby "2.7.6"
5
+ ruby "3.3.10"
6
6
 
7
+ gem "featurehub-sdk", path: "../.."
7
8
  gem "rack"
9
+ gem "redis"
8
10
  gem "sinatra"
9
- # gem "featurehub-sdk", path: "../../"
10
- gem "featurehub-sdk", git: "https://github.com/featurehub-io/featurehub-ruby-sdk.git",
11
- glob: "featurehub-sdk/*.gemspec"
11
+ # gem "featurehub-sdk", git: "https://github.com/featurehub-io/featurehub-ruby-sdk.git",
12
+ # glob: "featurehub-sdk/*.gemspec"
12
13
 
13
14
  group :development do
14
15
  gem "shotgun"