Skip to content

feat: Add environment ID support for hooks - #414

Merged
kinyoklion merged 4 commits into
mainfrom
devin/1786659538-ruby-hook-environment-id
Aug 20, 2026
Merged

feat: Add environment ID support for hooks#414
kinyoklion merged 4 commits into
mainfrom
devin/1786659538-ruby-hook-environment-id

Conversation

@kinyoklion

@kinyoklion kinyoklion commented Aug 13, 2026

Copy link
Copy Markdown
Member

Requirements

  • I have added test coverage for new or changed functionality
  • I have followed the repository's pull request submission guidelines
  • I have validated my changes against all supported platform versions

Related issues

Part of the cross-SDK work to expose the LaunchDarkly environment ID on hook contexts, per the hooks and OTEL specs. Equivalent implementations: launchdarkly/dotnet-core#81, launchdarkly/python-server-sdk#484, launchdarkly/cpp-sdks#594.

Describe the solution you've provided

EvaluationSeriesContext gains an optional environment_id, populated by LDClient from the active data system:

Interfaces::Hooks::EvaluationSeriesContext.new(key, context, default, method, @data_system.environment_id)

Both data systems now report it, so the value is only visible once LaunchDarkly has actually answered:

  • FDv1 — X-LD-EnvID is read from the streaming connection response headers (SSE::Client#on_connect) and from polling responses (Requestor#request_all_data_with_headers), then recorded on the data source UpdateSink, which the FDv1 data system exposes.
  • FDv2 — the ID was already parsed into Basis/Update by the polling and streaming data sources but discarded; the FDv2 data system now latches it from a successful initializer basis and from VALID synchronizer updates. This also covers the FDv1 fallback synchronizer, which reports headers the same way.

Error responses, missing headers, and empty header values are ignored and never clear a previously known ID.

The contract test service reports environmentId on the evaluation series context and declares the hook-environment-id capability.

Describe alternatives you've considered

A shared environment-ID holder threaded into each data source was rejected as a side channel; carrying the value with the data/response metadata matches the other SDKs and the existing FDv2 Basis/Update shape.

Additional context

  • No ld-eventsource change is needed: SSE::Client#on_connect already yields the successful response headers on every connection and reconnection.
  • hooks/evaluation/provides the environment ID passes against the released v2.39.0 harness (default/streaming and polling modes). The repo's v3 contract-test run is pinned to v3.0.0-alpha.6, which predates that test; bumping the pin can be a separate ci: change.
  • ruby-server-sdk-otel's tracing hook only uses the configured environment ID today; adding the series-context fallback is a follow-up in that repo.

Link to Devin session: https://app.devin.ai/sessions/bfe54128e2804a96bb100e6120e9a3ef
Requested by: @kinyoklion


Note

Overview
Evaluation hooks now get an optional environment_id on EvaluationSeriesContext, filled from the active data system after LaunchDarkly reports it.

FDv1 reads X-LD-EnvID from streaming connect headers and polling responses (request_all_data_with_headers) and stores it on the data-source update sink. FDv2 latches the ID from a successful initializer Basis and from VALID synchronizer updates (it was already on those types but unused). Missing, empty, or error-path values are ignored and never clear a known ID.

Contract tests declare hook-environment-id and include environmentId in hook callbacks. CI v3 contract tests now pin to v3 instead of v3.0.0-alpha.6.

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

Co-Authored-By: rlamb@launchdarkly.com <4955475+kinyoklion@users.noreply.github.com>
@kinyoklion kinyoklion self-assigned this Aug 13, 2026
@devin-ai-integration

Copy link
Copy Markdown
Contributor

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment, CI, and merge conflict monitoring

@devin-ai-integration

Copy link
Copy Markdown
Contributor

@cursor review

Co-Authored-By: rlamb@launchdarkly.com <4955475+kinyoklion@users.noreply.github.com>
@kinyoklion
kinyoklion marked this pull request as ready for review August 18, 2026 21:10
@kinyoklion
kinyoklion requested a review from a team as a code owner August 18, 2026 21:10
kinyoklion added a commit that referenced this pull request Aug 20, 2026
**Requirements**

- [x] I have added test coverage for new or changed functionality
- [x] I have followed the repository's [pull request submission
guidelines](../blob/main/CONTRIBUTING.md#submitting-pull-requests)
- [ ] I have validated my changes against all supported platform
versions

**Related issues**

Surfaced by contract test harness v3.2.0-alpha.6 (the pin bump in #414).
These 6 subtests failed against the newer harness:

- `events/context properties/single-kind, allAttributesPrivate,
slash-prefixed attribute name` (debug, identify, index-from-evaluation,
index-from-custom-event)
- `events/feature events/single-kind anonymous context redacts all
attributes/type: any`
- `events/feature events/multi-kind with anonymous context redacts
attributes appropriately/type: any`

**Describe the solution you've provided**

`_meta.redactedAttributes` in event payloads is a list of attribute
*references*, not raw attribute names. When a context has an attribute
whose name begins with `/` (e.g. `/ssn`), the SDK emitted the raw name,
which a consumer parses as a path expression pointing at a nested
property rather than the top-level attribute:

```
expected: "/~1ssn"
actual:   "/ssn"
```

`ContextFilter#check_whole_attribute_private` now converts the attribute
name to a reference with `Reference.create_literal` before adding it to
the redacted list, which escapes `/` and `~` for slash-prefixed names
and leaves all other names unchanged (`name` stays `name`, `a/b~c` stays
`a/b~c` since it is already a literal reference). This covers both the
`allAttributesPrivate`/configured-private paths and the
anonymous-context redaction path, which is why one change fixes all 6
subtests.

Verified locally: contract test service against released harness
v3.2.0-alpha.6, full suite — 4723 total, 14 skipped, all ran passed.

**Describe alternatives you've considered**

Escaping only in the `all_attributes_private` branch — rejected, the
same escaping is required wherever a whole attribute is redacted.

**Additional context**

Nested redactions (`redact_json_value`) already reported
`Reference#raw_path`, so they were unaffected.


Link to Devin session:
https://app.devin.ai/sessions/316afaccd2604f8d802a72c9080f6921
Requested by: @kinyoklion

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Overview**
> Fixes **`_meta.redactedAttributes`** in event payloads so entries are
**escaped attribute references**, not raw names. Attributes whose names
start with `/` (e.g. `/ssn`) were emitted as `/ssn`, which consumers
parse as a nested path instead of a top-level literal.
> 
> **`ContextFilter`** now passes **`Reference.create_literal`** into
**`check_whole_attribute_private`** for `name` and custom attributes,
and records **`attribute.raw_path.to_sym`** in the redacted list. That
aligns whole-attribute redaction with **`redact_json_value`**, which
already used **`raw_path`**.
> 
> The same path covers **all-attributes-private**, **configured private
attributes**, and **anonymous context** redaction. Tests assert escaped
values such as `"/~1ssn"` for slash-prefixed names.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
5814e95. 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: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@kinyoklion
kinyoklion merged commit 257e0af into main Aug 20, 2026
10 checks passed
@kinyoklion
kinyoklion deleted the devin/1786659538-ruby-hook-environment-id branch August 20, 2026 21:24
kinyoklion pushed a commit that referenced this pull request Aug 20, 2026
🤖 I have created a release *beep* *boop*
---


##
[8.15.0](8.14.0...8.15.0)
(2026-08-20)


### Features

* Add Config#with_wrapper_information
([#421](#421))
([2de2b75](2de2b75))
* Add environment ID support for hooks
([#414](#414))
([257e0af](257e0af))


### Bug Fixes

* Escape attribute names reported in redactedAttributes
([#415](#415))
([447296f](447296f))
* Send the FDv2 polling selector as the "basis" query parameter
([#418](#418))
([1084931](1084931))
* Stop per-context private attributes from accumulating between contexts
([#416](#416))
([2fd26ea](2fd26ea))

---
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 Please bump from **8.14.0** to **8.15.0**. Updates
`LaunchDarkly::VERSION`, the release-please manifest, provenance example
version, and changelog.
> 
> The 8.15.0 notes cover `Config#with_wrapper_information`, environment
ID support for hooks, redacted attribute escaping, FDv2 polling `basis`
query param, and a fix so per-context private attributes do not
accumulate.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
c2fb4f4. 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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants