openfeature-sdk 0.6.1 → 0.6.3
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/.release-please-manifest.json +1 -1
- data/CHANGELOG.md +21 -0
- data/Gemfile +3 -0
- data/Gemfile.lock +39 -1
- data/README.md +60 -10
- data/Rakefile +5 -0
- data/cucumber.yml +2 -0
- data/lib/open_feature/sdk/api.rb +15 -1
- data/lib/open_feature/sdk/client.rb +59 -24
- data/lib/open_feature/sdk/client_metadata.rb +3 -1
- data/lib/open_feature/sdk/configuration.rb +48 -19
- data/lib/open_feature/sdk/evaluation_context_builder.rb +2 -2
- data/lib/open_feature/sdk/hooks/hook_executor.rb +23 -1
- data/lib/open_feature/sdk/hooks/logging_hook.rb +84 -0
- data/lib/open_feature/sdk/hooks.rb +1 -0
- data/lib/open_feature/sdk/provider/in_memory_provider.rb +3 -1
- data/lib/open_feature/sdk/thread_local_transaction_context_propagator.rb +19 -0
- data/lib/open_feature/sdk/transaction_context_propagator.rb +15 -0
- data/lib/open_feature/sdk/version.rb +1 -1
- data/lib/open_feature/sdk.rb +2 -0
- data/renovate.json +1 -1
- metadata +5 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: bd3ec1b654a844eaa7bcafe400d6d2075199fa49ba6823aad1c8e1ce8c05a526
|
|
4
|
+
data.tar.gz: 3d159cb4f4df832154cfa7a4657ee53f357827a3d55dcbe124bf4716248e24b8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 66667a224162e00b882ad73b45e5d2825cc35f47007ca961749cc7115cbe1223d881be0cfff89eb4a5d3fd3f0235c2548f7a89420de9ff7bb3a6b0fd4bcada65
|
|
7
|
+
data.tar.gz: f799ac085e5593bc9b14a040ebddc8326c57ff46e36285ed0ea9b44008dd0d8d58a149a1c0f7fc9be50aeb3cf8d60ffc2859f2b3f9099d49e1db3df6f795b669
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.6.3](https://github.com/open-feature/ruby-sdk/compare/v0.6.2...v0.6.3) (2026-03-07)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* close spec compliance gaps for OpenFeature v0.8.0 ([#237](https://github.com/open-feature/ruby-sdk/issues/237)) ([9a87d04](https://github.com/open-feature/ruby-sdk/commit/9a87d04d5f261ea06e073f405c15613db7099d8a))
|
|
9
|
+
* enable Gherkin feature tests ([#50](https://github.com/open-feature/ruby-sdk/issues/50)) ([#233](https://github.com/open-feature/ruby-sdk/issues/233)) ([95845ba](https://github.com/open-feature/ruby-sdk/commit/95845ba6ec26357d9c0895d310361e411f85da11))
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
### Bug Fixes
|
|
13
|
+
|
|
14
|
+
* close remaining MUST-level spec compliance gaps ([#238](https://github.com/open-feature/ruby-sdk/issues/238)) ([1d08491](https://github.com/open-feature/ruby-sdk/commit/1d084911964c8672dd66b23834eec6f14e453749))
|
|
15
|
+
|
|
16
|
+
## [0.6.2](https://github.com/open-feature/ruby-sdk/compare/v0.6.1...v0.6.2) (2026-03-07)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
### Features
|
|
20
|
+
|
|
21
|
+
* add logging hook (spec Appendix A) ([#229](https://github.com/open-feature/ruby-sdk/issues/229)) ([2f681c9](https://github.com/open-feature/ruby-sdk/commit/2f681c910198d2bfa16389018f42ca9dc3270936))
|
|
22
|
+
* add transaction context propagation (spec 3.3) ([#230](https://github.com/open-feature/ruby-sdk/issues/230)) ([0aff30f](https://github.com/open-feature/ruby-sdk/commit/0aff30f77a0b680341cfd3d1f43e9d1f0ede1b75))
|
|
23
|
+
|
|
3
24
|
## [0.6.1](https://github.com/open-feature/ruby-sdk/compare/v0.6.0...v0.6.1) (2026-03-05)
|
|
4
25
|
|
|
5
26
|
|
data/Gemfile
CHANGED
data/Gemfile.lock
CHANGED
|
@@ -1,12 +1,40 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
openfeature-sdk (0.6.
|
|
4
|
+
openfeature-sdk (0.6.3)
|
|
5
5
|
|
|
6
6
|
GEM
|
|
7
7
|
remote: https://rubygems.org/
|
|
8
8
|
specs:
|
|
9
9
|
ast (2.4.3)
|
|
10
|
+
base64 (0.3.0)
|
|
11
|
+
bigdecimal (4.0.1)
|
|
12
|
+
builder (3.3.0)
|
|
13
|
+
cucumber (10.2.0)
|
|
14
|
+
base64 (~> 0.2)
|
|
15
|
+
builder (~> 3.2)
|
|
16
|
+
cucumber-ci-environment (> 9, < 12)
|
|
17
|
+
cucumber-core (> 15, < 17)
|
|
18
|
+
cucumber-cucumber-expressions (> 17, < 20)
|
|
19
|
+
cucumber-html-formatter (> 21, < 23)
|
|
20
|
+
diff-lcs (~> 1.5)
|
|
21
|
+
logger (~> 1.6)
|
|
22
|
+
mini_mime (~> 1.1)
|
|
23
|
+
multi_test (~> 1.1)
|
|
24
|
+
sys-uname (~> 1.3)
|
|
25
|
+
cucumber-ci-environment (11.0.0)
|
|
26
|
+
cucumber-core (16.2.0)
|
|
27
|
+
cucumber-gherkin (> 36, < 40)
|
|
28
|
+
cucumber-messages (> 31, < 33)
|
|
29
|
+
cucumber-tag-expressions (> 6, < 9)
|
|
30
|
+
cucumber-cucumber-expressions (19.0.0)
|
|
31
|
+
bigdecimal
|
|
32
|
+
cucumber-gherkin (39.0.0)
|
|
33
|
+
cucumber-messages (>= 31, < 33)
|
|
34
|
+
cucumber-html-formatter (22.3.0)
|
|
35
|
+
cucumber-messages (> 23, < 33)
|
|
36
|
+
cucumber-messages (32.2.0)
|
|
37
|
+
cucumber-tag-expressions (8.1.0)
|
|
10
38
|
date (3.5.1)
|
|
11
39
|
debug (1.11.1)
|
|
12
40
|
irb (~> 1.10)
|
|
@@ -14,6 +42,7 @@ GEM
|
|
|
14
42
|
diff-lcs (1.6.2)
|
|
15
43
|
docile (1.4.1)
|
|
16
44
|
erb (6.0.2)
|
|
45
|
+
ffi (1.17.3)
|
|
17
46
|
io-console (0.8.2)
|
|
18
47
|
irb (1.17.0)
|
|
19
48
|
pp (>= 0.6.0)
|
|
@@ -23,7 +52,11 @@ GEM
|
|
|
23
52
|
json (2.18.1)
|
|
24
53
|
language_server-protocol (3.17.0.5)
|
|
25
54
|
lint_roller (1.1.0)
|
|
55
|
+
logger (1.7.0)
|
|
26
56
|
markly (0.15.2)
|
|
57
|
+
memoist3 (1.0.0)
|
|
58
|
+
mini_mime (1.1.5)
|
|
59
|
+
multi_test (1.1.0)
|
|
27
60
|
parallel (1.27.0)
|
|
28
61
|
parser (3.3.10.2)
|
|
29
62
|
ast (~> 2.4.1)
|
|
@@ -100,6 +133,9 @@ GEM
|
|
|
100
133
|
lint_roller (~> 1.1)
|
|
101
134
|
rubocop-performance (~> 1.26.0)
|
|
102
135
|
stringio (3.2.0)
|
|
136
|
+
sys-uname (1.5.0)
|
|
137
|
+
ffi (~> 1.1)
|
|
138
|
+
memoist3 (~> 1.0.0)
|
|
103
139
|
timecop (0.9.10)
|
|
104
140
|
tsort (0.2.0)
|
|
105
141
|
unicode-display_width (3.2.0)
|
|
@@ -120,7 +156,9 @@ PLATFORMS
|
|
|
120
156
|
x86_64-linux
|
|
121
157
|
|
|
122
158
|
DEPENDENCIES
|
|
159
|
+
cucumber (~> 10.0)
|
|
123
160
|
debug
|
|
161
|
+
logger
|
|
124
162
|
markly
|
|
125
163
|
openfeature-sdk!
|
|
126
164
|
rake (~> 13.0)
|
data/README.md
CHANGED
|
@@ -17,8 +17,8 @@
|
|
|
17
17
|
</a>
|
|
18
18
|
<!-- x-release-please-start-version -->
|
|
19
19
|
|
|
20
|
-
<a href="https://github.com/open-feature/ruby-sdk/releases/tag/v0.6.
|
|
21
|
-
<img alt="Release" src="https://img.shields.io/static/v1?label=release&message=v0.6.
|
|
20
|
+
<a href="https://github.com/open-feature/ruby-sdk/releases/tag/v0.6.3">
|
|
21
|
+
<img alt="Release" src="https://img.shields.io/static/v1?label=release&message=v0.6.3&color=blue&style=for-the-badge" />
|
|
22
22
|
</a>
|
|
23
23
|
|
|
24
24
|
<!-- x-release-please-end -->
|
|
@@ -101,12 +101,12 @@ object = client.fetch_object_value(flag_key: 'object_value', default_value: { na
|
|
|
101
101
|
| ✅ | [Providers](#providers) | Integrate with a commercial, open source, or in-house feature management tool. |
|
|
102
102
|
| ✅ | [Targeting](#targeting) | Contextually-aware flag evaluation using [evaluation context](https://openfeature.dev/docs/reference/concepts/evaluation-context). |
|
|
103
103
|
| ✅ | [Hooks](#hooks) | Add functionality to various stages of the flag evaluation life-cycle. |
|
|
104
|
-
|
|
|
104
|
+
| ✅ | [Logging](#logging) | Integrate with popular logging packages. |
|
|
105
105
|
| ✅ | [Domains](#domains) | Logically bind clients with providers. |
|
|
106
106
|
| ✅ | [Eventing](#eventing) | React to state changes in the provider or flag management system. |
|
|
107
107
|
| ✅ | [Shutdown](#shutdown) | Gracefully clean up a provider during application shutdown. |
|
|
108
108
|
| ✅ | [Tracking](#tracking) | Associate user actions with feature flag evaluations for experimentation. |
|
|
109
|
-
|
|
|
109
|
+
| ✅ | [Transaction Context Propagation](#transaction-context-propagation) | Set a specific [evaluation context](https://openfeature.dev/docs/reference/concepts/evaluation-context) for a transaction (e.g. an HTTP request or a thread) |
|
|
110
110
|
| ✅ | [Extending](#extending) | Extend OpenFeature with custom providers and hooks. |
|
|
111
111
|
|
|
112
112
|
<sub>Implemented: ✅ | In-progress: ⚠️ | Not implemented yet: ❌</sub>
|
|
@@ -247,9 +247,27 @@ client.fetch_boolean_value(
|
|
|
247
247
|
|
|
248
248
|
### Logging
|
|
249
249
|
|
|
250
|
-
|
|
250
|
+
The SDK includes a built-in `LoggingHook` that provides structured log output for flag evaluations. It logs at the `before`, `after`, and `error` stages of the hook lifecycle.
|
|
251
251
|
|
|
252
|
-
|
|
252
|
+
```ruby
|
|
253
|
+
# Use the SDK's default logger (from Configuration)
|
|
254
|
+
OpenFeature::SDK.hooks << OpenFeature::SDK::Hooks::LoggingHook.new
|
|
255
|
+
|
|
256
|
+
# Or provide your own logger
|
|
257
|
+
logger = Logger.new($stdout)
|
|
258
|
+
OpenFeature::SDK.hooks << OpenFeature::SDK::Hooks::LoggingHook.new(logger: logger)
|
|
259
|
+
|
|
260
|
+
# Optionally include evaluation context in log output
|
|
261
|
+
OpenFeature::SDK.hooks << OpenFeature::SDK::Hooks::LoggingHook.new(
|
|
262
|
+
logger: logger,
|
|
263
|
+
include_evaluation_context: true
|
|
264
|
+
)
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
Log output uses a structured key=value format:
|
|
268
|
+
- **before** (DEBUG): `stage=before domain=my-domain provider_name=my-provider flag_key=my-flag default_value=false`
|
|
269
|
+
- **after** (DEBUG): includes `reason`, `variant`, and `value`
|
|
270
|
+
- **error** (ERROR): includes `error_code` and `error_message`
|
|
253
271
|
|
|
254
272
|
### Domains
|
|
255
273
|
|
|
@@ -361,12 +379,44 @@ Check the documentation for your [provider](#providers) for more information.
|
|
|
361
379
|
|
|
362
380
|
### Transaction Context Propagation
|
|
363
381
|
|
|
364
|
-
|
|
382
|
+
Transaction context is a container for transaction-specific evaluation context (e.g. user id, user agent, IP).
|
|
383
|
+
Transaction context can be set where specific data is available (e.g. an auth service or request handler) and by using the transaction context propagator it will automatically be applied to all flag evaluations within a transaction (e.g. a request or thread).
|
|
384
|
+
|
|
385
|
+
The SDK ships with a `ThreadLocalTransactionContextPropagator` that stores context in `Thread.current`:
|
|
386
|
+
|
|
387
|
+
```ruby
|
|
388
|
+
# Set up the propagator
|
|
389
|
+
OpenFeature::SDK.set_transaction_context_propagator(
|
|
390
|
+
OpenFeature::SDK::ThreadLocalTransactionContextPropagator.new
|
|
391
|
+
)
|
|
392
|
+
|
|
393
|
+
# Set transaction context (e.g. in a request middleware)
|
|
394
|
+
OpenFeature::SDK.set_transaction_context(
|
|
395
|
+
OpenFeature::SDK::EvaluationContext.new(targeting_key: "user-123", "email" => "user@example.com")
|
|
396
|
+
)
|
|
397
|
+
|
|
398
|
+
# Transaction context is automatically merged during flag evaluation.
|
|
399
|
+
# Merge precedence: invocation > client > transaction > API (global)
|
|
400
|
+
client = OpenFeature::SDK.build_client
|
|
401
|
+
client.fetch_boolean_value(flag_key: "my-flag", default_value: false)
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
You can implement a custom propagator by including the `TransactionContextPropagator` module:
|
|
365
405
|
|
|
366
|
-
|
|
367
|
-
|
|
406
|
+
```ruby
|
|
407
|
+
class MyRequestScopedPropagator
|
|
408
|
+
include OpenFeature::SDK::TransactionContextPropagator
|
|
368
409
|
|
|
369
|
-
|
|
410
|
+
def set_transaction_context(evaluation_context)
|
|
411
|
+
# Store context in your request-scoped storage
|
|
412
|
+
RequestStore[:openfeature_context] = evaluation_context
|
|
413
|
+
end
|
|
414
|
+
|
|
415
|
+
def get_transaction_context
|
|
416
|
+
RequestStore[:openfeature_context]
|
|
417
|
+
end
|
|
418
|
+
end
|
|
419
|
+
```
|
|
370
420
|
|
|
371
421
|
## Extending
|
|
372
422
|
|
data/Rakefile
CHANGED
data/cucumber.yml
ADDED
data/lib/open_feature/sdk/api.rb
CHANGED
|
@@ -34,7 +34,7 @@ module OpenFeature
|
|
|
34
34
|
include Singleton # Satisfies Flag Evaluation API Requirement 1.1.1
|
|
35
35
|
extend Forwardable
|
|
36
36
|
|
|
37
|
-
def_delegators :configuration, :provider, :set_provider, :set_provider_and_wait, :hooks, :evaluation_context
|
|
37
|
+
def_delegators :configuration, :provider, :set_provider, :set_provider_and_wait, :hooks, :add_hooks, :evaluation_context
|
|
38
38
|
|
|
39
39
|
def configuration
|
|
40
40
|
@configuration ||= Configuration.new
|
|
@@ -54,6 +54,11 @@ module OpenFeature
|
|
|
54
54
|
Client.new(provider: Provider::NoOpProvider.new, evaluation_context:)
|
|
55
55
|
end
|
|
56
56
|
|
|
57
|
+
def provider_metadata(domain: nil)
|
|
58
|
+
prov = provider(domain: domain)
|
|
59
|
+
prov.metadata if prov&.respond_to?(:metadata)
|
|
60
|
+
end
|
|
61
|
+
|
|
57
62
|
def add_handler(event_type, handler)
|
|
58
63
|
configuration.add_handler(event_type, handler)
|
|
59
64
|
end
|
|
@@ -70,6 +75,15 @@ module OpenFeature
|
|
|
70
75
|
configuration.logger = new_logger
|
|
71
76
|
end
|
|
72
77
|
|
|
78
|
+
def set_transaction_context_propagator(propagator)
|
|
79
|
+
configuration.transaction_context_propagator = propagator
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def set_transaction_context(evaluation_context)
|
|
83
|
+
propagator = configuration.transaction_context_propagator
|
|
84
|
+
propagator&.set_transaction_context(evaluation_context)
|
|
85
|
+
end
|
|
86
|
+
|
|
73
87
|
def shutdown
|
|
74
88
|
configuration.shutdown
|
|
75
89
|
end
|
|
@@ -28,6 +28,10 @@ module OpenFeature
|
|
|
28
28
|
@hooks = []
|
|
29
29
|
end
|
|
30
30
|
|
|
31
|
+
def add_hooks(*new_hooks)
|
|
32
|
+
@hooks.concat(new_hooks.flatten)
|
|
33
|
+
end
|
|
34
|
+
|
|
31
35
|
def provider_status
|
|
32
36
|
OpenFeature::SDK.configuration.provider_state(@provider)
|
|
33
37
|
end
|
|
@@ -49,11 +53,7 @@ module OpenFeature
|
|
|
49
53
|
def track(tracking_event_name, evaluation_context: nil, tracking_event_details: nil)
|
|
50
54
|
return unless @provider.respond_to?(:track)
|
|
51
55
|
|
|
52
|
-
built_context =
|
|
53
|
-
api_context: OpenFeature::SDK.evaluation_context,
|
|
54
|
-
client_context: self.evaluation_context,
|
|
55
|
-
invocation_context: evaluation_context
|
|
56
|
-
)
|
|
56
|
+
built_context = build_evaluation_context(evaluation_context)
|
|
57
57
|
|
|
58
58
|
@provider.track(tracking_event_name, evaluation_context: built_context, tracking_event_details: tracking_event_details)
|
|
59
59
|
end
|
|
@@ -74,31 +74,40 @@ module OpenFeature
|
|
|
74
74
|
def fetch_details(type:, flag_key:, default_value:, evaluation_context: nil, invocation_hooks: [], hook_hints: nil)
|
|
75
75
|
validate_default_value_type(type, default_value)
|
|
76
76
|
|
|
77
|
+
built_context = build_evaluation_context(evaluation_context)
|
|
78
|
+
|
|
79
|
+
# Assemble ordered hooks: API → Client → Invocation → Provider (spec 4.4.2)
|
|
80
|
+
provider_hooks = @provider.respond_to?(:hooks) ? Array(@provider.hooks) : []
|
|
81
|
+
ordered_hooks = [*OpenFeature::SDK.hooks, *@hooks, *invocation_hooks, *provider_hooks]
|
|
82
|
+
|
|
83
|
+
# Check for short-circuit conditions (spec 1.7.6 + 1.7.7)
|
|
84
|
+
short_circuit_code = nil
|
|
77
85
|
if OpenFeature::SDK.configuration.provider_tracked?(@provider)
|
|
78
|
-
|
|
79
|
-
|
|
86
|
+
short_circuit_code = short_circuit_error_code(provider_status)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Fast path: skip hook ceremony when no hooks are registered
|
|
90
|
+
if ordered_hooks.empty?
|
|
91
|
+
if short_circuit_code
|
|
80
92
|
resolution = Provider::ResolutionDetails.new(
|
|
81
93
|
value: default_value,
|
|
82
|
-
error_code:
|
|
94
|
+
error_code: short_circuit_code,
|
|
83
95
|
reason: Provider::Reason::ERROR
|
|
84
96
|
)
|
|
85
97
|
return EvaluationDetails.new(flag_key: flag_key, resolution_details: resolution)
|
|
86
98
|
end
|
|
87
|
-
end
|
|
88
|
-
|
|
89
|
-
built_context = EvaluationContextBuilder.new.call(
|
|
90
|
-
api_context: OpenFeature::SDK.evaluation_context,
|
|
91
|
-
client_context: self.evaluation_context,
|
|
92
|
-
invocation_context: evaluation_context
|
|
93
|
-
)
|
|
94
|
-
|
|
95
|
-
# Assemble ordered hooks: API → Client → Invocation → Provider (spec 4.4.2)
|
|
96
|
-
provider_hooks = @provider.respond_to?(:hooks) ? Array(@provider.hooks) : []
|
|
97
|
-
ordered_hooks = [*OpenFeature::SDK.hooks, *@hooks, *invocation_hooks, *provider_hooks]
|
|
98
99
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
100
|
+
begin
|
|
101
|
+
return evaluate_flag(type: type, flag_key: flag_key, default_value: default_value, evaluation_context: built_context)
|
|
102
|
+
rescue => e
|
|
103
|
+
resolution = Provider::ResolutionDetails.new(
|
|
104
|
+
value: default_value,
|
|
105
|
+
error_code: Provider::ErrorCode::GENERAL,
|
|
106
|
+
reason: Provider::Reason::ERROR,
|
|
107
|
+
error_message: e.message
|
|
108
|
+
)
|
|
109
|
+
return EvaluationDetails.new(flag_key: flag_key, resolution_details: resolution)
|
|
110
|
+
end
|
|
102
111
|
end
|
|
103
112
|
|
|
104
113
|
hook_context = Hooks::HookContext.new(
|
|
@@ -119,8 +128,21 @@ module OpenFeature
|
|
|
119
128
|
end
|
|
120
129
|
|
|
121
130
|
executor = Hooks::HookExecutor.new(logger: OpenFeature::SDK.configuration.logger)
|
|
122
|
-
|
|
123
|
-
|
|
131
|
+
|
|
132
|
+
if short_circuit_code
|
|
133
|
+
# Spec 1.7.6 + 1.7.7: short-circuit must still run error hooks and finally hooks
|
|
134
|
+
resolution = Provider::ResolutionDetails.new(
|
|
135
|
+
value: default_value,
|
|
136
|
+
error_code: short_circuit_code,
|
|
137
|
+
reason: Provider::Reason::ERROR
|
|
138
|
+
)
|
|
139
|
+
evaluation_details = EvaluationDetails.new(flag_key: flag_key, resolution_details: resolution)
|
|
140
|
+
executor.run_short_circuit(ordered_hooks: ordered_hooks, hook_context: hook_context, hints: hints, evaluation_details: evaluation_details)
|
|
141
|
+
evaluation_details
|
|
142
|
+
else
|
|
143
|
+
executor.execute(ordered_hooks: ordered_hooks, hook_context: hook_context, hints: hints) do |hctx|
|
|
144
|
+
evaluate_flag(type: type, flag_key: flag_key, default_value: default_value, evaluation_context: hctx.evaluation_context)
|
|
145
|
+
end
|
|
124
146
|
end
|
|
125
147
|
end
|
|
126
148
|
|
|
@@ -136,6 +158,7 @@ module OpenFeature
|
|
|
136
158
|
resolution_details.value = default_value
|
|
137
159
|
resolution_details.error_code = Provider::ErrorCode::TYPE_MISMATCH
|
|
138
160
|
resolution_details.reason = Provider::Reason::ERROR
|
|
161
|
+
resolution_details.variant = nil
|
|
139
162
|
end
|
|
140
163
|
|
|
141
164
|
EvaluationDetails.new(flag_key: flag_key, resolution_details: resolution_details)
|
|
@@ -148,6 +171,18 @@ module OpenFeature
|
|
|
148
171
|
end
|
|
149
172
|
end
|
|
150
173
|
|
|
174
|
+
def build_evaluation_context(invocation_context)
|
|
175
|
+
propagator = OpenFeature::SDK.configuration.transaction_context_propagator
|
|
176
|
+
transaction_context = propagator&.get_transaction_context
|
|
177
|
+
|
|
178
|
+
EvaluationContextBuilder.new.call(
|
|
179
|
+
api_context: OpenFeature::SDK.evaluation_context,
|
|
180
|
+
transaction_context: transaction_context,
|
|
181
|
+
client_context: evaluation_context,
|
|
182
|
+
invocation_context: invocation_context
|
|
183
|
+
)
|
|
184
|
+
end
|
|
185
|
+
|
|
151
186
|
def validate_default_value_type(type, default_value)
|
|
152
187
|
expected_classes = TYPE_CLASS_MAP[type]
|
|
153
188
|
unless expected_classes.any? { |klass| default_value.is_a?(klass) }
|
|
@@ -17,7 +17,7 @@ module OpenFeature
|
|
|
17
17
|
class Configuration
|
|
18
18
|
extend Forwardable
|
|
19
19
|
|
|
20
|
-
attr_accessor :evaluation_context, :hooks
|
|
20
|
+
attr_accessor :evaluation_context, :hooks, :transaction_context_propagator
|
|
21
21
|
attr_reader :logger
|
|
22
22
|
|
|
23
23
|
def initialize
|
|
@@ -31,6 +31,10 @@ module OpenFeature
|
|
|
31
31
|
@client_handlers_mutex = Mutex.new
|
|
32
32
|
end
|
|
33
33
|
|
|
34
|
+
def add_hooks(*new_hooks)
|
|
35
|
+
@hooks.concat(new_hooks.flatten)
|
|
36
|
+
end
|
|
37
|
+
|
|
34
38
|
def provider(domain: nil)
|
|
35
39
|
@providers[domain] || @providers[nil]
|
|
36
40
|
end
|
|
@@ -85,9 +89,11 @@ module OpenFeature
|
|
|
85
89
|
end
|
|
86
90
|
|
|
87
91
|
def shutdown
|
|
88
|
-
providers_to_shutdown = @provider_mutex.synchronize { @providers.values.uniq }
|
|
92
|
+
providers_to_shutdown = @provider_mutex.synchronize { @providers.values.uniq(&:object_id) }
|
|
89
93
|
|
|
90
94
|
providers_to_shutdown.each do |prov|
|
|
95
|
+
# Spec 1.7.9: Set provider state to NOT_READY before shutdown
|
|
96
|
+
@provider_state_registry.set_initial_state(prov, ProviderState::NOT_READY)
|
|
91
97
|
prov.shutdown if prov.respond_to?(:shutdown)
|
|
92
98
|
rescue => e
|
|
93
99
|
@logger&.warn("Error shutting down provider #{prov&.class&.name || "unknown"}: #{e.message}")
|
|
@@ -107,34 +113,46 @@ module OpenFeature
|
|
|
107
113
|
@provider_mutex.synchronize do
|
|
108
114
|
@providers.clear
|
|
109
115
|
end
|
|
116
|
+
@hooks.clear
|
|
117
|
+
@evaluation_context = nil
|
|
118
|
+
@transaction_context_propagator = nil
|
|
110
119
|
end
|
|
111
120
|
|
|
112
121
|
def set_provider_internal(provider, domain:, wait_for_init:)
|
|
113
122
|
# Capture evaluation context before acquiring mutex to prevent race conditions
|
|
114
123
|
context_for_init = @evaluation_context
|
|
115
124
|
|
|
116
|
-
old_provider
|
|
125
|
+
old_provider = nil
|
|
126
|
+
needs_init = false
|
|
127
|
+
needs_shutdown = false
|
|
117
128
|
|
|
118
129
|
@provider_mutex.synchronize do
|
|
119
130
|
old_provider = @providers[domain]
|
|
120
131
|
|
|
121
|
-
# Remove old provider state to prevent memory leaks
|
|
122
|
-
@provider_state_registry.remove_provider(old_provider)
|
|
123
|
-
|
|
124
132
|
new_providers = @providers.dup
|
|
125
133
|
new_providers[domain] = provider
|
|
126
134
|
@providers = new_providers
|
|
127
135
|
|
|
128
|
-
|
|
136
|
+
# Spec 1.1.2.2: Only initialize if the provider is not already active
|
|
137
|
+
# (i.e., not already bound to another domain)
|
|
138
|
+
already_active = @providers.any? { |d, p| d != domain && p.equal?(provider) && @provider_state_registry.tracked?(p) }
|
|
139
|
+
needs_init = !already_active
|
|
129
140
|
|
|
130
|
-
|
|
141
|
+
if needs_init
|
|
142
|
+
@provider_state_registry.set_initial_state(provider)
|
|
143
|
+
provider.send(:attach, self) if provider.is_a?(Provider::EventEmitter)
|
|
144
|
+
end
|
|
131
145
|
|
|
132
|
-
|
|
146
|
+
# Spec 1.1.2.3: Only shutdown old provider if it's no longer bound to any domain
|
|
147
|
+
if old_provider && !old_provider.equal?(provider)
|
|
148
|
+
still_bound = @providers.any? { |_, p| p.equal?(old_provider) }
|
|
149
|
+
needs_shutdown = !still_bound
|
|
150
|
+
@provider_state_registry.remove_provider(old_provider) unless still_bound
|
|
151
|
+
end
|
|
133
152
|
end
|
|
134
153
|
|
|
135
154
|
# Shutdown old provider outside mutex to avoid blocking other operations
|
|
136
|
-
|
|
137
|
-
if old_provider && old_provider != provider
|
|
155
|
+
if needs_shutdown
|
|
138
156
|
begin
|
|
139
157
|
old_provider.shutdown if old_provider.respond_to?(:shutdown)
|
|
140
158
|
rescue => e
|
|
@@ -143,12 +161,17 @@ module OpenFeature
|
|
|
143
161
|
end
|
|
144
162
|
|
|
145
163
|
# Initialize provider outside the mutex to avoid blocking other operations
|
|
146
|
-
if
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
164
|
+
if needs_init
|
|
165
|
+
if wait_for_init
|
|
166
|
+
init_provider(provider, context_for_init, raise_on_error: true)
|
|
167
|
+
else
|
|
168
|
+
Thread.new do
|
|
169
|
+
init_provider(provider, context_for_init, raise_on_error: false)
|
|
170
|
+
end
|
|
151
171
|
end
|
|
172
|
+
elsif wait_for_init
|
|
173
|
+
# Provider already active; no init needed but still dispatch READY
|
|
174
|
+
dispatch_provider_event(provider, ProviderEvent::PROVIDER_READY)
|
|
152
175
|
end
|
|
153
176
|
end
|
|
154
177
|
|
|
@@ -164,16 +187,22 @@ module OpenFeature
|
|
|
164
187
|
|
|
165
188
|
dispatch_provider_event(provider, ProviderEvent::PROVIDER_READY)
|
|
166
189
|
rescue => e
|
|
190
|
+
# Spec 1.7.8: Propagate error code from provider if available
|
|
191
|
+
error_code = if e.respond_to?(:error_code) && e.error_code
|
|
192
|
+
e.error_code
|
|
193
|
+
else
|
|
194
|
+
Provider::ErrorCode::GENERAL
|
|
195
|
+
end
|
|
196
|
+
|
|
167
197
|
dispatch_provider_event(provider, ProviderEvent::PROVIDER_ERROR,
|
|
168
|
-
error_code:
|
|
198
|
+
error_code: error_code,
|
|
169
199
|
message: e.message)
|
|
170
200
|
|
|
171
201
|
if raise_on_error
|
|
172
|
-
# Re-raise as ProviderInitializationError for synchronous callers
|
|
173
202
|
raise ProviderInitializationError.new(
|
|
174
203
|
"Provider #{provider.class.name} initialization failed: #{e.message}",
|
|
175
204
|
provider:,
|
|
176
|
-
error_code:
|
|
205
|
+
error_code: error_code,
|
|
177
206
|
original_error: e
|
|
178
207
|
)
|
|
179
208
|
end
|
|
@@ -4,8 +4,8 @@ module OpenFeature
|
|
|
4
4
|
module SDK
|
|
5
5
|
# Used to combine evaluation contexts from different sources
|
|
6
6
|
class EvaluationContextBuilder
|
|
7
|
-
def call(api_context:, client_context:, invocation_context:)
|
|
8
|
-
available_contexts = [api_context, client_context, invocation_context].compact
|
|
7
|
+
def call(api_context:, client_context:, invocation_context:, transaction_context: nil)
|
|
8
|
+
available_contexts = [api_context, transaction_context, client_context, invocation_context].compact
|
|
9
9
|
|
|
10
10
|
return nil if available_contexts.empty?
|
|
11
11
|
|
|
@@ -18,6 +18,20 @@ module OpenFeature
|
|
|
18
18
|
@logger = logger
|
|
19
19
|
end
|
|
20
20
|
|
|
21
|
+
# Runs error hooks and finally hooks for a short-circuit evaluation
|
|
22
|
+
# (spec 1.7.6 + 1.7.7). Before hooks and after hooks are NOT run.
|
|
23
|
+
#
|
|
24
|
+
# @param ordered_hooks [Array] hooks in before-order
|
|
25
|
+
# @param hook_context [HookContext] the hook context
|
|
26
|
+
# @param hints [Hints] hook hints
|
|
27
|
+
# @param evaluation_details [EvaluationDetails] the short-circuit result
|
|
28
|
+
def run_short_circuit(ordered_hooks:, hook_context:, hints:, evaluation_details:)
|
|
29
|
+
error = StandardError.new(evaluation_details.error_code)
|
|
30
|
+
run_error_hooks(ordered_hooks, hook_context, error, hints)
|
|
31
|
+
ensure
|
|
32
|
+
run_finally_hooks(ordered_hooks, hook_context, evaluation_details, hints)
|
|
33
|
+
end
|
|
34
|
+
|
|
21
35
|
# Executes the full hook lifecycle around the flag evaluation block.
|
|
22
36
|
#
|
|
23
37
|
# @param ordered_hooks [Array] hooks in before-order (API, Client, Invocation, Provider)
|
|
@@ -31,7 +45,15 @@ module OpenFeature
|
|
|
31
45
|
begin
|
|
32
46
|
run_before_hooks(ordered_hooks, hook_context, hints)
|
|
33
47
|
evaluation_details = evaluate_block.call(hook_context)
|
|
34
|
-
|
|
48
|
+
|
|
49
|
+
# Spec 4.3.6: If evaluation resulted in an error (e.g. FLAG_NOT_FOUND,
|
|
50
|
+
# TYPE_MISMATCH), run error hooks instead of after hooks.
|
|
51
|
+
if evaluation_details.error_code
|
|
52
|
+
error = StandardError.new(evaluation_details.error_message || evaluation_details.error_code)
|
|
53
|
+
run_error_hooks(ordered_hooks, hook_context, error, hints)
|
|
54
|
+
else
|
|
55
|
+
run_after_hooks(ordered_hooks, hook_context, evaluation_details, hints)
|
|
56
|
+
end
|
|
35
57
|
rescue => e
|
|
36
58
|
run_error_hooks(ordered_hooks, hook_context, e, hints)
|
|
37
59
|
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OpenFeature
|
|
4
|
+
module SDK
|
|
5
|
+
module Hooks
|
|
6
|
+
class LoggingHook
|
|
7
|
+
include Hook
|
|
8
|
+
|
|
9
|
+
def initialize(logger: nil, include_evaluation_context: false)
|
|
10
|
+
@logger = logger
|
|
11
|
+
@include_evaluation_context = include_evaluation_context
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def before(hook_context:, hints:)
|
|
15
|
+
logger&.debug { build_before_message(hook_context) }
|
|
16
|
+
nil
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def after(hook_context:, evaluation_details:, hints:)
|
|
20
|
+
logger&.debug { build_after_message(hook_context, evaluation_details) }
|
|
21
|
+
nil
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def error(hook_context:, exception:, hints:)
|
|
25
|
+
logger&.error { build_error_message(hook_context, exception) }
|
|
26
|
+
nil
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def logger
|
|
32
|
+
@logger || OpenFeature::SDK.configuration.logger
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def build_before_message(hook_context)
|
|
36
|
+
parts = base_parts("before", hook_context)
|
|
37
|
+
parts << "evaluation_context=#{format_context(hook_context.evaluation_context)}" if @include_evaluation_context
|
|
38
|
+
parts.join(" ")
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def build_after_message(hook_context, evaluation_details)
|
|
42
|
+
parts = base_parts("after", hook_context)
|
|
43
|
+
parts << "reason=#{sanitize(evaluation_details.reason)}" if evaluation_details.reason
|
|
44
|
+
parts << "variant=#{sanitize(evaluation_details.variant)}" if evaluation_details.variant
|
|
45
|
+
parts << "value=#{sanitize(evaluation_details.value)}"
|
|
46
|
+
parts << "evaluation_context=#{format_context(hook_context.evaluation_context)}" if @include_evaluation_context
|
|
47
|
+
parts.join(" ")
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def build_error_message(hook_context, exception)
|
|
51
|
+
parts = base_parts("error", hook_context)
|
|
52
|
+
error_code = exception.respond_to?(:error_code) ? exception.error_code : Provider::ErrorCode::GENERAL
|
|
53
|
+
parts << "error_code=#{sanitize(error_code)}"
|
|
54
|
+
parts << "error_message=#{sanitize(exception.message)}"
|
|
55
|
+
parts << "evaluation_context=#{format_context(hook_context.evaluation_context)}" if @include_evaluation_context
|
|
56
|
+
parts.join(" ")
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def base_parts(stage, hook_context)
|
|
60
|
+
domain = hook_context.client_metadata&.domain
|
|
61
|
+
provider_name = hook_context.provider_metadata&.name
|
|
62
|
+
[
|
|
63
|
+
"stage=#{stage}",
|
|
64
|
+
"domain=#{sanitize(domain)}",
|
|
65
|
+
"provider_name=#{sanitize(provider_name)}",
|
|
66
|
+
"flag_key=#{sanitize(hook_context.flag_key)}",
|
|
67
|
+
"default_value=#{sanitize(hook_context.default_value)}"
|
|
68
|
+
]
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def format_context(evaluation_context)
|
|
72
|
+
return "" unless evaluation_context
|
|
73
|
+
fields = evaluation_context.fields.dup
|
|
74
|
+
fields["targeting_key"] = evaluation_context.targeting_key if evaluation_context.targeting_key
|
|
75
|
+
fields.map { |k, v| "#{sanitize(k)}=#{sanitize(v)}" }.join(", ")
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def sanitize(value)
|
|
79
|
+
value.to_s.gsub(/[\n\r]/, " ")
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
@@ -29,8 +29,10 @@ module OpenFeature
|
|
|
29
29
|
end
|
|
30
30
|
|
|
31
31
|
def update_flags(new_flags)
|
|
32
|
+
old_keys = @flags.keys
|
|
32
33
|
@flags = new_flags.dup
|
|
33
|
-
|
|
34
|
+
changed_keys = (old_keys | new_flags.keys)
|
|
35
|
+
emit_provider_changed(changed_keys)
|
|
34
36
|
end
|
|
35
37
|
|
|
36
38
|
def fetch_boolean_value(flag_key:, default_value:, evaluation_context: nil)
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OpenFeature
|
|
4
|
+
module SDK
|
|
5
|
+
class ThreadLocalTransactionContextPropagator
|
|
6
|
+
include TransactionContextPropagator
|
|
7
|
+
|
|
8
|
+
THREAD_KEY = :openfeature_transaction_context
|
|
9
|
+
|
|
10
|
+
def set_transaction_context(evaluation_context)
|
|
11
|
+
Thread.current[THREAD_KEY] = evaluation_context
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def get_transaction_context
|
|
15
|
+
Thread.current[THREAD_KEY]
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OpenFeature
|
|
4
|
+
module SDK
|
|
5
|
+
module TransactionContextPropagator
|
|
6
|
+
def set_transaction_context(evaluation_context)
|
|
7
|
+
raise NotImplementedError
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def get_transaction_context
|
|
11
|
+
raise NotImplementedError
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
data/lib/open_feature/sdk.rb
CHANGED
data/renovate.json
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: openfeature-sdk
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.6.
|
|
4
|
+
version: 0.6.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- OpenFeature Authors
|
|
@@ -159,6 +159,7 @@ files:
|
|
|
159
159
|
- LICENSE
|
|
160
160
|
- README.md
|
|
161
161
|
- Rakefile
|
|
162
|
+
- cucumber.yml
|
|
162
163
|
- lib/open_feature/sdk.rb
|
|
163
164
|
- lib/open_feature/sdk/api.rb
|
|
164
165
|
- lib/open_feature/sdk/client.rb
|
|
@@ -173,6 +174,7 @@ files:
|
|
|
173
174
|
- lib/open_feature/sdk/hooks/hook.rb
|
|
174
175
|
- lib/open_feature/sdk/hooks/hook_context.rb
|
|
175
176
|
- lib/open_feature/sdk/hooks/hook_executor.rb
|
|
177
|
+
- lib/open_feature/sdk/hooks/logging_hook.rb
|
|
176
178
|
- lib/open_feature/sdk/provider.rb
|
|
177
179
|
- lib/open_feature/sdk/provider/error_code.rb
|
|
178
180
|
- lib/open_feature/sdk/provider/event_emitter.rb
|
|
@@ -185,7 +187,9 @@ files:
|
|
|
185
187
|
- lib/open_feature/sdk/provider_initialization_error.rb
|
|
186
188
|
- lib/open_feature/sdk/provider_state.rb
|
|
187
189
|
- lib/open_feature/sdk/provider_state_registry.rb
|
|
190
|
+
- lib/open_feature/sdk/thread_local_transaction_context_propagator.rb
|
|
188
191
|
- lib/open_feature/sdk/tracking_event_details.rb
|
|
192
|
+
- lib/open_feature/sdk/transaction_context_propagator.rb
|
|
189
193
|
- lib/open_feature/sdk/version.rb
|
|
190
194
|
- release-please-config.json
|
|
191
195
|
- renovate.json
|