Commit 587e2e4
fix(ruby): omit nil feature_flag.context.id for invalid contexts (#641)
## What
When a flag is evaluated with an **invalid context**, the observability
plugin's evaluation hook sets `feature_flag.context.id` to `nil`.
OpenTelemetry rejects nil attribute values, so it logs an error on every
such evaluation — twice (once on the `evaluation` span, once on the
`feature_flag` event):
```
OpenTelemetry error: invalid span attribute value type NilClass for key 'feature_flag.context.id' on span 'evaluation'
OpenTelemetry error: invalid event attribute value type NilClass for key 'feature_flag.context.id' on span 'evaluation'
```
This guards on the key itself via
`series_context.context&.fully_qualified_key` and `.compact`s the
span/event attribute hashes so nil values are dropped centrally — before
they reach OTel — for this and any future optional attribute.
## Root cause
The old guard only checked the context object was non-nil:
```ruby
context = series_context.context
if context
attrs[FEATURE_FLAG_CONTEXT_ID] = context.fully_qualified_key
end
```
But an invalid context (e.g. an empty/missing `key` or a non-string
`kind`) is a *present* `LDContext` object whose `fully_qualified_key` is
`nil` (`create_invalid_context` → `new(nil, nil, nil, …)` in the server
SDK). So we handed OTel a nil value. The fix omits the attribute
entirely when the key is unavailable.
## Impact
Cosmetic — telemetry still exports and lands normally (routing is by the
`launchdarkly.project_id` resource attribute, not this per-span
attribute). But it's noisy in customer logs, and the nil is also a
signal that those specific evaluations are running against an invalid
context (and therefore falling back to the flag's default value).
## Reported by
Surfaced by a customer integrating the Ruby plugin on Rails after their
upgrade — last remaining log noise once auto-instrumentation was
working.
## Test plan
- Adds a regression test that captures the OTel error channel and
reproduces the exact log lines without the fix.
- `bundle exec rake test` → 115 runs, 338 assertions, 0 failures.
## Follow-up (not in this PR)
`launchdarkly-server-sdk-otel`'s `tracing_hook.rb` has the same
nil-context pattern
(`evaluation_series_context.context.fully_qualified_key` with no guard).
Separate gem; worth a matching fix if/when that surfaces.
<!-- CURSOR_SUMMARY -->
---
> [!NOTE]
> **Low Risk**
> Telemetry-only change that omits optional span/event attributes; no
evaluation, auth, or export routing behavior changes.
>
> **Overview**
> Stops OpenTelemetry from logging **invalid attribute value type
NilClass** on every flag evaluation when the context is invalid but
still a non-nil `LDContext` (e.g. non-string `kind`), where
`fully_qualified_key` is nil.
>
> The evaluation hook no longer gates on `context` alone; it resolves
the id via **`context_id_for`**
(`series_context.context&.fully_qualified_key`) and **`.compact`s** span
and `feature_flag` event attribute hashes in
**`build_before_attributes`** and **`add_feature_flag_event`** so nil
optional fields are omitted before they reach OTel.
>
> Adds a regression test that captures the OTel logger and asserts
`feature_flag.context.id` is absent on the span and event for invalid
contexts.
>
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
56253e5. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
Co-authored-by: Claude <noreply@anthropic.com>1 parent c2ba28b commit 587e2e4
2 files changed
Lines changed: 61 additions & 10 deletions
Lines changed: 20 additions & 10 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
91 | 91 | | |
92 | 92 | | |
93 | 93 | | |
94 | | - | |
95 | | - | |
96 | | - | |
97 | | - | |
| 94 | + | |
98 | 95 | | |
99 | | - | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
100 | 111 | | |
101 | 112 | | |
102 | 113 | | |
| |||
127 | 138 | | |
128 | 139 | | |
129 | 140 | | |
130 | | - | |
131 | | - | |
132 | | - | |
133 | | - | |
| 141 | + | |
134 | 142 | | |
135 | 143 | | |
136 | 144 | | |
| |||
146 | 154 | | |
147 | 155 | | |
148 | 156 | | |
149 | | - | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
150 | 160 | | |
151 | 161 | | |
152 | 162 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
192 | 192 | | |
193 | 193 | | |
194 | 194 | | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
| 213 | + | |
| 214 | + | |
| 215 | + | |
| 216 | + | |
| 217 | + | |
| 218 | + | |
| 219 | + | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
| 225 | + | |
| 226 | + | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
| 230 | + | |
| 231 | + | |
| 232 | + | |
| 233 | + | |
| 234 | + | |
| 235 | + | |
195 | 236 | | |
196 | 237 | | |
197 | 238 | | |
| |||
0 commit comments