openfeature-sdk 0.6.0 → 0.6.1

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: ac3b2793c1b6453e2525dcc04d7646fd4e5d61a87e6ade8b257754cf9b9c991c
4
+ data.tar.gz: c5aeedfe23851ef99e26024bf1db5f4ba38d7f669b9ea6b3e1e5f63d0693c1a6
5
5
  SHA512:
6
- metadata.gz: 8dc359e5bb318d79e2fcb48bc0c393c055578cf0a331b02bc61f82910b3da973eaf487457db80d9cd73f12303b15a77cbef9062481933c3d7b4c305de0ed1cc2
7
- data.tar.gz: c733ce2f12dbd64ad9fd4e42a5c6cd17c805aa0ab746408c222aee42e6af5ba385ff92baf6cffbf79e4e949dbd7e6b5b2c67937694717ef5b41396353b9f9391
6
+ metadata.gz: 9aa41845c24248cf6eec381f4531fbfc301196ea748f3b3189333c4879c64a2dc1eda8a935e3514291dcbb5100c1668260e1b87bec837952e1eb1777ae083501
7
+ data.tar.gz: c162a21b0a5b4ee91f132e676440b84cf7862afccd689416f97e9b41f9284bf8b86de5050a1c32a3c98096ec39074cbb0ae1dcbe0d85d4db905d447f3baccd3d
@@ -1,3 +1,3 @@
1
1
  {
2
- ".": "0.6.0"
2
+ ".": "0.6.1"
3
3
  }
data/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.6.1](https://github.com/open-feature/ruby-sdk/compare/v0.6.0...v0.6.1) (2026-03-05)
4
+
5
+
6
+ ### Features
7
+
8
+ * 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))
9
+ * 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))
10
+ * 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))
11
+ * 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))
12
+ * 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))
13
+ * 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))
14
+
3
15
  ## [0.6.0](https://github.com/open-feature/ruby-sdk/compare/v0.5.1...v0.6.0) (2026-03-05)
4
16
 
5
17
 
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.1)
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.1">
21
+ <img alt="Release" src="https://img.shields.io/static/v1?label=release&message=v0.6.1&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. |
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
- | ⚠️ | [Shutdown](#shutdown) | Gracefully clean up a provider during application shutdown. |
107
+ | | [Shutdown](#shutdown) | Gracefully clean up a provider during application shutdown. |
108
+ | ✅ | [Tracking](#tracking) | Associate user actions with feature flag evaluations for experimentation. |
108
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
- | ⚠️ | [Extending](#extending) | Extend OpenFeature with custom providers and hooks. |
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,15 +201,49 @@ 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
232
+
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
210
239
 
211
- <!-- TODO: code example of setting hooks at all levels -->
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
 
@@ -276,19 +311,53 @@ OpenFeature::SDK.remove_handler(OpenFeature::SDK::ProviderEvent::PROVIDER_READY,
276
311
 
277
312
  ### Shutdown
278
313
 
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.
314
+ The OpenFeature API provides a `shutdown` method to perform cleanup of all registered providers.
282
315
  This should only be called when your application is in the process of shutting down.
283
316
 
317
+ ```ruby
318
+ # Shut down all registered providers and clear state
319
+ OpenFeature::SDK.shutdown
320
+ ```
321
+
322
+ Individual providers can implement a `shutdown` method to perform cleanup:
323
+
284
324
  ```ruby
285
325
  class MyProvider
286
326
  def shutdown
287
327
  # Perform any shutdown/reclamation steps with flag management system here
288
- # Return value is ignored
289
328
  end
290
329
  end
291
- ``` -->
330
+ ```
331
+
332
+ ### Tracking
333
+
334
+ The tracking API allows you to use OpenFeature abstractions and objects to associate user actions with feature flag evaluations.
335
+ This is essential for robust experimentation powered by feature flags.
336
+ 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.
337
+
338
+ ```ruby
339
+ client = OpenFeature::SDK.build_client
340
+
341
+ # Simple tracking event
342
+ client.track("checkout_completed")
343
+
344
+ # With evaluation context
345
+ client.track(
346
+ "purchase",
347
+ evaluation_context: OpenFeature::SDK::EvaluationContext.new(targeting_key: "user-123")
348
+ )
349
+
350
+ # With tracking event details (optional numeric value + custom fields)
351
+ details = OpenFeature::SDK::TrackingEventDetails.new(
352
+ value: 99.99,
353
+ plan: "premium",
354
+ currency: "USD"
355
+ )
356
+ client.track("subscription", tracking_event_details: details)
357
+ ```
358
+
359
+ Note that some providers may not support tracking; if the provider does not implement a `track` method, the call is a no-op.
360
+ Check the documentation for your [provider](#providers) for more information.
292
361
 
293
362
  ### Transaction Context Propagation
294
363
 
@@ -344,6 +413,12 @@ class MyProvider
344
413
  def fetch_object_value(flag_key:, default_value:, evaluation_context: nil)
345
414
  # Retrieve a hash value from provider source
346
415
  end
416
+
417
+ # Optional: implement tracking support (spec 6.1.4)
418
+ # If not defined, Client#track is a no-op
419
+ def track(tracking_event_name, evaluation_context:, tracking_event_details:)
420
+ # Record a tracking event with your flag management system
421
+ end
347
422
  end
348
423
  ```
349
424
 
@@ -351,17 +426,29 @@ end
351
426
 
352
427
  ### Develop a hook
353
428
 
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.
429
+ To develop a hook, you need to create a new project and include the OpenFeature SDK as a dependency.
357
430
  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). -->
431
+ Implement your own hook by including the `OpenFeature::SDK::Hooks::Hook` module.
432
+ You only need to define the stages you care about unimplemented stages are no-ops by default.
433
+
434
+ ```ruby
435
+ class MyLoggingHook
436
+ include OpenFeature::SDK::Hooks::Hook
361
437
 
362
- <!-- TODO: code example of hook implementation -->
438
+ def before(hook_context:, hints:)
439
+ puts "Evaluating #{hook_context.flag_key}"
440
+ nil # Return nil or an EvaluationContext to merge
441
+ end
442
+
443
+ def after(hook_context:, evaluation_details:, hints:)
444
+ puts "Result: #{evaluation_details.value}"
445
+ end
446
+
447
+ # error and finally are optional — only define what you need
448
+ end
449
+ ```
363
450
 
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! -->
451
+ > 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
452
 
366
453
  <!-- x-hide-in-docs-start -->
367
454
  ## ⭐️ 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,10 @@ module OpenFeature
68
69
  def logger=(new_logger)
69
70
  configuration.logger = new_logger
70
71
  end
72
+
73
+ def shutdown
74
+ configuration.shutdown
75
+ end
71
76
  end
72
77
  end
73
78
  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,22 @@ 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 = EvaluationContextBuilder.new.call(
53
+ api_context: OpenFeature::SDK.evaluation_context,
54
+ client_context: self.evaluation_context,
55
+ invocation_context: evaluation_context
56
+ )
57
+
58
+ @provider.track(tracking_event_name, evaluation_context: built_context, tracking_event_details: tracking_event_details)
59
+ end
60
+
41
61
  RESULT_TYPE.each do |result_type|
42
62
  SUFFIXES.each do |suffix|
43
63
  class_eval <<-RUBY, __FILE__, __LINE__ + 1
@@ -54,6 +74,18 @@ module OpenFeature
54
74
  def fetch_details(type:, flag_key:, default_value:, evaluation_context: nil, invocation_hooks: [], hook_hints: nil)
55
75
  validate_default_value_type(type, default_value)
56
76
 
77
+ if OpenFeature::SDK.configuration.provider_tracked?(@provider)
78
+ error_code = short_circuit_error_code(provider_status)
79
+ if error_code
80
+ resolution = Provider::ResolutionDetails.new(
81
+ value: default_value,
82
+ error_code: error_code,
83
+ reason: Provider::Reason::ERROR
84
+ )
85
+ return EvaluationDetails.new(flag_key: flag_key, resolution_details: resolution)
86
+ end
87
+ end
88
+
57
89
  built_context = EvaluationContextBuilder.new.call(
58
90
  api_context: OpenFeature::SDK.evaluation_context,
59
91
  client_context: self.evaluation_context,
@@ -109,6 +141,13 @@ module OpenFeature
109
141
  EvaluationDetails.new(flag_key: flag_key, resolution_details: resolution_details)
110
142
  end
111
143
 
144
+ def short_circuit_error_code(state)
145
+ case state
146
+ when ProviderState::NOT_READY then Provider::ErrorCode::PROVIDER_NOT_READY
147
+ when ProviderState::FATAL then Provider::ErrorCode::PROVIDER_FATAL
148
+ end
149
+ end
150
+
112
151
  def validate_default_value_type(type, default_value)
113
152
  expected_classes = TYPE_CLASS_MAP[type]
114
153
  unless expected_classes.any? { |klass| default_value.is_a?(klass) }
@@ -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
@@ -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
@@ -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,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
@@ -2,6 +2,6 @@
2
2
 
3
3
  module OpenFeature
4
4
  module SDK
5
- VERSION = "0.6.0"
5
+ VERSION = "0.6.1"
6
6
  end
7
7
  end
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.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - OpenFeature Authors
@@ -185,6 +185,7 @@ files:
185
185
  - lib/open_feature/sdk/provider_initialization_error.rb
186
186
  - lib/open_feature/sdk/provider_state.rb
187
187
  - lib/open_feature/sdk/provider_state_registry.rb
188
+ - lib/open_feature/sdk/tracking_event_details.rb
188
189
  - lib/open_feature/sdk/version.rb
189
190
  - release-please-config.json
190
191
  - renovate.json