feat: Add environment ID support for hooks - #202
Conversation
Co-Authored-By: rlamb@launchdarkly.com <4955475+kinyoklion@users.noreply.github.com>
🤖 Devin AI EngineerI'll be helping with this pull request! Here's what you should know: ✅ I will automatically:
Note: I can only respond to comments from users who have write access to this repository. ⚙️ Control Options:
|
|
@cursor review |
| * | ||
| * @param environmentId the environment ID reported by LaunchDarkly | ||
| */ | ||
| default void setEnvironmentId(String environmentId) {} |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Sounds good — leaving it as is.
There was a problem hiding this comment.
I prefer approaches where it goes through to the store with the flag data and perfer one operation.
There was a problem hiding this comment.
That's the shape I sketched above: the env ID rides the change set into the store, TransactionalDataStore exposes getEnvironmentId() alongside getSelector(), and the data systems read it back from the store. For FDv1 it would go on FullDataSet so init stays one operation.
Happy to rework it that way — just say the word and I'll push it.
There was a problem hiding this comment.
Reworked in 2ab6efb: DataSourceUpdateSink.setEnvironmentId is gone. The env ID now rides the data — FullDataSet carries it for FDv1 (DefaultFeatureRequestor from the poll response, StreamProcessor from the put event's stream headers), FDv2 already had it on the change set — and the stores retain it: InMemoryDataStore/WriteThroughStore/PersistentDataStoreWrapper implement DataStore.getEnvironmentId(), which the data systems read for the hook context. Empty/absent values don't clear a retained ID.
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 2ab6efb. Configure here.
…re init Co-Authored-By: rlamb@launchdarkly.com <4955475+kinyoklion@users.noreply.github.com>

Requirements
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
EvaluationSeriesContextgains anenvironmentIdfield, populated from theX-LD-EnvIDresponse header LaunchDarkly sends on flag delivery responses.FullDataSetcarries it for FDv1, and FDv2 change sets already did.DataStore.getEnvironmentId()(defaultnull) is implemented byInMemoryDataStore,WriteThroughStoreandPersistentDataStoreWrapper; the data systems read it from the store for the hook context.DefaultFeatureRequestor, after the response is confirmed successful), FDv1 streaming (StreamProcessor, from theputevent's stream headers), FDv2 initializers/synchronizers, and the FDv1 fallback adapter used under FDv2.environmentIdand declareshook-environment-id.Backend-only change, so no screenshots or staging preview apply.
Describe alternatives you've considered
A
DataSourceUpdateSink.setEnvironmentIdcall (the previous revision of this PR) — reviewers preferred a single operation that keeps the environment ID next to the data in the store.Additional context
Implementation details
EvaluatorWithHooksreceives a supplier so each evaluation sees the current value:FullDataSetgains a third constructor parameter (environmentId); the existing constructors delegate to it, so this is source and binary compatible for existing callers. Conversions that rebuild aFullDataSet(dependency sorting, persistent store serialization, change-set-to-legacy-init) now preserve the value.How to test
./gradlew test checkstyleMain checkstyleTest javadocinlib/sdk/serverhooks/evaluation/provides the environment IDpasses against released harness v2.39.0 (FDv1, default and polling) and v3.2.0-alpha.6 (FDv2); full suites also pass.Risks / follow-ups
v3.0.0-alpha.6, so CI won't exercise the new test until that pin is bumped.DataStoreimplementations that don't overridegetEnvironmentId()simply report no environment ID.TrackSeriesContextis unaffected.Link to Devin session: https://app.devin.ai/sessions/bfe54128e2804a96bb100e6120e9a3ef
Requested by: @kinyoklion
Note
Overview
Adds
environmentIdtoEvaluationSeriesContextso evaluation hooks can see which LaunchDarkly environment the flag data came from, sourced from theX-LD-EnvIDheader on flag delivery responses.The ID is carried with flag payloads rather than set through a separate sink:
FullDataSetand change sets preserve it through sorting, persistence conversion, and FDv1→FDv2 adaptation. Stores retain it viaDataStore.getEnvironmentId()(in-memory, write-through, and persistent wrapper); empty or missing values do not clear a previously stored ID. Capture happens on successful FDv1 poll and streamputevents, plus existing FDv2 paths;LDClientpassesdataSystem::getEnvironmentIdintoEvaluatorWithHookson each evaluation. Contract tests addhook-environment-idand reportenvironmentIdin hook callbacks.Reviewed by Cursor Bugbot for commit fc78a70. Bugbot is set up for automated code reviews on this repo. Configure here.