Skip to content

fix: conform feature_flag span event to the OTEL spec - #54

Merged
kinyoklion merged 4 commits into
mainfrom
fix/otel-spec-conformance-types-set-id
Sep 9, 2026
Merged

fix: conform feature_flag span event to the OTEL spec#54
kinyoklion merged 4 commits into
mainfrom
fix/otel-spec-conformance-types-set-id

Conversation

@Vadman97

@Vadman97 Vadman97 commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Summary

Brings the feature_flag span event into conformance with the OTEL spec (OTEL v1.0.0, ACCEPTED).

Three requirements were unmet:

Req Spec says This hook did
1.2.2.11 feature_flag.result.variationIndex is an int emitted str(...)
1.2.2.10 feature_flag.result.reason.inExperiment is a boolean emitted the string 'true'
1.2.2.9 / 1.2.4 support feature_flag.set.id and a configurable environmentId not implemented

The Go tracing hook (go-server-sdk/ldotel) already emits the specified types and supports set.id, so this also removes a cross-SDK inconsistency.

Why the types matter

Consumers matching on the typed value silently never matched. Our own OTel documentation gives this collector filter:

- 'not ((name == "feature_flag" and attributes["feature_flag.result.reason.inExperiment"] == true) or name == "exception")'

== true does not match the string "true", so that documented example did not work against spans produced by this hook.

Changes

  • variationIndex → int, inExperimentTrue
  • New HookOptions.environment_id; when set, emits feature_flag.set.id (req 1.2.2.9.1.1)
  • Invalid environment_id (non-string or empty) is ignored and logged to the ldclient.otel logger, equivalent to unset (reqs 1.2.4.1, 1.2.4.2)
  • attributes annotated Dict[str, AttributeValue] so mypy accepts the mixed value types
  • 7 new tests, including an explicit type-contract test to stop the strings regressing

Not implemented: req 1.2.2.9.2

Sourcing the environment ID from EvaluationSeriesContext when it is not configured is not implementable today. launchdarkly-server-sdk 9.14.1 exposes no environment ID on either EvaluationSeriesContext (key / context / default_value / method only) or plugin EnvironmentMetadata (sdk / sdk_key / application). Go has seriesContext.EnvironmentID(); Python has no equivalent. Only the config path (1.2.2.9.1) is covered here.

Compatibility

This changes the wire type of two attributes. Anyone filtering on the string forms ("true", "0") in a downstream OTel backend will need to match the typed values instead.

No impact on LaunchDarkly Observability data. Ingest stringifies span event attributes via fmt.Sprintf("%v", v) (backend/clickhouse/trace_row.go attributesToMap) into String columns, so True"true" and 0"0" — byte-identical to the previous output.

Testing

  • make test — 19 passed (12 pre-existing, 7 new)
  • make lintmypy, isort, pycodestyle all clean

Note for reviewers

Spec req 1.2.3.3 looks internally inconsistent and I did not touch it: it says the variation span must carry feature_flag.context.key, but its own prose cross-references 1.2.2.5, which defines feature_flag.context.id. Both this hook and the Go hook set context.id. Separately, the public observability docs state that a flag span event is identified by carrying feature_flag.context.key. Worth reconciling, but that's a spec decision rather than a code fix.

🤖 Generated with Claude Code


Note

Overview
Aligns LaunchDarkly Python tracing feature_flag span events with the OTEL spec and the Go hook behavior.

Typed attributes: feature_flag.result.variationIndex is now an int (not a string), and feature_flag.result.reason.inExperiment is a bool (True, not 'true'). Downstream filters that compare to typed values (e.g. == true) will work; consumers still matching string forms need to update.

Environment / set ID: Adds HookOptions.environment_id to emit feature_flag.set.id when set to a non-empty string; invalid values are ignored. When the SDK exposes environment_id on the evaluation series context, that value is used as a fallback, with the hook option taking precedence.

Tests are updated for the new types and expanded with coverage for set.id resolution, precedence, and a regression guard on attribute types.

Reviewed by Cursor Bugbot for commit b933164. Bugbot is set up for automated code reviews on this repo. Configure here.

The OTEL spec (launchdarkly/sdk-specs specs/OTEL-openteletry-integration)
types `feature_flag.result.variationIndex` as an int (req 1.2.2.11) and
`feature_flag.result.reason.inExperiment` as a boolean (req 1.2.2.10).
This hook was emitting both as strings, so consumers matching on the typed
value did not match. The collector filter example in our own OTel docs
matches `attributes["feature_flag.result.reason.inExperiment"] == true`,
which never matched spans produced by this hook. The Go tracing hook
already emits the specified types.

Also adds the `environment_id` option (req 1.2.4) and the resulting
`feature_flag.set.id` attribute (req 1.2.2.9), neither of which was
implemented here. Invalid values are ignored and logged (req 1.2.4.1,
1.2.4.2).

Req 1.2.2.9.2 -- sourcing the environment ID from EvaluationSeriesContext
when it is not configured -- remains unimplemented, because
launchdarkly-server-sdk does not expose an environment ID on either
EvaluationSeriesContext or plugin EnvironmentMetadata.

Note for LaunchDarkly Observability: stored values are unchanged. The
ingest path stringifies span event attributes with fmt.Sprintf("%v"),
so `True` -> "true" and `0` -> "0", byte-identical to the previous output.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@Vadman97
Vadman97 marked this pull request as ready for review August 11, 2026 21:49
@Vadman97
Vadman97 requested a review from a team as a code owner August 11, 2026 21:49

@kinyoklion kinyoklion left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs the update for reading the environment ID from the stream headers. The ability to configure it is a fallback.

The hook should include the environment ID. If the go SDK is currently only doing this, then it needs updated as well and I can check that.

Example of setting it from the .Net SDK: https://github.com/launchdarkly/dotnet-core/blob/98cf36ab260906284bc5276b8c656c399925d14e/pkgs/telemetry/src/TracingHook.cs#L235

Python has the eventsource embedded, so it should be a very small change really.

Co-Authored-By: rlamb@launchdarkly.com <4955475+kinyoklion@users.noreply.github.com>
@devin-ai-integration

Copy link
Copy Markdown
Contributor

Pushed 66cde76: the hook now reads the environment ID off EvaluationSeriesContext (available since launchdarkly-server-sdk 9.17.0, which sources it from the stream headers), with HookOptions.environment_id as the fallback — config wins when both are present, per spec 1.2.2.9 / the .NET hook.

The read is getattr(series_context, 'environment_id', None), so the minimum SDK version stays at >=9.4.0: on older SDKs the attribute simply isn't there and only the configured value is used. The "Not implemented: req 1.2.2.9.2" section of the description is now stale.

Validated against a real environment (hello-app SDK key), evaluating sample-feature inside a span and inspecting the exported feature_flag event:

SDK environment_id configured feature_flag.set.id
9.17.0 no [REDACTED SECRET] (real env ID from the stream)
9.17.0 configured-env-id configured-env-id
9.16.1 no absent
9.16.1 configured-env-id configured-env-id

Unit tests (24) and make lint pass on both 9.16.1 and 9.17.0.

Written by Devin

devin-ai-integration Bot and others added 2 commits September 9, 2026 20:37
Co-Authored-By: rlamb@launchdarkly.com <4955475+kinyoklion@users.noreply.github.com>

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit b933164. Configure here.

Comment thread ldotel/tracing.py
if isinstance(environment_id, str) and environment_id != '':
return environment_id

return None

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Invalid environment ID is not logged

Low Severity

_valid_environment_id treats a non-string or empty environment_id as unset but never logs. Spec requirements 1.2.4.1 and 1.2.4.2 require those invalid values to be written to the ldclient.otel logger, so misconfiguration stays silent.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit b933164. Configure here.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is fine for now.

@kinyoklion
kinyoklion merged commit 6308ee8 into main Sep 9, 2026
16 checks passed
@kinyoklion
kinyoklion deleted the fix/otel-spec-conformance-types-set-id branch September 9, 2026 20:51
kinyoklion pushed a commit that referenced this pull request Sep 9, 2026
🤖 I have created a release *beep* *boop*
---


##
[1.2.1](1.2.0...1.2.1)
(2026-09-09)


### Bug Fixes

* conform feature_flag span event to the OTEL spec
([#54](#54))
([6308ee8](6308ee8))

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Overview**
> **Release 1.2.1** — bumps the package version from `1.2.0` to `1.2.1`
in `pyproject.toml`, `ldotel/__init__.py`,
`.release-please-manifest.json`, and the provenance docs example in
`PROVENANCE.md`.
> 
> `CHANGELOG.md` records the patch release (2026-09-09) with a **bug
fix** note: `feature_flag` span events now conform to the OpenTelemetry
spec
([#54](#54)).
This PR does not include that implementation change—only the version and
release metadata updates from Release Please.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
4972e58. 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: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants