openfeature-sdk 0.6.1 → 0.6.2
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 +8 -0
- data/Gemfile.lock +1 -1
- data/README.md +60 -10
- data/lib/open_feature/sdk/api.rb +9 -0
- data/lib/open_feature/sdk/client.rb +14 -10
- data/lib/open_feature/sdk/configuration.rb +1 -1
- data/lib/open_feature/sdk/evaluation_context_builder.rb +2 -2
- 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/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
- metadata +4 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 304772054cf10c0cc02870fdeb7a1a7412342912e3f8a08d80c33fe25bd99849
|
|
4
|
+
data.tar.gz: 6a27c2a8bf23074292594f4eec0ecd4dd9df11ddc1e3e48c4c7ff245e76a6381
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: bcaacd882ecdf0ebd0643e1a839bae3d4efb02deb30f2128c5feca91c93e85bee65b755147cf4bef6edabc010875746b777cd4570f26161451a620db396e02e8
|
|
7
|
+
data.tar.gz: 5a6ba82992f4d8e960d350a573e48a5e015c9b8eb953737354a4573d0a9f89ca6c4615553d0240a91c1490aad8fc3e1c2ca4bfeddc64d0b1203835ede55e1c77
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.6.2](https://github.com/open-feature/ruby-sdk/compare/v0.6.1...v0.6.2) (2026-03-07)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* 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))
|
|
9
|
+
* 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))
|
|
10
|
+
|
|
3
11
|
## [0.6.1](https://github.com/open-feature/ruby-sdk/compare/v0.6.0...v0.6.1) (2026-03-05)
|
|
4
12
|
|
|
5
13
|
|
data/Gemfile.lock
CHANGED
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.2">
|
|
21
|
+
<img alt="Release" src="https://img.shields.io/static/v1?label=release&message=v0.6.2&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/lib/open_feature/sdk/api.rb
CHANGED
|
@@ -70,6 +70,15 @@ module OpenFeature
|
|
|
70
70
|
configuration.logger = new_logger
|
|
71
71
|
end
|
|
72
72
|
|
|
73
|
+
def set_transaction_context_propagator(propagator)
|
|
74
|
+
configuration.transaction_context_propagator = propagator
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def set_transaction_context(evaluation_context)
|
|
78
|
+
propagator = configuration.transaction_context_propagator
|
|
79
|
+
propagator&.set_transaction_context(evaluation_context)
|
|
80
|
+
end
|
|
81
|
+
|
|
73
82
|
def shutdown
|
|
74
83
|
configuration.shutdown
|
|
75
84
|
end
|
|
@@ -49,11 +49,7 @@ module OpenFeature
|
|
|
49
49
|
def track(tracking_event_name, evaluation_context: nil, tracking_event_details: nil)
|
|
50
50
|
return unless @provider.respond_to?(:track)
|
|
51
51
|
|
|
52
|
-
built_context =
|
|
53
|
-
api_context: OpenFeature::SDK.evaluation_context,
|
|
54
|
-
client_context: self.evaluation_context,
|
|
55
|
-
invocation_context: evaluation_context
|
|
56
|
-
)
|
|
52
|
+
built_context = build_evaluation_context(evaluation_context)
|
|
57
53
|
|
|
58
54
|
@provider.track(tracking_event_name, evaluation_context: built_context, tracking_event_details: tracking_event_details)
|
|
59
55
|
end
|
|
@@ -86,11 +82,7 @@ module OpenFeature
|
|
|
86
82
|
end
|
|
87
83
|
end
|
|
88
84
|
|
|
89
|
-
built_context =
|
|
90
|
-
api_context: OpenFeature::SDK.evaluation_context,
|
|
91
|
-
client_context: self.evaluation_context,
|
|
92
|
-
invocation_context: evaluation_context
|
|
93
|
-
)
|
|
85
|
+
built_context = build_evaluation_context(evaluation_context)
|
|
94
86
|
|
|
95
87
|
# Assemble ordered hooks: API → Client → Invocation → Provider (spec 4.4.2)
|
|
96
88
|
provider_hooks = @provider.respond_to?(:hooks) ? Array(@provider.hooks) : []
|
|
@@ -148,6 +140,18 @@ module OpenFeature
|
|
|
148
140
|
end
|
|
149
141
|
end
|
|
150
142
|
|
|
143
|
+
def build_evaluation_context(invocation_context)
|
|
144
|
+
propagator = OpenFeature::SDK.configuration.transaction_context_propagator
|
|
145
|
+
transaction_context = propagator&.get_transaction_context
|
|
146
|
+
|
|
147
|
+
EvaluationContextBuilder.new.call(
|
|
148
|
+
api_context: OpenFeature::SDK.evaluation_context,
|
|
149
|
+
transaction_context: transaction_context,
|
|
150
|
+
client_context: evaluation_context,
|
|
151
|
+
invocation_context: invocation_context
|
|
152
|
+
)
|
|
153
|
+
end
|
|
154
|
+
|
|
151
155
|
def validate_default_value_type(type, default_value)
|
|
152
156
|
expected_classes = TYPE_CLASS_MAP[type]
|
|
153
157
|
unless expected_classes.any? { |klass| default_value.is_a?(klass) }
|
|
@@ -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
|
|
|
@@ -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
|
|
@@ -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
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.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- OpenFeature Authors
|
|
@@ -173,6 +173,7 @@ files:
|
|
|
173
173
|
- lib/open_feature/sdk/hooks/hook.rb
|
|
174
174
|
- lib/open_feature/sdk/hooks/hook_context.rb
|
|
175
175
|
- lib/open_feature/sdk/hooks/hook_executor.rb
|
|
176
|
+
- lib/open_feature/sdk/hooks/logging_hook.rb
|
|
176
177
|
- lib/open_feature/sdk/provider.rb
|
|
177
178
|
- lib/open_feature/sdk/provider/error_code.rb
|
|
178
179
|
- lib/open_feature/sdk/provider/event_emitter.rb
|
|
@@ -185,7 +186,9 @@ files:
|
|
|
185
186
|
- lib/open_feature/sdk/provider_initialization_error.rb
|
|
186
187
|
- lib/open_feature/sdk/provider_state.rb
|
|
187
188
|
- lib/open_feature/sdk/provider_state_registry.rb
|
|
189
|
+
- lib/open_feature/sdk/thread_local_transaction_context_propagator.rb
|
|
188
190
|
- lib/open_feature/sdk/tracking_event_details.rb
|
|
191
|
+
- lib/open_feature/sdk/transaction_context_propagator.rb
|
|
189
192
|
- lib/open_feature/sdk/version.rb
|
|
190
193
|
- release-please-config.json
|
|
191
194
|
- renovate.json
|