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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ac3b2793c1b6453e2525dcc04d7646fd4e5d61a87e6ade8b257754cf9b9c991c
4
- data.tar.gz: c5aeedfe23851ef99e26024bf1db5f4ba38d7f669b9ea6b3e1e5f63d0693c1a6
3
+ metadata.gz: 304772054cf10c0cc02870fdeb7a1a7412342912e3f8a08d80c33fe25bd99849
4
+ data.tar.gz: 6a27c2a8bf23074292594f4eec0ecd4dd9df11ddc1e3e48c4c7ff245e76a6381
5
5
  SHA512:
6
- metadata.gz: 9aa41845c24248cf6eec381f4531fbfc301196ea748f3b3189333c4879c64a2dc1eda8a935e3514291dcbb5100c1668260e1b87bec837952e1eb1777ae083501
7
- data.tar.gz: c162a21b0a5b4ee91f132e676440b84cf7862afccd689416f97e9b41f9284bf8b86de5050a1c32a3c98096ec39074cbb0ae1dcbe0d85d4db905d447f3baccd3d
6
+ metadata.gz: bcaacd882ecdf0ebd0643e1a839bae3d4efb02deb30f2128c5feca91c93e85bee65b755147cf4bef6edabc010875746b777cd4570f26161451a620db396e02e8
7
+ data.tar.gz: 5a6ba82992f4d8e960d350a573e48a5e015c9b8eb953737354a4573d0a9f89ca6c4615553d0240a91c1490aad8fc3e1c2ca4bfeddc64d0b1203835ede55e1c77
@@ -1,3 +1,3 @@
1
1
  {
2
- ".": "0.6.1"
2
+ ".": "0.6.2"
3
3
  }
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
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- openfeature-sdk (0.6.1)
4
+ openfeature-sdk (0.6.2)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
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.1">
21
- <img alt="Release" src="https://img.shields.io/static/v1?label=release&message=v0.6.1&color=blue&style=for-the-badge" />
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
- | | [Logging](#logging) | Integrate with popular logging packages. |
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
- | | [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) |
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
- Coming Soon! [Issue available](https://github.com/open-feature/ruby-sdk/issues/148) to work on.
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
- <!-- TODO: talk about logging config and include a code example -->
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
- Coming Soon! [Issue available](https://github.com/open-feature/ruby-sdk/issues/150) to be worked on.
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
- <!-- Transaction context is a container for transaction-specific evaluation context (e.g. user id, user agent, IP).
367
- 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). -->
406
+ ```ruby
407
+ class MyRequestScopedPropagator
408
+ include OpenFeature::SDK::TransactionContextPropagator
368
409
 
369
- <!-- TODO: code example for global shutdown -->
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
 
@@ -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 = EvaluationContextBuilder.new.call(
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 = EvaluationContextBuilder.new.call(
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) }
@@ -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
@@ -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
@@ -4,3 +4,4 @@ require_relative "hooks/hints"
4
4
  require_relative "hooks/hook"
5
5
  require_relative "hooks/hook_context"
6
6
  require_relative "hooks/hook_executor"
7
+ require_relative "hooks/logging_hook"
@@ -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
@@ -2,6 +2,6 @@
2
2
 
3
3
  module OpenFeature
4
4
  module SDK
5
- VERSION = "0.6.1"
5
+ VERSION = "0.6.2"
6
6
  end
7
7
  end
@@ -2,6 +2,8 @@
2
2
 
3
3
  require_relative "sdk/version"
4
4
  require_relative "sdk/api"
5
+ require_relative "sdk/transaction_context_propagator"
6
+ require_relative "sdk/thread_local_transaction_context_propagator"
5
7
 
6
8
  module OpenFeature
7
9
  # TODO: Add documentation
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.1
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