openfeature-sdk 0.5.1 → 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: 36540a362c2f482ce84d2aa8ba3a6efd9b5d68fe7881ed4037f1422ad85f5525
4
- data.tar.gz: 4f7e6a764074d50467949e056b0d50be794df0a67323bb80c5e34a724236314b
3
+ metadata.gz: ac3b2793c1b6453e2525dcc04d7646fd4e5d61a87e6ade8b257754cf9b9c991c
4
+ data.tar.gz: c5aeedfe23851ef99e26024bf1db5f4ba38d7f669b9ea6b3e1e5f63d0693c1a6
5
5
  SHA512:
6
- metadata.gz: b77f6731c17b537069bbf72cf4760e88293bc59c9ae147079ff3b24ac6ac7f1b18b42b49e29fd1a04a85a6054d0a43834289561c06d6b342ba579ec23d510ef8
7
- data.tar.gz: 077d8a3f20c396c82413dad70b9a93cff0cd27a76ff8034f67c50bfa0d08235e3424755a4cd33b81c39fed962d7a62e40d1f257d3bbc4e87bac787d0e315b3fb
6
+ metadata.gz: 9aa41845c24248cf6eec381f4531fbfc301196ea748f3b3189333c4879c64a2dc1eda8a935e3514291dcbb5100c1668260e1b87bec837952e1eb1777ae083501
7
+ data.tar.gz: c162a21b0a5b4ee91f132e676440b84cf7862afccd689416f97e9b41f9284bf8b86de5050a1c32a3c98096ec39074cbb0ae1dcbe0d85d4db905d447f3baccd3d
@@ -1,3 +1,3 @@
1
1
  {
2
- ".": "0.5.1"
2
+ ".": "0.6.1"
3
3
  }
data/.ruby-version CHANGED
@@ -1 +1 @@
1
- 3.4.8
1
+ 4.0.1
data/.tool-versions CHANGED
@@ -1 +1 @@
1
- ruby 3.4.8
1
+ ruby 4.0.1
data/CHANGELOG.md CHANGED
@@ -1,5 +1,28 @@
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
+
15
+ ## [0.6.0](https://github.com/open-feature/ruby-sdk/compare/v0.5.1...v0.6.0) (2026-03-05)
16
+
17
+
18
+ ### ⚠ BREAKING CHANGES
19
+
20
+ * add Ruby 4.0 support, require minimum Ruby 3.4 ([#217](https://github.com/open-feature/ruby-sdk/issues/217))
21
+
22
+ ### Features
23
+
24
+ * add Ruby 4.0 support, require minimum Ruby 3.4 ([#217](https://github.com/open-feature/ruby-sdk/issues/217)) ([f38ba40](https://github.com/open-feature/ruby-sdk/commit/f38ba40b31beb650ba475c631947fc7969e476fa))
25
+
3
26
  ## [0.5.1](https://github.com/open-feature/ruby-sdk/compare/v0.5.0...v0.5.1) (2026-03-04)
4
27
 
5
28
 
data/CLAUDE.md CHANGED
@@ -2,71 +2,50 @@
2
2
 
3
3
  This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
4
 
5
- ## Project Overview
5
+ ## Overview
6
6
 
7
- OpenFeature Ruby SDK implements the [OpenFeature specification](https://openfeature.dev) (v0.8.0) for vendor-agnostic feature flag management. Published as the `openfeature-sdk` gem. Pure Ruby, no runtime dependencies. Requires Ruby >= 3.1.
7
+ This is the official OpenFeature SDK for Ruby — an implementation of the [OpenFeature specification](https://openfeature.dev) providing a vendor-agnostic API for feature flag evaluation. Published as the `openfeature-sdk` gem. Requires Ruby >= 3.1.
8
8
 
9
9
  ## Commands
10
10
 
11
- ```bash
12
- # Install dependencies
13
- bundle install
11
+ - **Run all tests:** `bundle exec rspec`
12
+ - **Run a single test file:** `bundle exec rspec spec/open_feature/sdk/client_spec.rb`
13
+ - **Run a specific test by line:** `bundle exec rspec spec/open_feature/sdk/client_spec.rb:43`
14
+ - **Lint:** `bundle exec standardrb`
15
+ - **Lint with autofix:** `bundle exec standardrb --fix`
16
+ - **Default rake (tests + lint):** `bundle exec rake`
14
17
 
15
- # Run full test suite + linting (default rake task)
16
- bundle exec rake
18
+ Note: Linting uses [Standard Ruby](https://github.com/standardrb/standard) (configured via the `standard` gem), which enforces double-quoted strings and its own opinionated style. There is no `.rubocop.yml` — Standard manages RuboCop configuration internally. Do not use `bundle exec rubocop` directly as a stale RuboCop server may apply different rules; always use `bundle exec standardrb`.
17
19
 
18
- # Run tests only
19
- bundle exec rspec
20
-
21
- # Run a single test file
22
- bundle exec rspec spec/open_feature/sdk/client_spec.rb
23
-
24
- # Run a specific test by line number
25
- bundle exec rspec spec/open_feature/sdk/client_spec.rb:40
26
-
27
- # Lint (StandardRB with performance plugin)
28
- bundle exec rake standard
20
+ ## Architecture
29
21
 
30
- # Auto-fix lint issues
31
- bundle exec standardrb --fix
32
- ```
22
+ ### Entry point and API singleton
33
23
 
34
- ## Architecture
24
+ `OpenFeature::SDK` (in `lib/open_feature/sdk.rb`) delegates all method calls to `API.instance` via `method_missing`. `API` is a Singleton that holds a `Configuration` object and builds `Client` instances.
35
25
 
36
- Entry point: `require 'open_feature/sdk'` — the `OpenFeature::SDK` module delegates all method calls to `API.instance` (Singleton) via `method_missing`.
26
+ ### Provider duck type
37
27
 
38
- ### Core Components
28
+ Providers are not subclasses — they follow a duck type interface. Any object implementing `fetch_boolean_value`, `fetch_string_value`, `fetch_number_value`, `fetch_integer_value`, `fetch_float_value`, and `fetch_object_value` (all accepting `flag_key:`, `default_value:`, `evaluation_context:`) works as a provider. Each method must return a `ResolutionDetails` struct. Two built-in providers exist: `NoOpProvider` (default) and `InMemoryProvider` (for testing). Providers may optionally implement `init(evaluation_context)`, `shutdown`, and `metadata`.
39
29
 
40
- - **API** (`lib/open_feature/sdk/api.rb`) Singleton orchestrator. Manages providers (global or domain-scoped), builds clients, stores API-level evaluation context, and registers event handlers.
41
- - **Configuration** (`lib/open_feature/sdk/configuration.rb`) — Thread-safe provider storage. Handles provider lifecycle (init/shutdown), domain-scoped provider mapping, and event dispatching. Uses Mutex for all shared state.
42
- - **Client** (`lib/open_feature/sdk/client.rb`) — Flag evaluation interface. Uses `class_eval` metaprogramming to generate 12 typed methods: `fetch_{boolean,string,number,integer,float,object}_value` and `fetch_*_details` variants. Merges evaluation contexts (API + client + invocation).
43
- - **EvaluationContext** (`lib/open_feature/sdk/evaluation_context.rb`) — Key-value targeting data with a special `targeting_key`. Supports merging with precedence: invocation > client > API.
30
+ ### Client dynamic method generation
44
31
 
45
- ### Provider System
32
+ `Client` uses `class_eval` to metaprogram `fetch_<type>_value` and `fetch_<type>_details` methods from `RESULT_TYPE` and `SUFFIXES` arrays. This generates 12 public methods (6 types × 2 suffixes).
46
33
 
47
- - **Provider interface** — Must implement 6 `fetch_*_value` methods, optional `init(evaluation_context)` and `shutdown`. Returns `ResolutionDetails`.
48
- - **EventEmitter** (`lib/open_feature/sdk/provider/event_emitter.rb`) — Mixin that providers include to emit lifecycle events.
49
- - **Built-in providers**: `NoOpProvider` (default), `InMemoryProvider` (testing/examples).
50
- - **Provider states**: `NOT_READY → READY`, with `ERROR`, `FATAL`, `STALE` transitions. Tracked per-instance via `ProviderStateRegistry` using `object_id`.
51
- - **Initialization modes**: `set_provider` (async, background thread) or `set_provider_and_wait` (sync, raises `ProviderInitializationError` on failure).
34
+ ### Evaluation context merging
52
35
 
53
- ### Event System
36
+ `EvaluationContextBuilder` merges three layers of context with this precedence: invocation > client > API (global). Context is a hash-like object with a special `targeting_key` field.
54
37
 
55
- - **EventDispatcher** (`lib/open_feature/sdk/event_dispatcher.rb`) — Thread-safe pub-sub. Handlers called outside mutex to prevent deadlocks. Supports API-level and client-level handlers.
56
- - **ProviderEvent** constants: `PROVIDER_READY`, `PROVIDER_ERROR`, `PROVIDER_STALE`, `PROVIDER_CONFIGURATION_CHANGED`.
38
+ ### Provider eventing
57
39
 
58
- ## Test Structure
40
+ `Configuration` manages provider lifecycle events (READY, ERROR, STALE, CONFIGURATION_CHANGED). Providers can emit spontaneous events by including `Provider::EventEmitter`. Event handlers can be registered at API level (global) or client level (domain-scoped). `ProviderStateRegistry` tracks provider states; `EventDispatcher` manages handler registration and invocation.
59
41
 
60
- Tests in `spec/` split into two categories:
61
- - `spec/specification/` — OpenFeature spec compliance tests, organized by requirement number (e.g., "Requirement 1.1.1")
62
- - `spec/open_feature/` — Unit tests for individual components
42
+ ### Domain-based provider binding
63
43
 
64
- Uses Timecop for time-sensitive tests (auto-reset after each test), SimpleCov for coverage.
44
+ Providers can be registered for specific domains. `Configuration#provider(domain:)` resolves domain-specific providers, falling back to the default (nil-domain) provider. Clients are built with an optional `domain:` that binds them to a specific provider.
65
45
 
66
46
  ## Conventions
67
47
 
68
- - **Linter**: StandardRB (Ruby Standard Style) with `standard-performance` plugin, targeting Ruby 3.1
69
- - **Commits**: Conventional Commits required for PR titles (enforced by CI)
70
- - **Releases**: Automated via release-please; changelog auto-generated
71
- - **Threading**: All shared mutable state must be Mutex-protected. Provider storage uses immutable reassignment (`@providers = @providers.dup.merge(...)`)
72
- - **Structs for DTOs**: `EvaluationDetails`, `ResolutionDetails`, `ClientMetadata`, `ProviderMetadata` are `Struct`-based
48
+ - All `.rb` files must have `# frozen_string_literal: true` as the first line.
49
+ - Tests live under `spec/` and mirror the `lib/` structure. `spec/specification/` contains tests mapped to OpenFeature spec requirements.
50
+ - Always sign git commits using the `-S` flag.
51
+ - Always include DCO sign-off in commits using the `-s` flag (i.e., `git commit -s -S`). This adds a `Signed-off-by` trailer required by the project's CI.
data/Gemfile.lock CHANGED
@@ -1,41 +1,51 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- openfeature-sdk (0.5.1)
4
+ openfeature-sdk (0.6.1)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
8
8
  specs:
9
- ast (2.4.2)
10
- debug (1.9.2)
9
+ ast (2.4.3)
10
+ date (3.5.1)
11
+ debug (1.11.1)
11
12
  irb (~> 1.10)
12
13
  reline (>= 0.3.8)
13
- diff-lcs (1.5.1)
14
- docile (1.4.0)
15
- io-console (0.7.2)
16
- irb (1.12.0)
17
- rdoc
14
+ diff-lcs (1.6.2)
15
+ docile (1.4.1)
16
+ erb (6.0.2)
17
+ io-console (0.8.2)
18
+ irb (1.17.0)
19
+ pp (>= 0.6.0)
20
+ prism (>= 1.3.0)
21
+ rdoc (>= 4.0.0)
18
22
  reline (>= 0.4.2)
19
- json (2.7.1)
20
- language_server-protocol (3.17.0.3)
23
+ json (2.18.1)
24
+ language_server-protocol (3.17.0.5)
21
25
  lint_roller (1.1.0)
22
- markly (0.10.0)
23
- parallel (1.24.0)
24
- parser (3.3.0.5)
26
+ markly (0.15.2)
27
+ parallel (1.27.0)
28
+ parser (3.3.10.2)
25
29
  ast (~> 2.4.1)
26
30
  racc
27
- psych (5.1.2)
31
+ pp (0.6.3)
32
+ prettyprint
33
+ prettyprint (0.2.0)
34
+ prism (1.9.0)
35
+ psych (5.3.1)
36
+ date
28
37
  stringio
29
- racc (1.7.3)
38
+ racc (1.8.1)
30
39
  rainbow (3.1.1)
31
- rake (13.1.0)
32
- rdoc (6.6.3.1)
40
+ rake (13.3.1)
41
+ rdoc (7.2.0)
42
+ erb
33
43
  psych (>= 4.0.0)
34
- regexp_parser (2.9.0)
35
- reline (0.5.0)
44
+ tsort
45
+ regexp_parser (2.11.3)
46
+ reline (0.6.3)
36
47
  io-console (~> 0.5)
37
- rexml (3.3.6)
38
- strscan
48
+ rexml (3.4.4)
39
49
  rspec (3.12.0)
40
50
  rspec-core (~> 3.12.0)
41
51
  rspec-expectations (~> 3.12.0)
@@ -49,54 +59,59 @@ GEM
49
59
  diff-lcs (>= 1.2.0, < 2.0)
50
60
  rspec-support (~> 3.12.0)
51
61
  rspec-support (3.12.2)
52
- rubocop (1.62.1)
62
+ rubocop (1.84.2)
53
63
  json (~> 2.3)
54
- language_server-protocol (>= 3.17.0)
64
+ language_server-protocol (~> 3.17.0.2)
65
+ lint_roller (~> 1.1.0)
55
66
  parallel (~> 1.10)
56
67
  parser (>= 3.3.0.2)
57
68
  rainbow (>= 2.2.2, < 4.0)
58
- regexp_parser (>= 1.8, < 3.0)
59
- rexml (>= 3.2.5, < 4.0)
60
- rubocop-ast (>= 1.31.1, < 2.0)
69
+ regexp_parser (>= 2.9.3, < 3.0)
70
+ rubocop-ast (>= 1.49.0, < 2.0)
61
71
  ruby-progressbar (~> 1.7)
62
- unicode-display_width (>= 2.4.0, < 3.0)
63
- rubocop-ast (1.31.2)
64
- parser (>= 3.3.0.4)
65
- rubocop-performance (1.20.2)
66
- rubocop (>= 1.48.1, < 2.0)
67
- rubocop-ast (>= 1.30.0, < 2.0)
72
+ unicode-display_width (>= 2.4.0, < 4.0)
73
+ rubocop-ast (1.49.0)
74
+ parser (>= 3.3.7.2)
75
+ prism (~> 1.7)
76
+ rubocop-performance (1.26.1)
77
+ lint_roller (~> 1.1)
78
+ rubocop (>= 1.75.0, < 2.0)
79
+ rubocop-ast (>= 1.47.1, < 2.0)
68
80
  ruby-progressbar (1.13.0)
69
81
  simplecov (0.22.0)
70
82
  docile (~> 1.1)
71
83
  simplecov-html (~> 0.11)
72
84
  simplecov_json_formatter (~> 0.1)
73
- simplecov-cobertura (2.1.0)
85
+ simplecov-cobertura (3.1.0)
74
86
  rexml
75
87
  simplecov (~> 0.19)
76
- simplecov-html (0.12.3)
88
+ simplecov-html (0.13.2)
77
89
  simplecov_json_formatter (0.1.4)
78
- standard (1.35.1)
90
+ standard (1.54.0)
79
91
  language_server-protocol (~> 3.17.0.2)
80
92
  lint_roller (~> 1.0)
81
- rubocop (~> 1.62.0)
93
+ rubocop (~> 1.84.0)
82
94
  standard-custom (~> 1.0.0)
83
- standard-performance (~> 1.3)
95
+ standard-performance (~> 1.8)
84
96
  standard-custom (1.0.2)
85
97
  lint_roller (~> 1.0)
86
98
  rubocop (~> 1.50)
87
- standard-performance (1.3.1)
99
+ standard-performance (1.9.0)
88
100
  lint_roller (~> 1.1)
89
- rubocop-performance (~> 1.20.2)
90
- stringio (3.1.0)
91
- strscan (3.1.0)
101
+ rubocop-performance (~> 1.26.0)
102
+ stringio (3.2.0)
92
103
  timecop (0.9.10)
93
- unicode-display_width (2.5.0)
104
+ tsort (0.2.0)
105
+ unicode-display_width (3.2.0)
106
+ unicode-emoji (~> 4.1)
107
+ unicode-emoji (4.2.0)
94
108
 
95
109
  PLATFORMS
96
110
  arm64-darwin-21
97
111
  arm64-darwin-22
98
112
  arm64-darwin-23
99
113
  arm64-darwin-24
114
+ arm64-darwin-25
100
115
  x64-mingw-ucrt
101
116
  x64-mingw32
102
117
  x86_64-darwin-19
@@ -111,7 +126,7 @@ DEPENDENCIES
111
126
  rake (~> 13.0)
112
127
  rspec (~> 3.12.0)
113
128
  simplecov (~> 0.22.0)
114
- simplecov-cobertura (~> 2.1.0)
129
+ simplecov-cobertura (~> 3.0)
115
130
  standard
116
131
  standard-performance
117
132
  timecop (~> 0.9.10)
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.5.1">
21
- <img alt="Release" src="https://img.shields.io/static/v1?label=release&message=v0.5.1&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 -->
@@ -38,9 +38,8 @@
38
38
 
39
39
  | Supported Ruby Version | OS |
40
40
  | ------------ | --------------------- |
41
- | Ruby 3.1.4 | Windows, MacOS, Linux |
42
- | Ruby 3.2.3 | Windows, MacOS, Linux |
43
- | Ruby 3.3.0 | Windows, MacOS, Linux |
41
+ | Ruby 3.4.x | Windows, MacOS, Linux |
42
+ | Ruby 4.0.x | Windows, MacOS, Linux |
44
43
 
45
44
  ### Install
46
45
 
@@ -101,13 +100,14 @@ object = client.fetch_object_value(flag_key: 'object_value', default_value: { na
101
100
  | ------ | --------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------|
102
101
  | ✅ | [Providers](#providers) | Integrate with a commercial, open source, or in-house feature management tool. |
103
102
  | ✅ | [Targeting](#targeting) | Contextually-aware flag evaluation using [evaluation context](https://openfeature.dev/docs/reference/concepts/evaluation-context). |
104
- | ⚠️ | [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. |
105
104
  | ❌ | [Logging](#logging) | Integrate with popular logging packages. |
106
105
  | ✅ | [Domains](#domains) | Logically bind clients with providers. |
107
106
  | ✅ | [Eventing](#eventing) | React to state changes in the provider or flag management system. |
108
- | ⚠️ | [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. |
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
- | ⚠️ | [Extending](#extending) | Extend OpenFeature with custom providers and hooks. |
110
+ | | [Extending](#extending) | Extend OpenFeature with custom providers and hooks. |
111
111
 
112
112
  <sub>Implemented: ✅ | In-progress: ⚠️ | Not implemented yet: ❌</sub>
113
113
 
@@ -201,15 +201,49 @@ bool_value = client.fetch_boolean_value(
201
201
 
202
202
  ### Hooks
203
203
 
204
- Coming Soon! [Issue available](https://github.com/open-feature/ruby-sdk/issues/52) to be worked on.
205
-
206
- <!-- [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.
207
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.
208
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.
209
207
 
210
- 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
211
239
 
212
- <!-- 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
+ ```
213
247
 
214
248
  ### Logging
215
249
 
@@ -277,19 +311,53 @@ OpenFeature::SDK.remove_handler(OpenFeature::SDK::ProviderEvent::PROVIDER_READY,
277
311
 
278
312
  ### Shutdown
279
313
 
280
- Coming Soon! [Issue available](https://github.com/open-feature/ruby-sdk/issues/149) to be worked on.
281
-
282
- <!-- 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.
283
315
  This should only be called when your application is in the process of shutting down.
284
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
+
285
324
  ```ruby
286
325
  class MyProvider
287
326
  def shutdown
288
327
  # Perform any shutdown/reclamation steps with flag management system here
289
- # Return value is ignored
290
328
  end
291
329
  end
292
- ``` -->
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.
293
361
 
294
362
  ### Transaction Context Propagation
295
363
 
@@ -345,6 +413,12 @@ class MyProvider
345
413
  def fetch_object_value(flag_key:, default_value:, evaluation_context: nil)
346
414
  # Retrieve a hash value from provider source
347
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
348
422
  end
349
423
  ```
350
424
 
@@ -352,17 +426,29 @@ end
352
426
 
353
427
  ### Develop a hook
354
428
 
355
- Coming Soon! [Issue available](https://github.com/open-feature/ruby-sdk/issues/52) to be worked on.
356
-
357
- <!-- 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.
358
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.
359
- Implement your own hook by conforming to the `Hook interface`.
360
- To satisfy the interface, all methods (`Before`/`After`/`Finally`/`Error`) need to be defined.
361
- 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
362
437
 
363
- <!-- 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
+ ```
364
450
 
365
- <!-- > 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!
366
452
 
367
453
  <!-- x-hide-in-docs-start -->
368
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) }
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module OpenFeature
2
4
  module SDK
3
5
  ClientMetadata = Struct.new(:domain, keyword_init: true)
@@ -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
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module OpenFeature
2
4
  module SDK
3
5
  class EvaluationContext
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module OpenFeature
2
4
  module SDK
3
5
  # Used to combine evaluation contexts from different sources
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module OpenFeature
2
4
  module SDK
3
5
  EvaluationDetails = Struct.new(:flag_key, :resolution_details, keyword_init: true) do
@@ -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
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module OpenFeature
2
4
  module SDK
3
5
  module Provider
@@ -1,8 +1,11 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module OpenFeature
2
4
  module SDK
3
5
  module Provider
4
- # TODO: Add evaluation context support
5
6
  class InMemoryProvider
7
+ include Provider::EventEmitter
8
+
6
9
  NAME = "In-memory Provider"
7
10
 
8
11
  attr_reader :metadata
@@ -22,7 +25,12 @@ module OpenFeature
22
25
 
23
26
  def add_flag(flag_key:, value:)
24
27
  flags[flag_key] = value
25
- # 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)
26
34
  end
27
35
 
28
36
  def fetch_boolean_value(flag_key:, default_value:, evaluation_context: nil)
@@ -54,13 +62,24 @@ module OpenFeature
54
62
  attr_reader :flags
55
63
 
56
64
  def fetch_value(flag_key:, default_value:, evaluation_context:)
57
- value = flags[flag_key]
65
+ raw_value = flags[flag_key]
58
66
 
59
- if value.nil?
67
+ if raw_value.nil?
60
68
  return ResolutionDetails.new(value: default_value, error_code: ErrorCode::FLAG_NOT_FOUND, reason: Reason::ERROR)
61
69
  end
62
70
 
63
- 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)
64
83
  end
65
84
  end
66
85
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module OpenFeature
2
4
  module SDK
3
5
  module Provider
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module OpenFeature
2
4
  module SDK
3
5
  module Provider
@@ -1,7 +1,22 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module OpenFeature
2
4
  module SDK
3
5
  module Provider
4
- 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
5
20
  end
6
21
  end
7
22
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative "provider/error_code"
2
4
  require_relative "provider/reason"
3
5
  require_relative "provider/resolution_details"
@@ -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.5.1"
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.5.1
4
+ version: 0.6.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - OpenFeature Authors
@@ -113,14 +113,14 @@ dependencies:
113
113
  requirements:
114
114
  - - "~>"
115
115
  - !ruby/object:Gem::Version
116
- version: 2.1.0
116
+ version: '3.0'
117
117
  type: :development
118
118
  prerelease: false
119
119
  version_requirements: !ruby/object:Gem::Requirement
120
120
  requirements:
121
121
  - - "~>"
122
122
  - !ruby/object:Gem::Version
123
- version: 2.1.0
123
+ version: '3.0'
124
124
  - !ruby/object:Gem::Dependency
125
125
  name: timecop
126
126
  requirement: !ruby/object:Gem::Requirement
@@ -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
@@ -204,14 +205,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
204
205
  requirements:
205
206
  - - ">="
206
207
  - !ruby/object:Gem::Version
207
- version: '3.1'
208
+ version: '3.4'
208
209
  required_rubygems_version: !ruby/object:Gem::Requirement
209
210
  requirements:
210
211
  - - ">="
211
212
  - !ruby/object:Gem::Version
212
213
  version: '0'
213
214
  requirements: []
214
- rubygems_version: 3.6.9
215
+ rubygems_version: 4.0.3
215
216
  specification_version: 4
216
217
  summary: OpenFeature SDK for Ruby
217
218
  test_files: []