Skip to content

feat: Add environment ID support for hooks - #202

Open
kinyoklion wants to merge 1 commit into
mainfrom
devin/1786661026-java-hook-environment-id
Open

feat: Add environment ID support for hooks#202
kinyoklion wants to merge 1 commit into
mainfrom
devin/1786661026-java-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

Companion to the equivalent work in the other SDKs: launchdarkly/python-server-sdk#484, launchdarkly/cpp-sdks#594, launchdarkly/ruby-server-sdk#414.

Describe the solution you've provided

EvaluationSeriesContext gains an environmentId field, populated from the X-LD-EnvID response header LaunchDarkly sends on flag delivery responses.

The value is latched into DataSourceUpdatesImpl and read back per evaluation through a supplier given to EvaluatorWithHooks, so it reflects the current data system state:

new EvaluationSeriesContext(method, featureKey, context, defaultValue, environmentIdSupplier.get())

Capture points:

  • FDv1 polling: DefaultFeatureRequestor records the header after the response is confirmed successful; PollingProcessor reports it to the update sink.
  • FDv1 streaming: StreamProcessor reads it from the message event headers.
  • FDv2: change sets already carried the environment ID; DataSourceUpdatesImpl.apply now latches it, but only once the data has been applied to the store successfully.
  • FDv1 fallback under FDv2: DataSourceSynchronizerAdapter forwards the ID onto the change sets it synthesizes.

Only non-empty values are latched, so a missing/empty header or an error response never clears a previously reported ID, and nothing is exposed before a successful response. The contract test service reports environmentId and declares hook-environment-id.

Describe alternatives you've considered

Storing the ID as data store metadata (as dotnet-core does) — the change set already carries it here, so latching in the update sink keeps the existing architecture and covers FDv1, which has no change set.

Additional context

Verified against the released harnesses: hooks/evaluation/provides the environment ID passes on v2.39.0 (FDv1, default and polling) and v3.2.0-alpha.6 (FDv2). The repo's v3 contract test run is still pinned to v3.0.0-alpha.6, so it won't exercise the new test until that pin is bumped.

Track hooks are not implemented in this SDK, so TrackSeriesContext is unaffected.

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


Note

Overview
Adds environmentId to EvaluationSeriesContext so evaluation hooks can see which LaunchDarkly environment supplied the flag data. The value comes from the X-LD-EnvID header on successful flag delivery responses.

The SDK latches a non-empty environment ID in DataSourceUpdatesImpl (empty/null never clears a prior value). EvaluatorWithHooks reads it per evaluation via dataSystem::getEnvironmentId and passes it into hook beforeEvaluation / afterEvaluation.

Capture paths: FDv1 polling (DefaultFeatureRequestorPollingProcessor), FDv1 streaming (StreamProcessor message headers), FDv2 (apply on change sets after store success), and FDv1-under-FDv2 fallback (DataSourceSynchronizerAdapter on synthesized change sets). Contract tests declare hook-environment-id and forward environmentId in hook callbacks.

Reviewed by Cursor Bugbot for commit cc24d67. 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 devin-ai-integration Bot added the devin-pr Label for PR's created using devin. label Aug 13, 2026
@devin-ai-integration

Copy link
Copy Markdown
Contributor

@cursor review

*
* @param environmentId the environment ID reported by LaunchDarkly
*/
default void setEnvironmentId(String environmentId) {}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

This is an interesting different approach. Because it changes the interface the data sources use instead of changing the interfaces of the sources themselves.

I do see that a downside could be that it makes it a 2 step process instead of a single step.

I also don't like that the environment ID doesn't live alongside the store.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Agreed on both counts — I went this way because FDv1 has no metadata channel at all (DataStore.init(FullDataSet) carries only data + shouldPersist), so the sink was the one place both generations already meet.

The store-adjacent alternative, mirroring how the selector is handled: TransactionalDataStore already receives the whole change set, so it could expose getEnvironmentId() next to getSelector(), InMemoryDataStore retains what applyFullPayload is already handed, and FDv2DataSystem reads it from the store instead of the sink — one step, and it lives with the data. FDv1 would still need a channel; the least invasive is putting it on FullDataSet alongside shouldPersist so init stays single-step, with FDv1DataSystem reading it back off the store as well.

That's a bigger change to the store interfaces than what's here. Want me to rework it that way?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I am going to leave it as is until there are more opinions. This code will be deprecated when we remove FDv1 and the FDv2 path should be cleaner.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Sounds good — leaving it as is.

@kinyoklion
kinyoklion marked this pull request as ready for review August 14, 2026 21:35
@kinyoklion
kinyoklion requested a review from a team as a code owner August 14, 2026 21:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

devin-pr Label for PR's created using devin.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant