|
| 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 commit comments