fix: conform feature_flag span event to the OTEL spec - #54
Conversation
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>
kinyoklion
left a comment
There was a problem hiding this comment.
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>
|
Pushed 66cde76: the hook now reads the environment ID off The read is Validated against a real environment (hello-app SDK key), evaluating
Unit tests (24) and |
Co-Authored-By: rlamb@launchdarkly.com <4955475+kinyoklion@users.noreply.github.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
❌ 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.
| if isinstance(environment_id, str) and environment_id != '': | ||
| return environment_id | ||
|
|
||
| return None |
There was a problem hiding this comment.
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)
Reviewed by Cursor Bugbot for commit b933164. Configure here.
🤖 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>


Summary
Brings the
feature_flagspan event into conformance with the OTEL spec (OTELv1.0.0, ACCEPTED).Three requirements were unmet:
feature_flag.result.variationIndexis an intstr(...)feature_flag.result.reason.inExperimentis a boolean'true'feature_flag.set.idand a configurableenvironmentIdThe Go tracing hook (
go-server-sdk/ldotel) already emits the specified types and supportsset.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")'== truedoes not match the string"true", so that documented example did not work against spans produced by this hook.Changes
variationIndex→ int,inExperiment→TrueHookOptions.environment_id; when set, emitsfeature_flag.set.id(req 1.2.2.9.1.1)environment_id(non-string or empty) is ignored and logged to theldclient.otellogger, equivalent to unset (reqs 1.2.4.1, 1.2.4.2)attributesannotatedDict[str, AttributeValue]so mypy accepts the mixed value typesNot implemented: req 1.2.2.9.2
Sourcing the environment ID from
EvaluationSeriesContextwhen it is not configured is not implementable today.launchdarkly-server-sdk9.14.1 exposes no environment ID on eitherEvaluationSeriesContext(key/context/default_value/methodonly) or pluginEnvironmentMetadata(sdk/sdk_key/application). Go hasseriesContext.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.goattributesToMap) intoStringcolumns, soTrue→"true"and0→"0"— byte-identical to the previous output.Testing
make test— 19 passed (12 pre-existing, 7 new)make lint—mypy,isort,pycodestyleall cleanNote 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 definesfeature_flag.context.id. Both this hook and the Go hook setcontext.id. Separately, the public observability docs state that a flag span event is identified by carryingfeature_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_flagspan events with the OTEL spec and the Go hook behavior.Typed attributes:
feature_flag.result.variationIndexis now an int (not a string), andfeature_flag.result.reason.inExperimentis 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_idto emitfeature_flag.set.idwhen set to a non-empty string; invalid values are ignored. When the SDK exposesenvironment_idon 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.idresolution, 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.