featurehub-sdk 1.3.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.claude/CLAUDE.md +85 -0
- data/.ruby-version +1 -1
- data/CHANGELOG.md +13 -1
- data/Gemfile +1 -1
- data/Gemfile.lock +20 -8
- data/README.md +306 -119
- data/examples/rails_example/.ruby-version +1 -1
- data/examples/rails_example/Dockerfile +1 -1
- data/examples/sinatra/.dockerignore +7 -0
- data/examples/sinatra/.ruby-version +1 -1
- data/examples/sinatra/Dockerfile +14 -25
- data/examples/sinatra/Gemfile +5 -4
- data/examples/sinatra/Gemfile.lock +40 -32
- data/examples/sinatra/app/application.rb +21 -9
- data/examples/sinatra/docker-compose.yaml +24 -0
- data/examples/sinatra/feature-flags.yaml +6 -0
- data/examples/sinatra/sinatra.iml +35 -14
- data/examples/sinatra/start.sh +2 -0
- data/featurehub-sdk.gemspec +4 -1
- data/lib/feature_hub/sdk/context.rb +28 -7
- data/lib/feature_hub/sdk/feature_hub_config.rb +68 -12
- data/lib/feature_hub/sdk/feature_repository.rb +52 -13
- data/lib/feature_hub/sdk/{feature_state.rb → feature_state_holder.rb} +13 -9
- data/lib/feature_hub/sdk/interceptors.rb +10 -6
- data/lib/feature_hub/sdk/internal_feature_repository.rb +7 -3
- data/lib/feature_hub/sdk/local_yaml_interceptor.rb +99 -0
- data/lib/feature_hub/sdk/local_yaml_store.rb +71 -0
- data/lib/feature_hub/sdk/poll_edge_service.rb +6 -11
- data/lib/feature_hub/sdk/raw_update_feature_listener.rb +19 -0
- data/lib/feature_hub/sdk/redis_session_store.rb +130 -0
- data/lib/feature_hub/sdk/strategy_attributes.rb +7 -0
- data/lib/feature_hub/sdk/streaming_edge_service.rb +4 -6
- data/lib/feature_hub/sdk/version.rb +2 -2
- data/lib/featurehub-sdk.rb +5 -1
- data/sig/feature_hub/featurehub.rbs +127 -28
- metadata +27 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c3c5127a58f5d1a4c63242919d593399ee0e097dcbd54239584d3620eb00f5a9
|
|
4
|
+
data.tar.gz: 4de3a2a249d35c6dfd52249a6b42171cd27e9aac8ed4c95f2c0efdaa9947663b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9a41c73c154804782a9795337e33de707a482f2fedb41337783222f561ce1eccae41dfca59ad393a36d9fa8d214f8f54778b016092de3c04e4c9d83c9da44a95
|
|
7
|
+
data.tar.gz: aac1dfb0a3d61d77f4046b9710f146ce0f2d0562a7c324e8e03c43c7276c5eb555322f9b3e49543aeb9958b1b4328b15dc003c29da682160a3b29ac782c830af
|
data/.claude/CLAUDE.md
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# CLAUDE.md
|
|
2
|
+
|
|
3
|
+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
4
|
+
|
|
5
|
+
## Commands
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
bundle install # Install dependencies
|
|
9
|
+
bundle exec rake # Run tests + linting (default)
|
|
10
|
+
bundle exec rspec # Run tests only
|
|
11
|
+
bundle exec rubocop # Lint only
|
|
12
|
+
|
|
13
|
+
# Run a single test file
|
|
14
|
+
bundle exec rspec spec/feature_hub/sdk/feature_hub_config_spec.rb
|
|
15
|
+
|
|
16
|
+
# Run a specific test by line number
|
|
17
|
+
bundle exec rspec spec/feature_hub/sdk/feature_hub_config_spec.rb:42
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Architecture
|
|
21
|
+
|
|
22
|
+
This is the FeatureHub Ruby SDK (gem: `featurehub-sdk`, version 2.0.0), targeting Ruby >= 3.2. All code lives under `lib/feature_hub/sdk/` and is namespaced as `FeatureHub::Sdk`.
|
|
23
|
+
|
|
24
|
+
### Core Data Flow
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
FeatureHubConfig.init(url, api_key)
|
|
28
|
+
→ creates EdgeService (streaming SSE or polling HTTP)
|
|
29
|
+
→ edge service receives feature JSON from FeatureHub server
|
|
30
|
+
→ FeatureHubRepository stores feature states
|
|
31
|
+
|
|
32
|
+
config.new_context().user_key("id").build()
|
|
33
|
+
→ ClientEvalContext: polls edge, evaluates strategies locally
|
|
34
|
+
→ ServerEvalContext: sends context as HTTP header, server evaluates
|
|
35
|
+
|
|
36
|
+
context.feature("MY_FLAG").boolean
|
|
37
|
+
→ FeatureStateHolder checks interceptors, then calls repository.apply(strategies, ctx)
|
|
38
|
+
→ ApplyFeature matches rollout strategies, returns Applied(value)
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### Key Classes
|
|
42
|
+
|
|
43
|
+
- **FeatureHubConfig** ([feature_hub_config.rb](lib/feature_hub/sdk/feature_hub_config.rb)): Entry point. Detects client vs server evaluation by checking for `*` in the API key. Manages edge service lifecycle. Call `new_context()` to get a context builder.
|
|
44
|
+
|
|
45
|
+
- **FeatureHubRepository** ([feature_repository.rb](lib/feature_hub/sdk/feature_repository.rb)): Stores features as a hash (symbol keys → `FeatureStateHolder`). Notifies listeners on changes (`:features`, `:feature`, `:delete_feature`, `:failed`). Checks interceptors before applying strategies. Tracks readiness via `@ready`.
|
|
46
|
+
|
|
47
|
+
- **FeatureStateHolder** ([feature_state_holder.rb](lib/feature_hub/sdk/feature_state_holder.rb)): Wraps feature JSON (`key`, `id`, `type`, `value`, `version`, `l` (locked), `strategies`). Provides typed accessors: `.flag`, `.string`, `.number`, `.raw_json`, `.boolean`. Supports `with_context(ctx)` for strategy evaluation.
|
|
48
|
+
|
|
49
|
+
- **Context classes** ([context.rb](lib/feature_hub/sdk/context.rb)): Fluent builder pattern — `context.user_key("x").platform("ios").country("gb").build()`. Attributes stored as `symbol → [array]`. Two subclasses: `ClientEvalFeatureContext` (local strategy eval) and `ServerEvalFeatureContext` (server-side eval, sends `x-featurehub` header).
|
|
50
|
+
|
|
51
|
+
- **Edge services**: `StreamingEdgeService` ([streaming_edge_service.rb](lib/feature_hub/sdk/streaming_edge_service.rb)) uses SSE via `ld-eventsource`. `PollingEdgeService` ([poll_edge_service.rb](lib/feature_hub/sdk/poll_edge_service.rb)) uses Faraday with `Concurrent::TimerTask`, supports ETags. Call `force_new_edge_service()` after process fork (Puma/Passenger/Unicorn).
|
|
52
|
+
|
|
53
|
+
- **ApplyFeature** ([impl/apply_features.rb](lib/feature_hub/sdk/impl/apply_features.rb)): Client-side strategy evaluator. Iterates rollout strategies, calculates percentage allocation via Murmur3 hash of `percentage_key + feature_id`, matches attribute conditions via `MatcherRegistry`. Returns `Applied(matched:, value:)`.
|
|
54
|
+
|
|
55
|
+
- **EnvironmentInterceptor** ([interceptors.rb](lib/feature_hub/sdk/interceptors.rb)): Override features at runtime via `FEATUREHUB_OVERRIDE_FEATURES=true` + `FEATUREHUB_<FEATURE_NAME>=<value>` env vars.
|
|
56
|
+
|
|
57
|
+
### Client vs Server Evaluation
|
|
58
|
+
|
|
59
|
+
- **Client-evaluated** API key contains `*` (e.g., `abc*def`): full strategy data sent to SDK, evaluated locally by `ApplyFeature`.
|
|
60
|
+
- **Server-evaluated** API key has no `*`: context attributes sent as `x-featurehub` HTTP header, server evaluates and returns resolved values.
|
|
61
|
+
|
|
62
|
+
### Strategy Attribute Matchers
|
|
63
|
+
|
|
64
|
+
`MatcherRegistry` dispatches to typed matchers based on `field_type`: `BOOLEAN`, `STRING`/`DATE`/`DATE_TIME` (via `StringMatcher`), `NUMBER`, `SEMANTIC_VERSION` (uses `sem_version` gem), `IP_ADDRESS` (CIDR support). All conditions in a strategy must match for the strategy to apply.
|
|
65
|
+
|
|
66
|
+
## RBS Type Signatures
|
|
67
|
+
|
|
68
|
+
Type definitions live in [sig/feature_hub/featurehub.rbs](sig/feature_hub/featurehub.rbs). Key signatures to be aware of:
|
|
69
|
+
|
|
70
|
+
- **Feature value type**: `[bool? | String? | Float?]` — the union type used throughout for feature values (`Applied#value`, `FeatureStateHolder#value`, `RolloutStrategy#value`, `InterceptorValue`)
|
|
71
|
+
- **`FeatureStateHolder#initialize`** takes `key:`, `repo:`, `feature_state:`, `parent_state:`, and `ctx:` — the `parent_state` and `ctx` support the `with_context` pattern for context-scoped evaluation
|
|
72
|
+
- **`InternalFeatureRepository`** is the abstract interface that `FeatureHubRepository` implements; `FeatureStateHolder` and context classes depend on this interface, not the concrete class
|
|
73
|
+
- **`ClientContext#build`** returns `ClientContext` (async); **`build_sync`** also returns `ClientContext` (blocking)
|
|
74
|
+
- **`FeatureHubConfig#repository`** takes an optional `InternalFeatureRepository?` and returns one — it acts as both getter and setter
|
|
75
|
+
- **`RolloutStrategyAttribute`**: `values` is `Array[[bool? | String? | Float?]]`; use `float_values` / `str_values` for typed access
|
|
76
|
+
- **`RolloutStrategyCondition`**: predicate methods only (e.g., `equals?`, `regex?`, `includes?`) — no raw string comparison against condition type strings
|
|
77
|
+
|
|
78
|
+
## Conventions
|
|
79
|
+
|
|
80
|
+
- `# frozen_string_literal: true` on every file
|
|
81
|
+
- Double-quoted strings (RuboCop enforced)
|
|
82
|
+
- Feature keys stored/looked up as symbols internally
|
|
83
|
+
- RuboCop: max line length 120, metrics cops disabled, documentation disabled
|
|
84
|
+
- Tests mirror lib structure: `spec/feature_hub/sdk/**/*_spec.rb`
|
|
85
|
+
- Use `instance_double` for mocking, `aggregate_failures` for multiple assertions
|
data/.ruby-version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
3.
|
|
1
|
+
3.3.10
|
data/CHANGELOG.md
CHANGED
|
@@ -1,4 +1,16 @@
|
|
|
1
|
-
## [
|
|
1
|
+
## [2.0.0] - 2026-03-22
|
|
2
|
+
|
|
3
|
+
- Refactor FeatureState to FeatureStateHolder to be consistent with other SDKs
|
|
4
|
+
- Add FeatureValueType to reduce duplication
|
|
5
|
+
- Add local YAML file interceptor with an optional timer to watch for changes
|
|
6
|
+
- Add `RawUpdateFeatureListener` base class so custom listeners can react to raw edge updates (including update source tracking)
|
|
7
|
+
- Add `LocalYamlStore`: load features from a local YAML file without an Edge server, as an alternative to the value interceptor
|
|
8
|
+
- Add `RedisSessionStore`: persist features in Redis so they survive process restarts and are shared across processes (client-evaluated only)
|
|
9
|
+
- Support punch-through context evaluation for external stores so per-request context attributes are applied even when features come from Redis or YAML
|
|
10
|
+
- Add `value` accessor to `FeatureStateHolder` as a convenience shortcut alongside the typed accessors
|
|
11
|
+
- Edge services now explicitly close the repository on shutdown
|
|
12
|
+
|
|
13
|
+
## [1.3.0] - 2026-01-11
|
|
2
14
|
|
|
3
15
|
- update gem dependencies
|
|
4
16
|
- bump minimum ruby version to 3.2
|
data/Gemfile
CHANGED
data/Gemfile.lock
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
featurehub-sdk (
|
|
4
|
+
featurehub-sdk (2.0.0)
|
|
5
5
|
concurrent-ruby (~> 1.3)
|
|
6
6
|
faraday (~> 2)
|
|
7
|
-
ld-eventsource (~> 2.
|
|
7
|
+
ld-eventsource (~> 2.5.1)
|
|
8
8
|
murmurhash3 (~> 0.1.7)
|
|
9
9
|
sem_version (~> 2.0.0)
|
|
10
10
|
|
|
@@ -15,6 +15,7 @@ GEM
|
|
|
15
15
|
public_suffix (>= 2.0.2, < 8.0)
|
|
16
16
|
ast (2.4.3)
|
|
17
17
|
concurrent-ruby (1.3.6)
|
|
18
|
+
connection_pool (3.0.2)
|
|
18
19
|
diff-lcs (1.6.2)
|
|
19
20
|
docile (1.4.1)
|
|
20
21
|
domain_name (0.6.20240107)
|
|
@@ -25,6 +26,7 @@ GEM
|
|
|
25
26
|
faraday-net_http (3.4.2)
|
|
26
27
|
net-http (~> 0.5)
|
|
27
28
|
ffi (1.17.3-arm64-darwin)
|
|
29
|
+
ffi (1.17.3-x86_64-darwin)
|
|
28
30
|
ffi (1.17.3-x86_64-linux-gnu)
|
|
29
31
|
ffi-compiler (1.3.2)
|
|
30
32
|
ffi (>= 1.15.5)
|
|
@@ -37,9 +39,9 @@ GEM
|
|
|
37
39
|
http-cookie (1.1.0)
|
|
38
40
|
domain_name (~> 0.5)
|
|
39
41
|
http-form_data (2.3.0)
|
|
40
|
-
json (2.
|
|
42
|
+
json (2.19.2)
|
|
41
43
|
language_server-protocol (3.17.0.5)
|
|
42
|
-
ld-eventsource (2.
|
|
44
|
+
ld-eventsource (2.5.1)
|
|
43
45
|
concurrent-ruby (~> 1.0)
|
|
44
46
|
http (>= 4.4.1, < 6.0.0)
|
|
45
47
|
lint_roller (1.1.0)
|
|
@@ -59,6 +61,10 @@ GEM
|
|
|
59
61
|
racc (1.8.1)
|
|
60
62
|
rainbow (3.1.1)
|
|
61
63
|
rake (13.3.1)
|
|
64
|
+
redis (5.4.1)
|
|
65
|
+
redis-client (>= 0.22.0)
|
|
66
|
+
redis-client (0.28.0)
|
|
67
|
+
connection_pool
|
|
62
68
|
regexp_parser (2.11.3)
|
|
63
69
|
rspec (3.13.2)
|
|
64
70
|
rspec-core (~> 3.13.0)
|
|
@@ -102,15 +108,17 @@ GEM
|
|
|
102
108
|
|
|
103
109
|
PLATFORMS
|
|
104
110
|
arm64-darwin
|
|
111
|
+
x86_64-darwin-24
|
|
105
112
|
x86_64-linux
|
|
106
113
|
|
|
107
114
|
DEPENDENCIES
|
|
108
115
|
concurrent-ruby (~> 1.3)
|
|
109
116
|
faraday (~> 2)
|
|
110
117
|
featurehub-sdk!
|
|
111
|
-
ld-eventsource (~> 2.
|
|
118
|
+
ld-eventsource (~> 2.5.1)
|
|
112
119
|
murmurhash3 (~> 0.1.7)
|
|
113
120
|
rake (~> 13.0)
|
|
121
|
+
redis (~> 5)
|
|
114
122
|
rspec (~> 3.0)
|
|
115
123
|
rubocop (~> 1.21)
|
|
116
124
|
sem_version (~> 2.0.0)
|
|
@@ -120,21 +128,23 @@ CHECKSUMS
|
|
|
120
128
|
addressable (2.8.8) sha256=7c13b8f9536cf6364c03b9d417c19986019e28f7c00ac8132da4eb0fe393b057
|
|
121
129
|
ast (2.4.3) sha256=954615157c1d6a382bc27d690d973195e79db7f55e9765ac7c481c60bdb4d383
|
|
122
130
|
concurrent-ruby (1.3.6) sha256=6b56837e1e7e5292f9864f34b69c5a2cbc75c0cf5338f1ce9903d10fa762d5ab
|
|
131
|
+
connection_pool (3.0.2) sha256=33fff5ba71a12d2aa26cb72b1db8bba2a1a01823559fb01d29eb74c286e62e0a
|
|
123
132
|
diff-lcs (1.6.2) sha256=9ae0d2cba7d4df3075fe8cd8602a8604993efc0dfa934cff568969efb1909962
|
|
124
133
|
docile (1.4.1) sha256=96159be799bfa73cdb721b840e9802126e4e03dfc26863db73647204c727f21e
|
|
125
134
|
domain_name (0.6.20240107) sha256=5f693b2215708476517479bf2b3802e49068ad82167bcd2286f899536a17d933
|
|
126
135
|
faraday (2.14.0) sha256=8699cfe5d97e55268f2596f9a9d5a43736808a943714e3d9a53e6110593941cd
|
|
127
136
|
faraday-net_http (3.4.2) sha256=f147758260d3526939bf57ecf911682f94926a3666502e24c69992765875906c
|
|
128
|
-
featurehub-sdk (
|
|
137
|
+
featurehub-sdk (2.0.0)
|
|
129
138
|
ffi (1.17.3-arm64-darwin) sha256=0c690555d4cee17a7f07c04d59df39b2fba74ec440b19da1f685c6579bb0717f
|
|
139
|
+
ffi (1.17.3-x86_64-darwin) sha256=1f211811eb5cfaa25998322cdd92ab104bfbd26d1c4c08471599c511f2c00bb5
|
|
130
140
|
ffi (1.17.3-x86_64-linux-gnu) sha256=3746b01f677aae7b16dc1acb7cb3cc17b3e35bdae7676a3f568153fb0e2c887f
|
|
131
141
|
ffi-compiler (1.3.2) sha256=a94f3d81d12caf5c5d4ecf13980a70d0aeaa72268f3b9cc13358bcc6509184a0
|
|
132
142
|
http (5.3.1) sha256=c50802d8e9be3926cb84ac3b36d1a31fbbac383bc4cbecdce9053cb604231d7d
|
|
133
143
|
http-cookie (1.1.0) sha256=38a5e60d1527eebc396831b8c4b9455440509881219273a6c99943d29eadbb19
|
|
134
144
|
http-form_data (2.3.0) sha256=cc4eeb1361d9876821e31d7b1cf0b68f1cf874b201d27903480479d86448a5f3
|
|
135
|
-
json (2.
|
|
145
|
+
json (2.19.2) sha256=e7e1bd318b2c37c4ceee2444841c86539bc462e81f40d134cf97826cb14e83cf
|
|
136
146
|
language_server-protocol (3.17.0.5) sha256=fd1e39a51a28bf3eec959379985a72e296e9f9acfce46f6a79d31ca8760803cc
|
|
137
|
-
ld-eventsource (2.
|
|
147
|
+
ld-eventsource (2.5.1) sha256=19cb44b7d3f91f76b7401a1b74293b32c723389b2d6f9d81ecb803c628ac1b25
|
|
138
148
|
lint_roller (1.1.0) sha256=2c0c845b632a7d172cb849cc90c1bce937a28c5c8ccccb50dfd46a485003cc87
|
|
139
149
|
llhttp-ffi (0.5.1) sha256=9a25a7fc19311f691a78c9c0ac0fbf4675adbd0cca74310228fdf841018fa7bc
|
|
140
150
|
logger (1.7.0) sha256=196edec7cc44b66cfb40f9755ce11b392f21f7967696af15d274dde7edff0203
|
|
@@ -147,6 +157,8 @@ CHECKSUMS
|
|
|
147
157
|
racc (1.8.1) sha256=4a7f6929691dbec8b5209a0b373bc2614882b55fc5d2e447a21aaa691303d62f
|
|
148
158
|
rainbow (3.1.1) sha256=039491aa3a89f42efa1d6dec2fc4e62ede96eb6acd95e52f1ad581182b79bc6a
|
|
149
159
|
rake (13.3.1) sha256=8c9e89d09f66a26a01264e7e3480ec0607f0c497a861ef16063604b1b08eb19c
|
|
160
|
+
redis (5.4.1) sha256=b5e675b57ad22b15c9bcc765d5ac26f60b675408af916d31527af9bd5a81faae
|
|
161
|
+
redis-client (0.28.0) sha256=888892f9cd8787a41c0ece00bdf5f556dfff7770326ce40bb2bc11f1bfec824b
|
|
150
162
|
regexp_parser (2.11.3) sha256=ca13f381a173b7a93450e53459075c9b76a10433caadcb2f1180f2c741fc55a4
|
|
151
163
|
rspec (3.13.2) sha256=206284a08ad798e61f86d7ca3e376718d52c0bc944626b2349266f239f820587
|
|
152
164
|
rspec-core (3.13.6) sha256=a8823c6411667b60a8bca135364351dda34cd55e44ff94c4be4633b37d828b2d
|