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 +4 -4
- data/.release-please-manifest.json +1 -1
- data/CHANGELOG.md +7 -0
- data/Gemfile.lock +1 -1
- data/README.md +2 -2
- data/docs/plans/2026-03-07-telemetry-utility-design.md +98 -0
- data/docs/plans/2026-03-07-telemetry-utility-plan.md +578 -0
- data/lib/open_feature/sdk/telemetry.rb +76 -0
- data/lib/open_feature/sdk/version.rb +1 -1
- data/lib/open_feature/sdk.rb +1 -0
- data/renovate.json +2 -1
- metadata +4 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4aa61c48f017d3311bfce9022a5e08d023ee1e2c09eb8799ff0e0a8424cf9fe3
|
|
4
|
+
data.tar.gz: 8c559636076a0ec01911e9057d57cde4e430a7bc4b2543835dcd90527e0f0c0e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 0f683b0d315b225259e540fb9f7d86f60a7e334c1501b7f05d45d677d1e7ee6cfb951b463514c05d9c64b5b413935d1bdf95cd684aa5f05136ed87a6cf2ba122
|
|
7
|
+
data.tar.gz: 618dcac7749c461bb77b92f7f7b68e2071b4b71825d05330bb18a08df3a929019ef0fa8d12d79b117fdd3fce431d48f5e15d2f90aa884fa4f28b1226229f9db0
|
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
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.
|
|
21
|
-
<img alt="Release" src="https://img.shields.io/static/v1?label=release&message=v0.6.
|
|
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
|
data/lib/open_feature/sdk.rb
CHANGED
data/renovate.json
CHANGED
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.
|
|
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
|