openfeature-sdk 0.6.0 → 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: af657f35506e7f76184269c57877bbe87cb75f03a2e697e228e9b48f0b2bb17c
4
- data.tar.gz: 57944d4c3a74838e73a1f55b4f06e9b82ec4b5186c76ad000577504b30d72d88
3
+ metadata.gz: 304772054cf10c0cc02870fdeb7a1a7412342912e3f8a08d80c33fe25bd99849
4
+ data.tar.gz: 6a27c2a8bf23074292594f4eec0ecd4dd9df11ddc1e3e48c4c7ff245e76a6381
5
5
  SHA512:
6
- metadata.gz: 8dc359e5bb318d79e2fcb48bc0c393c055578cf0a331b02bc61f82910b3da973eaf487457db80d9cd73f12303b15a77cbef9062481933c3d7b4c305de0ed1cc2
7
- data.tar.gz: c733ce2f12dbd64ad9fd4e42a5c6cd17c805aa0ab746408c222aee42e6af5ba385ff92baf6cffbf79e4e949dbd7e6b5b2c67937694717ef5b41396353b9f9391
6
+ metadata.gz: bcaacd882ecdf0ebd0643e1a839bae3d4efb02deb30f2128c5feca91c93e85bee65b755147cf4bef6edabc010875746b777cd4570f26161451a620db396e02e8
7
+ data.tar.gz: 5a6ba82992f4d8e960d350a573e48a5e015c9b8eb953737354a4573d0a9f89ca6c4615553d0240a91c1490aad8fc3e1c2ca4bfeddc64d0b1203835ede55e1c77
@@ -1,3 +1,3 @@
1
1
  {
2
- ".": "0.6.0"
2
+ ".": "0.6.2"
3
3
  }
data/CHANGELOG.md CHANGED
@@ -1,5 +1,25 @@
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
+
11
+ ## [0.6.1](https://github.com/open-feature/ruby-sdk/compare/v0.6.0...v0.6.1) (2026-03-05)
12
+
13
+
14
+ ### Features
15
+
16
+ * add flag metadata defaulting and immutability ([#221](https://github.com/open-feature/ruby-sdk/issues/221)) ([a300fc5](https://github.com/open-feature/ruby-sdk/commit/a300fc559293169f22eb1ce26f738cdee664cd26))
17
+ * add hook data per-hook mutable state ([#222](https://github.com/open-feature/ruby-sdk/issues/222)) ([28518a0](https://github.com/open-feature/ruby-sdk/commit/28518a0e08143d167b9d34c86e57a583fe5ee0de))
18
+ * add InMemoryProvider context callbacks and event emission ([#224](https://github.com/open-feature/ruby-sdk/issues/224)) ([0a148f6](https://github.com/open-feature/ruby-sdk/commit/0a148f66abc815fc2ec9fd70027075125dbd504a))
19
+ * add shutdown API, provider status, and status short-circuit ([#223](https://github.com/open-feature/ruby-sdk/issues/223)) ([f9c32ad](https://github.com/open-feature/ruby-sdk/commit/f9c32ad1b467af25697423a542bc568597f39743))
20
+ * implement Tracking API (spec section 6) ([#227](https://github.com/open-feature/ruby-sdk/issues/227)) ([5576fce](https://github.com/open-feature/ruby-sdk/commit/5576fce1c3bcf6e7510d8957c7e40e85c4b83b6f))
21
+ * populate event details payload with error_code and message ([#225](https://github.com/open-feature/ruby-sdk/issues/225)) ([a185003](https://github.com/open-feature/ruby-sdk/commit/a185003dc09a69b2dda1fe569d1f82c45979cdad))
22
+
3
23
  ## [0.6.0](https://github.com/open-feature/ruby-sdk/compare/v0.5.1...v0.6.0) (2026-03-05)
4
24
 
5
25
 
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- openfeature-sdk (0.6.0)
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.0">
21
- <img alt="Release" src="https://img.shields.io/static/v1?label=release&message=v0.6.0&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 -->
@@ -100,13 +100,14 @@ object = client.fetch_object_value(flag_key: 'object_value', default_value: { na
100
100
  | ------ | --------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------|
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
- | ⚠️ | [Hooks](#hooks) | Add functionality to various stages of the flag evaluation life-cycle. |
104
- | | [Logging](#logging) | Integrate with popular logging packages. |
103
+ | | [Hooks](#hooks) | Add functionality to various stages of the flag evaluation life-cycle. |
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
- | ⚠️ | [Shutdown](#shutdown) | Gracefully clean up a provider during application shutdown. |
108
- | | [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
- | ⚠️ | [Extending](#extending) | Extend OpenFeature with custom providers and hooks. |
107
+ | | [Shutdown](#shutdown) | Gracefully clean up a provider during application shutdown. |
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) |
110
+ | ✅ | [Extending](#extending) | Extend OpenFeature with custom providers and hooks. |
110
111
 
111
112
  <sub>Implemented: ✅ | In-progress: ⚠️ | Not implemented yet: ❌</sub>
112
113
 
@@ -200,21 +201,73 @@ bool_value = client.fetch_boolean_value(
200
201
 
201
202
  ### Hooks
202
203
 
203
- Coming Soon! [Issue available](https://github.com/open-feature/ruby-sdk/issues/52) to be worked on.
204
-
205
- <!-- [Hooks](https://openfeature.dev/docs/reference/concepts/hooks) allow for custom logic to be added at well-defined points of the flag evaluation life-cycle.
204
+ [Hooks](https://openfeature.dev/docs/reference/concepts/hooks) allow for custom logic to be added at well-defined points of the flag evaluation life-cycle.
206
205
  Look [here](https://openfeature.dev/ecosystem/?instant_search%5BrefinementList%5D%5Btype%5D%5B0%5D=Hook&instant_search%5BrefinementList%5D%5Btechnology%5D%5B0%5D=Ruby) for a complete list of available hooks.
207
206
  If the hook you're looking for hasn't been created yet, see the [develop a hook](#develop-a-hook) section to learn how to build it yourself.
208
207
 
209
- Once you've added a hook as a dependency, it can be registered at the global, client, or flag invocation level. -->
208
+ Hooks can be registered at the global, client, or flag invocation level.
209
+
210
+ ```ruby
211
+ # Define a hook
212
+ class MyHook
213
+ include OpenFeature::SDK::Hooks::Hook
214
+
215
+ def before(hook_context:, hints:)
216
+ puts "Evaluating flag: #{hook_context.flag_key}"
217
+ nil
218
+ end
219
+
220
+ def after(hook_context:, evaluation_details:, hints:)
221
+ puts "Flag #{hook_context.flag_key} evaluated to: #{evaluation_details.value}"
222
+ end
223
+
224
+ def error(hook_context:, exception:, hints:)
225
+ puts "Error evaluating #{hook_context.flag_key}: #{exception.message}"
226
+ end
227
+
228
+ def finally(hook_context:, evaluation_details:, hints:)
229
+ puts "Evaluation complete for #{hook_context.flag_key}"
230
+ end
231
+ end
210
232
 
211
- <!-- TODO: code example of setting hooks at all levels -->
233
+ # Register at the API (global) level
234
+ OpenFeature::SDK.hooks << MyHook.new
235
+
236
+ # Register at the client level
237
+ client = OpenFeature::SDK.build_client
238
+ client.hooks << MyHook.new
239
+
240
+ # Register at the invocation level
241
+ client.fetch_boolean_value(
242
+ flag_key: "my-flag",
243
+ default_value: false,
244
+ hooks: [MyHook.new]
245
+ )
246
+ ```
212
247
 
213
248
  ### Logging
214
249
 
215
- 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.
216
251
 
217
- <!-- 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`
218
271
 
219
272
  ### Domains
220
273
 
@@ -276,28 +329,94 @@ OpenFeature::SDK.remove_handler(OpenFeature::SDK::ProviderEvent::PROVIDER_READY,
276
329
 
277
330
  ### Shutdown
278
331
 
279
- Coming Soon! [Issue available](https://github.com/open-feature/ruby-sdk/issues/149) to be worked on.
280
-
281
- <!-- TODO The OpenFeature API provides a close function to perform a cleanup of all registered providers.
332
+ The OpenFeature API provides a `shutdown` method to perform cleanup of all registered providers.
282
333
  This should only be called when your application is in the process of shutting down.
283
334
 
335
+ ```ruby
336
+ # Shut down all registered providers and clear state
337
+ OpenFeature::SDK.shutdown
338
+ ```
339
+
340
+ Individual providers can implement a `shutdown` method to perform cleanup:
341
+
284
342
  ```ruby
285
343
  class MyProvider
286
344
  def shutdown
287
345
  # Perform any shutdown/reclamation steps with flag management system here
288
- # Return value is ignored
289
346
  end
290
347
  end
291
- ``` -->
348
+ ```
349
+
350
+ ### Tracking
351
+
352
+ The tracking API allows you to use OpenFeature abstractions and objects to associate user actions with feature flag evaluations.
353
+ This is essential for robust experimentation powered by feature flags.
354
+ For example, a flag enhancing the appearance of a UI component might drive user engagement to a new feature; to test this hypothesis, telemetry collected by a [hook](#hooks) or [provider](#providers) can be associated with telemetry reported in the client's `track` function.
355
+
356
+ ```ruby
357
+ client = OpenFeature::SDK.build_client
358
+
359
+ # Simple tracking event
360
+ client.track("checkout_completed")
361
+
362
+ # With evaluation context
363
+ client.track(
364
+ "purchase",
365
+ evaluation_context: OpenFeature::SDK::EvaluationContext.new(targeting_key: "user-123")
366
+ )
367
+
368
+ # With tracking event details (optional numeric value + custom fields)
369
+ details = OpenFeature::SDK::TrackingEventDetails.new(
370
+ value: 99.99,
371
+ plan: "premium",
372
+ currency: "USD"
373
+ )
374
+ client.track("subscription", tracking_event_details: details)
375
+ ```
376
+
377
+ Note that some providers may not support tracking; if the provider does not implement a `track` method, the call is a no-op.
378
+ Check the documentation for your [provider](#providers) for more information.
292
379
 
293
380
  ### Transaction Context Propagation
294
381
 
295
- 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`:
296
386
 
297
- <!-- Transaction context is a container for transaction-specific evaluation context (e.g. user id, user agent, IP).
298
- 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). -->
387
+ ```ruby
388
+ # Set up the propagator
389
+ OpenFeature::SDK.set_transaction_context_propagator(
390
+ OpenFeature::SDK::ThreadLocalTransactionContextPropagator.new
391
+ )
299
392
 
300
- <!-- TODO: code example for global shutdown -->
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:
405
+
406
+ ```ruby
407
+ class MyRequestScopedPropagator
408
+ include OpenFeature::SDK::TransactionContextPropagator
409
+
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
+ ```
301
420
 
302
421
  ## Extending
303
422
 
@@ -344,6 +463,12 @@ class MyProvider
344
463
  def fetch_object_value(flag_key:, default_value:, evaluation_context: nil)
345
464
  # Retrieve a hash value from provider source
346
465
  end
466
+
467
+ # Optional: implement tracking support (spec 6.1.4)
468
+ # If not defined, Client#track is a no-op
469
+ def track(tracking_event_name, evaluation_context:, tracking_event_details:)
470
+ # Record a tracking event with your flag management system
471
+ end
347
472
  end
348
473
  ```
349
474
 
@@ -351,17 +476,29 @@ end
351
476
 
352
477
  ### Develop a hook
353
478
 
354
- Coming Soon! [Issue available](https://github.com/open-feature/ruby-sdk/issues/52) to be worked on.
355
-
356
- <!-- To develop a hook, you need to create a new project and include the OpenFeature SDK as a dependency.
479
+ To develop a hook, you need to create a new project and include the OpenFeature SDK as a dependency.
357
480
  This can be a new repository or included in [the existing contrib repository](https://github.com/open-feature/ruby-sdk-contrib) available under the OpenFeature organization.
358
- Implement your own hook by conforming to the `Hook interface`.
359
- To satisfy the interface, all methods (`Before`/`After`/`Finally`/`Error`) need to be defined.
360
- To avoid defining empty functions, make use of the `UnimplementedHook` struct (which already implements all the empty functions). -->
481
+ Implement your own hook by including the `OpenFeature::SDK::Hooks::Hook` module.
482
+ You only need to define the stages you care about unimplemented stages are no-ops by default.
361
483
 
362
- <!-- TODO: code example of hook implementation -->
484
+ ```ruby
485
+ class MyLoggingHook
486
+ include OpenFeature::SDK::Hooks::Hook
487
+
488
+ def before(hook_context:, hints:)
489
+ puts "Evaluating #{hook_context.flag_key}"
490
+ nil # Return nil or an EvaluationContext to merge
491
+ end
492
+
493
+ def after(hook_context:, evaluation_details:, hints:)
494
+ puts "Result: #{evaluation_details.value}"
495
+ end
496
+
497
+ # error and finally are optional — only define what you need
498
+ end
499
+ ```
363
500
 
364
- <!-- > Built a new hook? [Let us know](https://github.com/open-feature/openfeature.dev/issues/new?assignees=&labels=hook&projects=&template=document-hook.yaml&title=%5BHook%5D%3A+) so we can add it to the docs! -->
501
+ > Built a new hook? [Let us know](https://github.com/open-feature/openfeature.dev/issues/new?assignees=&labels=hook&projects=&template=document-hook.yaml&title=%5BHook%5D%3A+) so we can add it to the docs!
365
502
 
366
503
  <!-- x-hide-in-docs-start -->
367
504
  ## ⭐️ Support the project
@@ -10,6 +10,7 @@ require_relative "evaluation_details"
10
10
  require_relative "client_metadata"
11
11
  require_relative "hooks"
12
12
  require_relative "client"
13
+ require_relative "tracking_event_details"
13
14
  require_relative "provider"
14
15
 
15
16
  module OpenFeature
@@ -68,6 +69,19 @@ module OpenFeature
68
69
  def logger=(new_logger)
69
70
  configuration.logger = new_logger
70
71
  end
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
+
82
+ def shutdown
83
+ configuration.shutdown
84
+ end
71
85
  end
72
86
  end
73
87
  end
@@ -28,6 +28,10 @@ module OpenFeature
28
28
  @hooks = []
29
29
  end
30
30
 
31
+ def provider_status
32
+ OpenFeature::SDK.configuration.provider_state(@provider)
33
+ end
34
+
31
35
  def add_handler(event_type, handler = nil, &block)
32
36
  actual_handler = handler || block
33
37
  OpenFeature::SDK.configuration.add_client_handler(self, event_type, actual_handler)
@@ -38,6 +42,18 @@ module OpenFeature
38
42
  OpenFeature::SDK.configuration.remove_client_handler(self, event_type, actual_handler)
39
43
  end
40
44
 
45
+ # Tracking API (spec 6.1.1.1) — dynamic-context paradigm
46
+ #
47
+ # Records a tracking event. If the provider does not implement
48
+ # tracking, this is a no-op (spec 6.1.4).
49
+ def track(tracking_event_name, evaluation_context: nil, tracking_event_details: nil)
50
+ return unless @provider.respond_to?(:track)
51
+
52
+ built_context = build_evaluation_context(evaluation_context)
53
+
54
+ @provider.track(tracking_event_name, evaluation_context: built_context, tracking_event_details: tracking_event_details)
55
+ end
56
+
41
57
  RESULT_TYPE.each do |result_type|
42
58
  SUFFIXES.each do |suffix|
43
59
  class_eval <<-RUBY, __FILE__, __LINE__ + 1
@@ -54,11 +70,19 @@ module OpenFeature
54
70
  def fetch_details(type:, flag_key:, default_value:, evaluation_context: nil, invocation_hooks: [], hook_hints: nil)
55
71
  validate_default_value_type(type, default_value)
56
72
 
57
- built_context = EvaluationContextBuilder.new.call(
58
- api_context: OpenFeature::SDK.evaluation_context,
59
- client_context: self.evaluation_context,
60
- invocation_context: evaluation_context
61
- )
73
+ if OpenFeature::SDK.configuration.provider_tracked?(@provider)
74
+ error_code = short_circuit_error_code(provider_status)
75
+ if error_code
76
+ resolution = Provider::ResolutionDetails.new(
77
+ value: default_value,
78
+ error_code: error_code,
79
+ reason: Provider::Reason::ERROR
80
+ )
81
+ return EvaluationDetails.new(flag_key: flag_key, resolution_details: resolution)
82
+ end
83
+ end
84
+
85
+ built_context = build_evaluation_context(evaluation_context)
62
86
 
63
87
  # Assemble ordered hooks: API → Client → Invocation → Provider (spec 4.4.2)
64
88
  provider_hooks = @provider.respond_to?(:hooks) ? Array(@provider.hooks) : []
@@ -109,6 +133,25 @@ module OpenFeature
109
133
  EvaluationDetails.new(flag_key: flag_key, resolution_details: resolution_details)
110
134
  end
111
135
 
136
+ def short_circuit_error_code(state)
137
+ case state
138
+ when ProviderState::NOT_READY then Provider::ErrorCode::PROVIDER_NOT_READY
139
+ when ProviderState::FATAL then Provider::ErrorCode::PROVIDER_FATAL
140
+ end
141
+ end
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
+
112
155
  def validate_default_value_type(type, default_value)
113
156
  expected_classes = TYPE_CLASS_MAP[type]
114
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
@@ -76,6 +76,26 @@ module OpenFeature
76
76
  set_provider_internal(provider, domain: domain, wait_for_init: true)
77
77
  end
78
78
 
79
+ def provider_state(provider)
80
+ @provider_state_registry.get_state(provider)
81
+ end
82
+
83
+ def provider_tracked?(provider)
84
+ @provider_state_registry.tracked?(provider)
85
+ end
86
+
87
+ def shutdown
88
+ providers_to_shutdown = @provider_mutex.synchronize { @providers.values.uniq }
89
+
90
+ providers_to_shutdown.each do |prov|
91
+ prov.shutdown if prov.respond_to?(:shutdown)
92
+ rescue => e
93
+ @logger&.warn("Error shutting down provider #{prov&.class&.name || "unknown"}: #{e.message}")
94
+ end
95
+
96
+ reset
97
+ end
98
+
79
99
  private
80
100
 
81
101
  def reset
@@ -171,10 +191,6 @@ module OpenFeature
171
191
  run_handlers_for_provider(provider, event_type, event_details)
172
192
  end
173
193
 
174
- def provider_state(provider)
175
- @provider_state_registry.get_state(provider)
176
- end
177
-
178
194
  private
179
195
 
180
196
  def extract_provider_name(provider)
@@ -219,8 +235,7 @@ module OpenFeature
219
235
  provider_state = @provider_state_registry.get_state(provider)
220
236
 
221
237
  if event_type == status_to_event[provider_state]
222
- provider_name = extract_provider_name(provider)
223
- event_details = {provider_name: provider_name}
238
+ event_details = build_event_details(provider)
224
239
 
225
240
  begin
226
241
  handler.call(event_details)
@@ -237,8 +252,7 @@ module OpenFeature
237
252
  provider_state = @provider_state_registry.get_state(client_provider)
238
253
 
239
254
  if event_type == status_to_event[provider_state]
240
- provider_name = extract_provider_name(client_provider)
241
- event_details = {provider_name: provider_name}
255
+ event_details = build_event_details(client_provider)
242
256
 
243
257
  begin
244
258
  handler.call(event_details)
@@ -248,6 +262,11 @@ module OpenFeature
248
262
  end
249
263
  end
250
264
  end
265
+
266
+ def build_event_details(provider)
267
+ stored_details = @provider_state_registry.get_details(provider)
268
+ {provider_name: extract_provider_name(provider)}.merge(stored_details)
269
+ end
251
270
  end
252
271
  end
253
272
  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
 
@@ -22,6 +22,14 @@ module OpenFeature
22
22
  @evaluation_context = evaluation_context
23
23
  @client_metadata = client_metadata
24
24
  @provider_metadata = provider_metadata
25
+ @hook_data = {}
26
+ end
27
+
28
+ # Returns a mutable hash scoped to the given hook instance.
29
+ # The same hash is returned across all hook stages (before, after, error, finally),
30
+ # allowing hooks to share state across their lifecycle (spec 4.1.5, 4.6.1).
31
+ def hook_data_for(hook)
32
+ @hook_data[hook.object_id] ||= {}
25
33
  end
26
34
  end
27
35
  end
@@ -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"
@@ -3,8 +3,9 @@
3
3
  module OpenFeature
4
4
  module SDK
5
5
  module Provider
6
- # TODO: Add evaluation context support
7
6
  class InMemoryProvider
7
+ include Provider::EventEmitter
8
+
8
9
  NAME = "In-memory Provider"
9
10
 
10
11
  attr_reader :metadata
@@ -24,7 +25,12 @@ module OpenFeature
24
25
 
25
26
  def add_flag(flag_key:, value:)
26
27
  flags[flag_key] = value
27
- # TODO: Emit PROVIDER_CONFIGURATION_CHANGED event once events are implemented
28
+ emit_provider_changed([flag_key])
29
+ end
30
+
31
+ def update_flags(new_flags)
32
+ @flags = new_flags.dup
33
+ emit_provider_changed(new_flags.keys)
28
34
  end
29
35
 
30
36
  def fetch_boolean_value(flag_key:, default_value:, evaluation_context: nil)
@@ -56,13 +62,24 @@ module OpenFeature
56
62
  attr_reader :flags
57
63
 
58
64
  def fetch_value(flag_key:, default_value:, evaluation_context:)
59
- value = flags[flag_key]
65
+ raw_value = flags[flag_key]
60
66
 
61
- if value.nil?
67
+ if raw_value.nil?
62
68
  return ResolutionDetails.new(value: default_value, error_code: ErrorCode::FLAG_NOT_FOUND, reason: Reason::ERROR)
63
69
  end
64
70
 
65
- ResolutionDetails.new(value:, reason: Reason::STATIC)
71
+ if raw_value.respond_to?(:call)
72
+ value = raw_value.call(evaluation_context)
73
+ ResolutionDetails.new(value: value, reason: Reason::TARGETING_MATCH)
74
+ else
75
+ ResolutionDetails.new(value: raw_value, reason: Reason::STATIC)
76
+ end
77
+ end
78
+
79
+ def emit_provider_changed(flag_keys)
80
+ return unless configuration_attached?
81
+
82
+ emit_event(ProviderEvent::PROVIDER_CONFIGURATION_CHANGED, flags_changed: flag_keys)
66
83
  end
67
84
  end
68
85
  end
@@ -3,7 +3,20 @@
3
3
  module OpenFeature
4
4
  module SDK
5
5
  module Provider
6
- ResolutionDetails = Struct.new(:value, :reason, :variant, :error_code, :error_message, :flag_metadata, keyword_init: true)
6
+ EMPTY_FLAG_METADATA = {}.freeze
7
+
8
+ ResolutionDetails = Struct.new(:value, :reason, :variant, :error_code, :error_message, :flag_metadata, keyword_init: true) do
9
+ def flag_metadata
10
+ raw = self[:flag_metadata]
11
+ if raw.nil?
12
+ EMPTY_FLAG_METADATA
13
+ elsif raw.frozen?
14
+ raw
15
+ else
16
+ raw.dup.freeze
17
+ end
18
+ end
19
+ end
7
20
  end
8
21
  end
9
22
  end
@@ -17,7 +17,7 @@ module OpenFeature
17
17
  return unless provider
18
18
 
19
19
  @mutex.synchronize do
20
- @states[provider.object_id] = state
20
+ @states[provider.object_id] = {state: state, details: {}}
21
21
  end
22
22
  end
23
23
 
@@ -29,7 +29,7 @@ module OpenFeature
29
29
  # Only update state if the event should cause a state change
30
30
  if new_state
31
31
  @mutex.synchronize do
32
- @states[provider.object_id] = new_state
32
+ @states[provider.object_id] = {state: new_state, details: event_details || {}}
33
33
  end
34
34
  new_state
35
35
  else
@@ -42,7 +42,17 @@ module OpenFeature
42
42
  return ProviderState::NOT_READY unless provider
43
43
 
44
44
  @mutex.synchronize do
45
- @states[provider.object_id] || ProviderState::NOT_READY
45
+ entry = @states[provider.object_id]
46
+ entry ? entry[:state] : ProviderState::NOT_READY
47
+ end
48
+ end
49
+
50
+ def get_details(provider)
51
+ return {} unless provider
52
+
53
+ @mutex.synchronize do
54
+ entry = @states[provider.object_id]
55
+ entry ? entry[:details] : {}
46
56
  end
47
57
  end
48
58
 
@@ -54,6 +64,14 @@ module OpenFeature
54
64
  end
55
65
  end
56
66
 
67
+ def tracked?(provider)
68
+ return false unless provider
69
+
70
+ @mutex.synchronize do
71
+ @states.key?(provider.object_id)
72
+ end
73
+ end
74
+
57
75
  def ready?(provider)
58
76
  get_state(provider) == ProviderState::READY
59
77
  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,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OpenFeature
4
+ module SDK
5
+ # Represents tracking event details per spec section 6.2.
6
+ #
7
+ # Requirement 6.2.1: MUST define an optional numeric value.
8
+ # Requirement 6.2.2: MUST support custom fields (string keys,
9
+ # boolean/string/number/structure values).
10
+ class TrackingEventDetails
11
+ attr_reader :value, :fields
12
+
13
+ def initialize(value: nil, **fields)
14
+ if !value.nil? && !value.is_a?(Numeric)
15
+ raise ArgumentError, "Tracking event value must be Numeric, got #{value.class}"
16
+ end
17
+
18
+ @value = value
19
+ @fields = fields.transform_keys(&:to_s)
20
+ end
21
+ end
22
+ end
23
+ 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.0"
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.0
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,6 +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
190
+ - lib/open_feature/sdk/tracking_event_details.rb
191
+ - lib/open_feature/sdk/transaction_context_propagator.rb
188
192
  - lib/open_feature/sdk/version.rb
189
193
  - release-please-config.json
190
194
  - renovate.json