openfeature-sdk 0.6.3 → 0.6.4

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: bd3ec1b654a844eaa7bcafe400d6d2075199fa49ba6823aad1c8e1ce8c05a526
4
- data.tar.gz: 3d159cb4f4df832154cfa7a4657ee53f357827a3d55dcbe124bf4716248e24b8
3
+ metadata.gz: 4aa61c48f017d3311bfce9022a5e08d023ee1e2c09eb8799ff0e0a8424cf9fe3
4
+ data.tar.gz: 8c559636076a0ec01911e9057d57cde4e430a7bc4b2543835dcd90527e0f0c0e
5
5
  SHA512:
6
- metadata.gz: 66667a224162e00b882ad73b45e5d2825cc35f47007ca961749cc7115cbe1223d881be0cfff89eb4a5d3fd3f0235c2548f7a89420de9ff7bb3a6b0fd4bcada65
7
- data.tar.gz: f799ac085e5593bc9b14a040ebddc8326c57ff46e36285ed0ea9b44008dd0d8d58a149a1c0f7fc9be50aeb3cf8d60ffc2859f2b3f9099d49e1db3df6f795b669
6
+ metadata.gz: 0f683b0d315b225259e540fb9f7d86f60a7e334c1501b7f05d45d677d1e7ee6cfb951b463514c05d9c64b5b413935d1bdf95cd684aa5f05136ed87a6cf2ba122
7
+ data.tar.gz: 618dcac7749c461bb77b92f7f7b68e2071b4b71825d05330bb18a08df3a929019ef0fa8d12d79b117fdd3fce431d48f5e15d2f90aa884fa4f28b1226229f9db0
@@ -1,3 +1,3 @@
1
1
  {
2
- ".": "0.6.3"
2
+ ".": "0.6.4"
3
3
  }
data/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.6.4](https://github.com/open-feature/ruby-sdk/compare/v0.6.3...v0.6.4) (2026-03-07)
4
+
5
+
6
+ ### Features
7
+
8
+ * add OTel-compatible telemetry utility ([#240](https://github.com/open-feature/ruby-sdk/issues/240)) ([a03e524](https://github.com/open-feature/ruby-sdk/commit/a03e524681a38c8762257049fae360fa15fcfba3))
9
+
3
10
  ## [0.6.3](https://github.com/open-feature/ruby-sdk/compare/v0.6.2...v0.6.3) (2026-03-07)
4
11
 
5
12
 
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- openfeature-sdk (0.6.3)
4
+ openfeature-sdk (0.6.4)
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.3">
21
- <img alt="Release" src="https://img.shields.io/static/v1?label=release&message=v0.6.3&color=blue&style=for-the-badge" />
20
+ <a href="https://github.com/open-feature/ruby-sdk/releases/tag/v0.6.4">
21
+ <img alt="Release" src="https://img.shields.io/static/v1?label=release&message=v0.6.4&color=blue&style=for-the-badge" />
22
22
  </a>
23
23
 
24
24
  <!-- x-release-please-end -->
@@ -0,0 +1,98 @@
1
+ # Telemetry Utility Design
2
+
3
+ ## Overview
4
+
5
+ Add an OTel-compatible telemetry utility to the OpenFeature Ruby SDK that creates
6
+ structured evaluation events from hook context and evaluation details. This utility
7
+ is dependency-free (no OTel gem required) and follows the pattern established by
8
+ the Go SDK's `telemetry` package.
9
+
10
+ Addresses: https://github.com/open-feature/ruby-sdk/issues/176
11
+
12
+ ## References
13
+
14
+ - [OpenFeature Spec Appendix D (Observability)](https://openfeature.dev/specification/appendix-d/)
15
+ - [OTel Semantic Conventions for Feature Flags](https://opentelemetry.io/docs/specs/semconv/feature-flags/feature-flags-logs/)
16
+ - [Go SDK telemetry package](https://github.com/open-feature/go-sdk/tree/main/openfeature/telemetry)
17
+ - [JS SDK reference PR](https://github.com/open-feature/js-sdk/pull/1120)
18
+
19
+ ## Design Decisions
20
+
21
+ 1. **Single public method** accepting `hook_context:` and `evaluation_details:` keyword
22
+ arguments — mirrors the `finally` hook stage signature for zero-friction integration.
23
+ 2. **Returns a Struct** (`EvaluationEvent`) with `name` and `attributes` fields — matches
24
+ the SDK's existing Struct conventions (`ResolutionDetails`, `ClientMetadata`, etc.).
25
+ 3. **Constants in the Telemetry module directly** — flat namespace matching Go SDK and
26
+ existing Ruby SDK patterns (e.g., `Provider::Reason`).
27
+ 4. **Hard-coded metadata mappings only** — maps `contextId`, `flagSetId`, `version` from
28
+ flag metadata to OTel keys. Unknown metadata keys are ignored. Custom attributes can
29
+ be added via hooks in ruby-sdk-contrib.
30
+ 5. **No third-party dependencies** — pure data transformation using only standard library.
31
+
32
+ ## File Structure
33
+
34
+ - `lib/open_feature/sdk/telemetry.rb` — module with constants, struct, and utility function
35
+ - `spec/open_feature/sdk/telemetry_spec.rb` — tests
36
+ - `lib/open_feature/sdk.rb` — add `require_relative "sdk/telemetry"`
37
+
38
+ ## Constants
39
+
40
+ ```ruby
41
+ EVENT_NAME = "feature_flag.evaluation"
42
+
43
+ FLAG_KEY = "feature_flag.key"
44
+ CONTEXT_ID_KEY = "feature_flag.context.id"
45
+ ERROR_MESSAGE_KEY = "error.message"
46
+ ERROR_TYPE_KEY = "error.type"
47
+ PROVIDER_NAME_KEY = "feature_flag.provider.name"
48
+ RESULT_REASON_KEY = "feature_flag.result.reason"
49
+ RESULT_VALUE_KEY = "feature_flag.result.value"
50
+ RESULT_VARIANT_KEY = "feature_flag.result.variant"
51
+ FLAG_SET_ID_KEY = "feature_flag.set.id"
52
+ VERSION_KEY = "feature_flag.version"
53
+ ```
54
+
55
+ ## Public API
56
+
57
+ ```ruby
58
+ OpenFeature::SDK::Telemetry.create_evaluation_event(
59
+ hook_context:, # Hooks::HookContext
60
+ evaluation_details: # EvaluationDetails or nil
61
+ ) # => EvaluationEvent
62
+ ```
63
+
64
+ Returns `EvaluationEvent = Struct.new(:name, :attributes, keyword_init: true)`.
65
+
66
+ ## Attribute Population Rules
67
+
68
+ | Attribute | Source | Condition |
69
+ |-----------|--------|-----------|
70
+ | `feature_flag.key` | `hook_context.flag_key` | Always |
71
+ | `feature_flag.provider.name` | `hook_context.provider_metadata.name` | When present |
72
+ | `feature_flag.result.variant` | `evaluation_details.variant` | When present (takes precedence over value) |
73
+ | `feature_flag.result.value` | `evaluation_details.value` | Only when variant is nil |
74
+ | `feature_flag.result.reason` | `evaluation_details.reason.downcase` | When present |
75
+ | `error.type` | `evaluation_details.error_code.downcase` | When error occurred |
76
+ | `error.message` | `evaluation_details.error_message` | When error occurred |
77
+ | `feature_flag.context.id` | `targeting_key` or metadata `contextId` | Metadata takes precedence |
78
+ | `feature_flag.set.id` | metadata `flagSetId` | When present in flag_metadata |
79
+ | `feature_flag.version` | metadata `version` | When present in flag_metadata |
80
+
81
+ ## Error Handling
82
+
83
+ No defensive `rescue` in the utility — it is a pure data transformation. Nil inputs
84
+ are handled via guard clauses. The calling hook is responsible for exception safety
85
+ (consistent with the existing hook executor pattern).
86
+
87
+ ## Test Plan
88
+
89
+ 1. Happy path with all attributes populated
90
+ 2. Variant vs value precedence
91
+ 3. Enum downcasing (reason and error_code)
92
+ 4. Error attributes present only on error
93
+ 5. Nil evaluation_details
94
+ 6. Nil/empty flag_metadata
95
+ 7. Metadata contextId overrides targeting_key
96
+ 8. Targeting key fallback when no contextId
97
+ 9. Unknown metadata keys ignored
98
+ 10. Return type verification
@@ -0,0 +1,578 @@
1
+ # Telemetry Utility Implementation Plan
2
+
3
+ > **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
4
+
5
+ **Goal:** Add a dependency-free `OpenFeature::SDK::Telemetry` module that creates OTel-compatible evaluation events from hook context and evaluation details.
6
+
7
+ **Architecture:** Single module (`Telemetry`) in one file with constants, a Struct, and one public `module_function`. Designed for consumption in a hook's `finally` stage. No third-party dependencies.
8
+
9
+ **Tech Stack:** Ruby >= 3.1, RSpec, Standard Ruby (linter)
10
+
11
+ ---
12
+
13
+ ### Task 1: Create telemetry module with constants and struct
14
+
15
+ **Files:**
16
+ - Create: `lib/open_feature/sdk/telemetry.rb`
17
+
18
+ **Step 1: Create the module file with constants and struct**
19
+
20
+ ```ruby
21
+ # frozen_string_literal: true
22
+
23
+ module OpenFeature
24
+ module SDK
25
+ module Telemetry
26
+ EVENT_NAME = "feature_flag.evaluation"
27
+
28
+ FLAG_KEY = "feature_flag.key"
29
+ CONTEXT_ID_KEY = "feature_flag.context.id"
30
+ ERROR_MESSAGE_KEY = "error.message"
31
+ ERROR_TYPE_KEY = "error.type"
32
+ PROVIDER_NAME_KEY = "feature_flag.provider.name"
33
+ RESULT_REASON_KEY = "feature_flag.result.reason"
34
+ RESULT_VALUE_KEY = "feature_flag.result.value"
35
+ RESULT_VARIANT_KEY = "feature_flag.result.variant"
36
+ FLAG_SET_ID_KEY = "feature_flag.set.id"
37
+ VERSION_KEY = "feature_flag.version"
38
+
39
+ METADATA_KEY_MAP = {
40
+ "contextId" => CONTEXT_ID_KEY,
41
+ "flagSetId" => FLAG_SET_ID_KEY,
42
+ "version" => VERSION_KEY
43
+ }.freeze
44
+
45
+ EvaluationEvent = Struct.new(:name, :attributes, keyword_init: true)
46
+ end
47
+ end
48
+ end
49
+ ```
50
+
51
+ **Step 2: Wire up the require**
52
+
53
+ Modify: `lib/open_feature/sdk.rb` — add `require_relative "sdk/telemetry"` after the existing requires (line 3 area).
54
+
55
+ **Step 3: Verify it loads**
56
+
57
+ Run: `bundle exec ruby -e "require 'open_feature/sdk'; puts OpenFeature::SDK::Telemetry::EVENT_NAME"`
58
+ Expected: `feature_flag.evaluation`
59
+
60
+ **Step 4: Commit**
61
+
62
+ ```bash
63
+ git add lib/open_feature/sdk/telemetry.rb lib/open_feature/sdk.rb
64
+ git commit -s -S -m "feat(telemetry): add Telemetry module with OTel constants and EvaluationEvent struct"
65
+ ```
66
+
67
+ ---
68
+
69
+ ### Task 2: Write tests for happy path and return type
70
+
71
+ **Files:**
72
+ - Create: `spec/open_feature/sdk/telemetry_spec.rb`
73
+
74
+ **Step 1: Write the test file with happy path and return type tests**
75
+
76
+ ```ruby
77
+ # frozen_string_literal: true
78
+
79
+ require "spec_helper"
80
+
81
+ RSpec.describe OpenFeature::SDK::Telemetry do
82
+ let(:client_metadata) { OpenFeature::SDK::ClientMetadata.new(domain: "test-domain") }
83
+ let(:provider_metadata) { OpenFeature::SDK::Provider::ProviderMetadata.new(name: "test-provider") }
84
+ let(:evaluation_context) { OpenFeature::SDK::EvaluationContext.new(targeting_key: "user-123") }
85
+
86
+ let(:hook_context) do
87
+ OpenFeature::SDK::Hooks::HookContext.new(
88
+ flag_key: "my-flag",
89
+ flag_value_type: :boolean,
90
+ default_value: false,
91
+ evaluation_context: evaluation_context,
92
+ client_metadata: client_metadata,
93
+ provider_metadata: provider_metadata
94
+ )
95
+ end
96
+
97
+ let(:flag_metadata) do
98
+ {
99
+ "contextId" => "ctx-456",
100
+ "flagSetId" => "set-789",
101
+ "version" => "v1.0"
102
+ }
103
+ end
104
+
105
+ let(:resolution_details) do
106
+ OpenFeature::SDK::Provider::ResolutionDetails.new(
107
+ value: true,
108
+ reason: "TARGETING_MATCH",
109
+ variant: "enabled",
110
+ flag_metadata: flag_metadata
111
+ )
112
+ end
113
+
114
+ let(:evaluation_details) do
115
+ OpenFeature::SDK::EvaluationDetails.new(
116
+ flag_key: "my-flag",
117
+ resolution_details: resolution_details
118
+ )
119
+ end
120
+
121
+ describe ".create_evaluation_event" do
122
+ context "with full data" do
123
+ it "returns an EvaluationEvent with all attributes populated" do
124
+ event = described_class.create_evaluation_event(
125
+ hook_context: hook_context,
126
+ evaluation_details: evaluation_details
127
+ )
128
+
129
+ expect(event).to be_a(OpenFeature::SDK::Telemetry::EvaluationEvent)
130
+ expect(event.name).to eq("feature_flag.evaluation")
131
+ expect(event.attributes).to eq(
132
+ "feature_flag.key" => "my-flag",
133
+ "feature_flag.provider.name" => "test-provider",
134
+ "feature_flag.result.variant" => "enabled",
135
+ "feature_flag.result.reason" => "targeting_match",
136
+ "feature_flag.context.id" => "ctx-456",
137
+ "feature_flag.set.id" => "set-789",
138
+ "feature_flag.version" => "v1.0"
139
+ )
140
+ end
141
+ end
142
+ end
143
+ end
144
+ ```
145
+
146
+ **Step 2: Run the test to verify it fails**
147
+
148
+ Run: `bundle exec rspec spec/open_feature/sdk/telemetry_spec.rb`
149
+ Expected: FAIL — `NoMethodError: undefined method 'create_evaluation_event'`
150
+
151
+ **Step 3: Commit the failing test**
152
+
153
+ ```bash
154
+ git add spec/open_feature/sdk/telemetry_spec.rb
155
+ git commit -s -S -m "test(telemetry): add failing test for happy path"
156
+ ```
157
+
158
+ ---
159
+
160
+ ### Task 3: Implement create_evaluation_event
161
+
162
+ **Files:**
163
+ - Modify: `lib/open_feature/sdk/telemetry.rb`
164
+
165
+ **Step 1: Add the implementation to the Telemetry module**
166
+
167
+ Add after the `EvaluationEvent` struct definition:
168
+
169
+ ```ruby
170
+ module_function
171
+
172
+ def create_evaluation_event(hook_context:, evaluation_details:)
173
+ attributes = {FLAG_KEY => hook_context.flag_key}
174
+
175
+ provider_name = hook_context.provider_metadata&.name
176
+ attributes[PROVIDER_NAME_KEY] = provider_name if provider_name
177
+
178
+ targeting_key = hook_context.evaluation_context&.targeting_key
179
+ attributes[CONTEXT_ID_KEY] = targeting_key if targeting_key
180
+
181
+ if evaluation_details
182
+ if evaluation_details.variant
183
+ attributes[RESULT_VARIANT_KEY] = evaluation_details.variant
184
+ else
185
+ attributes[RESULT_VALUE_KEY] = evaluation_details.value
186
+ end
187
+
188
+ if evaluation_details.reason
189
+ attributes[RESULT_REASON_KEY] = evaluation_details.reason.downcase
190
+ end
191
+
192
+ if evaluation_details.error_code
193
+ attributes[ERROR_TYPE_KEY] = evaluation_details.error_code.downcase
194
+ end
195
+
196
+ if evaluation_details.error_message
197
+ attributes[ERROR_MESSAGE_KEY] = evaluation_details.error_message
198
+ end
199
+
200
+ extract_metadata(evaluation_details.flag_metadata, attributes)
201
+ end
202
+
203
+ EvaluationEvent.new(name: EVENT_NAME, attributes: attributes)
204
+ end
205
+
206
+ def extract_metadata(flag_metadata, attributes)
207
+ return unless flag_metadata
208
+
209
+ METADATA_KEY_MAP.each do |metadata_key, otel_key|
210
+ value = flag_metadata[metadata_key]
211
+ attributes[otel_key] = value unless value.nil?
212
+ end
213
+ end
214
+
215
+ private_class_method :extract_metadata
216
+ ```
217
+
218
+ **Step 2: Run the test to verify it passes**
219
+
220
+ Run: `bundle exec rspec spec/open_feature/sdk/telemetry_spec.rb`
221
+ Expected: PASS (1 example, 0 failures)
222
+
223
+ **Step 3: Commit**
224
+
225
+ ```bash
226
+ git add lib/open_feature/sdk/telemetry.rb
227
+ git commit -s -S -m "feat(telemetry): implement create_evaluation_event"
228
+ ```
229
+
230
+ ---
231
+
232
+ ### Task 4: Add tests for variant vs value precedence
233
+
234
+ **Files:**
235
+ - Modify: `spec/open_feature/sdk/telemetry_spec.rb`
236
+
237
+ **Step 1: Add variant/value precedence tests**
238
+
239
+ Add inside the `describe ".create_evaluation_event"` block:
240
+
241
+ ```ruby
242
+ context "variant vs value precedence" do
243
+ it "uses variant when present and omits value" do
244
+ event = described_class.create_evaluation_event(
245
+ hook_context: hook_context,
246
+ evaluation_details: evaluation_details
247
+ )
248
+
249
+ expect(event.attributes).to have_key("feature_flag.result.variant")
250
+ expect(event.attributes).not_to have_key("feature_flag.result.value")
251
+ end
252
+
253
+ it "uses value when variant is nil" do
254
+ resolution = OpenFeature::SDK::Provider::ResolutionDetails.new(
255
+ value: "blue",
256
+ reason: "STATIC"
257
+ )
258
+ details = OpenFeature::SDK::EvaluationDetails.new(
259
+ flag_key: "color-flag",
260
+ resolution_details: resolution
261
+ )
262
+
263
+ event = described_class.create_evaluation_event(
264
+ hook_context: hook_context,
265
+ evaluation_details: details
266
+ )
267
+
268
+ expect(event.attributes["feature_flag.result.value"]).to eq("blue")
269
+ expect(event.attributes).not_to have_key("feature_flag.result.variant")
270
+ end
271
+ end
272
+ ```
273
+
274
+ **Step 2: Run tests**
275
+
276
+ Run: `bundle exec rspec spec/open_feature/sdk/telemetry_spec.rb`
277
+ Expected: PASS (3 examples, 0 failures)
278
+
279
+ **Step 3: Commit**
280
+
281
+ ```bash
282
+ git add spec/open_feature/sdk/telemetry_spec.rb
283
+ git commit -s -S -m "test(telemetry): add variant vs value precedence tests"
284
+ ```
285
+
286
+ ---
287
+
288
+ ### Task 5: Add tests for enum downcasing
289
+
290
+ **Files:**
291
+ - Modify: `spec/open_feature/sdk/telemetry_spec.rb`
292
+
293
+ **Step 1: Add enum downcasing tests**
294
+
295
+ Add inside the `describe ".create_evaluation_event"` block:
296
+
297
+ ```ruby
298
+ context "enum downcasing" do
299
+ it "downcases reason to OTel convention" do
300
+ event = described_class.create_evaluation_event(
301
+ hook_context: hook_context,
302
+ evaluation_details: evaluation_details
303
+ )
304
+
305
+ expect(event.attributes["feature_flag.result.reason"]).to eq("targeting_match")
306
+ end
307
+
308
+ it "downcases error_code to OTel convention" do
309
+ resolution = OpenFeature::SDK::Provider::ResolutionDetails.new(
310
+ value: false,
311
+ reason: "ERROR",
312
+ error_code: "FLAG_NOT_FOUND",
313
+ error_message: "Flag not found"
314
+ )
315
+ details = OpenFeature::SDK::EvaluationDetails.new(
316
+ flag_key: "missing-flag",
317
+ resolution_details: resolution
318
+ )
319
+
320
+ event = described_class.create_evaluation_event(
321
+ hook_context: hook_context,
322
+ evaluation_details: details
323
+ )
324
+
325
+ expect(event.attributes["error.type"]).to eq("flag_not_found")
326
+ expect(event.attributes["feature_flag.result.reason"]).to eq("error")
327
+ end
328
+ end
329
+ ```
330
+
331
+ **Step 2: Run tests**
332
+
333
+ Run: `bundle exec rspec spec/open_feature/sdk/telemetry_spec.rb`
334
+ Expected: PASS (5 examples, 0 failures)
335
+
336
+ **Step 3: Commit**
337
+
338
+ ```bash
339
+ git add spec/open_feature/sdk/telemetry_spec.rb
340
+ git commit -s -S -m "test(telemetry): add enum downcasing tests"
341
+ ```
342
+
343
+ ---
344
+
345
+ ### Task 6: Add tests for error attributes and nil evaluation_details
346
+
347
+ **Files:**
348
+ - Modify: `spec/open_feature/sdk/telemetry_spec.rb`
349
+
350
+ **Step 1: Add error and nil evaluation_details tests**
351
+
352
+ Add inside the `describe ".create_evaluation_event"` block:
353
+
354
+ ```ruby
355
+ context "error attributes" do
356
+ it "includes error attributes only when error occurred" do
357
+ resolution = OpenFeature::SDK::Provider::ResolutionDetails.new(
358
+ value: false,
359
+ reason: "ERROR",
360
+ error_code: "PARSE_ERROR",
361
+ error_message: "Could not parse flag"
362
+ )
363
+ details = OpenFeature::SDK::EvaluationDetails.new(
364
+ flag_key: "bad-flag",
365
+ resolution_details: resolution
366
+ )
367
+
368
+ event = described_class.create_evaluation_event(
369
+ hook_context: hook_context,
370
+ evaluation_details: details
371
+ )
372
+
373
+ expect(event.attributes["error.type"]).to eq("parse_error")
374
+ expect(event.attributes["error.message"]).to eq("Could not parse flag")
375
+ end
376
+
377
+ it "omits error attributes when no error" do
378
+ event = described_class.create_evaluation_event(
379
+ hook_context: hook_context,
380
+ evaluation_details: evaluation_details
381
+ )
382
+
383
+ expect(event.attributes).not_to have_key("error.type")
384
+ expect(event.attributes).not_to have_key("error.message")
385
+ end
386
+ end
387
+
388
+ context "nil evaluation_details" do
389
+ it "returns event with only flag_key and available context" do
390
+ event = described_class.create_evaluation_event(
391
+ hook_context: hook_context,
392
+ evaluation_details: nil
393
+ )
394
+
395
+ expect(event.name).to eq("feature_flag.evaluation")
396
+ expect(event.attributes).to eq(
397
+ "feature_flag.key" => "my-flag",
398
+ "feature_flag.provider.name" => "test-provider",
399
+ "feature_flag.context.id" => "user-123"
400
+ )
401
+ end
402
+ end
403
+ ```
404
+
405
+ **Step 2: Run tests**
406
+
407
+ Run: `bundle exec rspec spec/open_feature/sdk/telemetry_spec.rb`
408
+ Expected: PASS (8 examples, 0 failures)
409
+
410
+ **Step 3: Commit**
411
+
412
+ ```bash
413
+ git add spec/open_feature/sdk/telemetry_spec.rb
414
+ git commit -s -S -m "test(telemetry): add error attributes and nil evaluation_details tests"
415
+ ```
416
+
417
+ ---
418
+
419
+ ### Task 7: Add tests for flag metadata and context ID precedence
420
+
421
+ **Files:**
422
+ - Modify: `spec/open_feature/sdk/telemetry_spec.rb`
423
+
424
+ **Step 1: Add metadata and context ID tests**
425
+
426
+ Add inside the `describe ".create_evaluation_event"` block:
427
+
428
+ ```ruby
429
+ context "flag metadata" do
430
+ it "ignores nil flag_metadata" do
431
+ resolution = OpenFeature::SDK::Provider::ResolutionDetails.new(
432
+ value: true,
433
+ variant: "on"
434
+ )
435
+ details = OpenFeature::SDK::EvaluationDetails.new(
436
+ flag_key: "my-flag",
437
+ resolution_details: resolution
438
+ )
439
+
440
+ event = described_class.create_evaluation_event(
441
+ hook_context: hook_context,
442
+ evaluation_details: details
443
+ )
444
+
445
+ expect(event.attributes).not_to have_key("feature_flag.set.id")
446
+ expect(event.attributes).not_to have_key("feature_flag.version")
447
+ end
448
+
449
+ it "ignores empty flag_metadata" do
450
+ resolution = OpenFeature::SDK::Provider::ResolutionDetails.new(
451
+ value: true,
452
+ variant: "on",
453
+ flag_metadata: {}
454
+ )
455
+ details = OpenFeature::SDK::EvaluationDetails.new(
456
+ flag_key: "my-flag",
457
+ resolution_details: resolution
458
+ )
459
+
460
+ event = described_class.create_evaluation_event(
461
+ hook_context: hook_context,
462
+ evaluation_details: details
463
+ )
464
+
465
+ expect(event.attributes).not_to have_key("feature_flag.set.id")
466
+ expect(event.attributes).not_to have_key("feature_flag.version")
467
+ end
468
+
469
+ it "ignores unknown metadata keys" do
470
+ resolution = OpenFeature::SDK::Provider::ResolutionDetails.new(
471
+ value: true,
472
+ variant: "on",
473
+ flag_metadata: {"customKey" => "custom-value", "anotherKey" => 42}
474
+ )
475
+ details = OpenFeature::SDK::EvaluationDetails.new(
476
+ flag_key: "my-flag",
477
+ resolution_details: resolution
478
+ )
479
+
480
+ event = described_class.create_evaluation_event(
481
+ hook_context: hook_context,
482
+ evaluation_details: details
483
+ )
484
+
485
+ expect(event.attributes).not_to have_key("customKey")
486
+ expect(event.attributes).not_to have_key("anotherKey")
487
+ end
488
+ end
489
+
490
+ context "context ID precedence" do
491
+ it "uses metadata contextId over targeting_key" do
492
+ event = described_class.create_evaluation_event(
493
+ hook_context: hook_context,
494
+ evaluation_details: evaluation_details
495
+ )
496
+
497
+ # flag_metadata has contextId "ctx-456", targeting_key is "user-123"
498
+ expect(event.attributes["feature_flag.context.id"]).to eq("ctx-456")
499
+ end
500
+
501
+ it "falls back to targeting_key when no contextId in metadata" do
502
+ resolution = OpenFeature::SDK::Provider::ResolutionDetails.new(
503
+ value: true,
504
+ variant: "on",
505
+ flag_metadata: {"flagSetId" => "set-1"}
506
+ )
507
+ details = OpenFeature::SDK::EvaluationDetails.new(
508
+ flag_key: "my-flag",
509
+ resolution_details: resolution
510
+ )
511
+
512
+ event = described_class.create_evaluation_event(
513
+ hook_context: hook_context,
514
+ evaluation_details: details
515
+ )
516
+
517
+ expect(event.attributes["feature_flag.context.id"]).to eq("user-123")
518
+ end
519
+
520
+ it "omits context ID when neither targeting_key nor contextId available" do
521
+ bare_context = OpenFeature::SDK::EvaluationContext.new(env: "prod")
522
+ bare_hook_context = OpenFeature::SDK::Hooks::HookContext.new(
523
+ flag_key: "my-flag",
524
+ flag_value_type: :boolean,
525
+ default_value: false,
526
+ evaluation_context: bare_context
527
+ )
528
+ resolution = OpenFeature::SDK::Provider::ResolutionDetails.new(
529
+ value: true,
530
+ variant: "on"
531
+ )
532
+ details = OpenFeature::SDK::EvaluationDetails.new(
533
+ flag_key: "my-flag",
534
+ resolution_details: resolution
535
+ )
536
+
537
+ event = described_class.create_evaluation_event(
538
+ hook_context: bare_hook_context,
539
+ evaluation_details: details
540
+ )
541
+
542
+ expect(event.attributes).not_to have_key("feature_flag.context.id")
543
+ end
544
+ end
545
+ ```
546
+
547
+ **Step 2: Run tests**
548
+
549
+ Run: `bundle exec rspec spec/open_feature/sdk/telemetry_spec.rb`
550
+ Expected: PASS (14 examples, 0 failures)
551
+
552
+ **Step 3: Commit**
553
+
554
+ ```bash
555
+ git add spec/open_feature/sdk/telemetry_spec.rb
556
+ git commit -s -S -m "test(telemetry): add flag metadata and context ID precedence tests"
557
+ ```
558
+
559
+ ---
560
+
561
+ ### Task 8: Run full suite and lint
562
+
563
+ **Step 1: Run full test suite**
564
+
565
+ Run: `bundle exec rspec`
566
+ Expected: All existing tests still pass, plus 14 new telemetry tests.
567
+
568
+ **Step 2: Run linter**
569
+
570
+ Run: `bundle exec standardrb`
571
+ Expected: No offenses. If any, fix with `bundle exec standardrb --fix`.
572
+
573
+ **Step 3: Final commit if lint fixes needed**
574
+
575
+ ```bash
576
+ git add -A
577
+ git commit -s -S -m "style(telemetry): fix standardrb lint issues"
578
+ ```
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OpenFeature
4
+ module SDK
5
+ module Telemetry
6
+ EVENT_NAME = "feature_flag.evaluation"
7
+
8
+ FLAG_KEY = "feature_flag.key"
9
+ CONTEXT_ID_KEY = "feature_flag.context.id"
10
+ ERROR_MESSAGE_KEY = "error.message"
11
+ ERROR_TYPE_KEY = "error.type"
12
+ PROVIDER_NAME_KEY = "feature_flag.provider.name"
13
+ RESULT_REASON_KEY = "feature_flag.result.reason"
14
+ RESULT_VALUE_KEY = "feature_flag.result.value"
15
+ RESULT_VARIANT_KEY = "feature_flag.result.variant"
16
+ FLAG_SET_ID_KEY = "feature_flag.set.id"
17
+ VERSION_KEY = "feature_flag.version"
18
+
19
+ METADATA_KEY_MAP = {
20
+ "contextId" => CONTEXT_ID_KEY,
21
+ "flagSetId" => FLAG_SET_ID_KEY,
22
+ "version" => VERSION_KEY
23
+ }.freeze
24
+
25
+ EvaluationEvent = Struct.new(:name, :attributes, keyword_init: true)
26
+
27
+ module_function
28
+
29
+ def create_evaluation_event(hook_context:, evaluation_details:)
30
+ attributes = {FLAG_KEY => hook_context.flag_key}
31
+
32
+ provider_name = hook_context.provider_metadata&.name
33
+ attributes[PROVIDER_NAME_KEY] = provider_name if provider_name
34
+
35
+ targeting_key = hook_context.evaluation_context&.targeting_key
36
+ # Set from targeting_key; may be overridden by contextId in flag metadata below
37
+ attributes[CONTEXT_ID_KEY] = targeting_key if targeting_key
38
+
39
+ if evaluation_details
40
+ if evaluation_details.variant
41
+ attributes[RESULT_VARIANT_KEY] = evaluation_details.variant
42
+ else
43
+ attributes[RESULT_VALUE_KEY] = evaluation_details.value
44
+ end
45
+
46
+ if evaluation_details.reason
47
+ attributes[RESULT_REASON_KEY] = evaluation_details.reason.downcase
48
+ end
49
+
50
+ if evaluation_details.error_code
51
+ attributes[ERROR_TYPE_KEY] = evaluation_details.error_code.downcase
52
+ end
53
+
54
+ if evaluation_details.error_message
55
+ attributes[ERROR_MESSAGE_KEY] = evaluation_details.error_message
56
+ end
57
+
58
+ extract_metadata(evaluation_details.flag_metadata, attributes)
59
+ end
60
+
61
+ EvaluationEvent.new(name: EVENT_NAME, attributes: attributes.freeze)
62
+ end
63
+
64
+ def extract_metadata(flag_metadata, attributes)
65
+ return unless flag_metadata
66
+
67
+ METADATA_KEY_MAP.each do |metadata_key, otel_key|
68
+ value = flag_metadata[metadata_key]
69
+ attributes[otel_key] = value unless value.nil?
70
+ end
71
+ end
72
+
73
+ private_class_method :extract_metadata
74
+ end
75
+ end
76
+ end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module OpenFeature
4
4
  module SDK
5
- VERSION = "0.6.3"
5
+ VERSION = "0.6.4"
6
6
  end
7
7
  end
@@ -2,6 +2,7 @@
2
2
 
3
3
  require_relative "sdk/version"
4
4
  require_relative "sdk/api"
5
+ require_relative "sdk/telemetry"
5
6
  require_relative "sdk/transaction_context_propagator"
6
7
  require_relative "sdk/thread_local_transaction_context_propagator"
7
8
 
data/renovate.json CHANGED
@@ -5,5 +5,6 @@
5
5
  ":automergeStableNonMajor",
6
6
  "security:minimumReleaseAgeNpm"
7
7
  ],
8
- "semanticCommits": "enabled"
8
+ "semanticCommits": "enabled",
9
+ "platformAutomerge": true
9
10
  }
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.3
4
+ version: 0.6.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - OpenFeature Authors
@@ -160,6 +160,8 @@ files:
160
160
  - README.md
161
161
  - Rakefile
162
162
  - cucumber.yml
163
+ - docs/plans/2026-03-07-telemetry-utility-design.md
164
+ - docs/plans/2026-03-07-telemetry-utility-plan.md
163
165
  - lib/open_feature/sdk.rb
164
166
  - lib/open_feature/sdk/api.rb
165
167
  - lib/open_feature/sdk/client.rb
@@ -187,6 +189,7 @@ files:
187
189
  - lib/open_feature/sdk/provider_initialization_error.rb
188
190
  - lib/open_feature/sdk/provider_state.rb
189
191
  - lib/open_feature/sdk/provider_state_registry.rb
192
+ - lib/open_feature/sdk/telemetry.rb
190
193
  - lib/open_feature/sdk/thread_local_transaction_context_propagator.rb
191
194
  - lib/open_feature/sdk/tracking_event_details.rb
192
195
  - lib/open_feature/sdk/transaction_context_propagator.rb