diff --git a/contract-tests/server-contract-tests/src/main.cpp b/contract-tests/server-contract-tests/src/main.cpp index 2422623a3..57520a2a5 100644 --- a/contract-tests/server-contract-tests/src/main.cpp +++ b/contract-tests/server-contract-tests/src/main.cpp @@ -50,6 +50,7 @@ int main(int argc, char* argv[]) { srv.add_capability("client-prereq-events"); srv.add_capability("evaluation-hooks"); srv.add_capability("track-hooks"); + srv.add_capability("hook-environment-id"); srv.add_capability("wrapper"); srv.add_capability("instance-id"); srv.add_capability("fdv1-fallback"); diff --git a/libs/server-sdk/src/client_impl.cpp b/libs/server-sdk/src/client_impl.cpp index ba04dda7a..cec42d573 100644 --- a/libs/server-sdk/src/client_impl.cpp +++ b/libs/server-sdk/src/client_impl.cpp @@ -378,8 +378,9 @@ void ClientImpl::TrackInternal(Context const& ctx, // In this SDK the data is type-safe, and will be enqueued, so it makes // minimal functional difference. if (!config_.Hooks().empty()) { - hooks::TrackSeriesContext series_context( - ctx, event_name, metric_value, data, hook_context, std::nullopt); + hooks::TrackSeriesContext series_context(ctx, event_name, metric_value, + data, hook_context, + data_system_->EnvironmentId()); hooks::ExecuteAfterTrack(config_.Hooks(), series_context, logger_); } @@ -487,7 +488,7 @@ EvaluationDetail ClientImpl::VariationInternal( if (!config_.Hooks().empty()) { hooks::EvaluationSeriesContext series_context( key, context, default_value, method_name, hook_context, - std::nullopt); + data_system_->EnvironmentId()); // Executor only created if there are hooks. executor.emplace(config_.Hooks(), logger_); executor->BeforeEvaluation(series_context); @@ -501,7 +502,7 @@ EvaluationDetail ClientImpl::VariationInternal( if (executor) { hooks::EvaluationSeriesContext series_context( key, context, default_value, method_name, hook_context, - std::nullopt); + data_system_->EnvironmentId()); executor->AfterEvaluation(series_context, detail); } @@ -523,7 +524,7 @@ EvaluationDetail ClientImpl::VariationInternal( if (executor) { hooks::EvaluationSeriesContext series_context( key, context, default_value, method_name, hook_context, - std::nullopt); + data_system_->EnvironmentId()); executor->AfterEvaluation(series_context, detail); } @@ -539,7 +540,7 @@ EvaluationDetail ClientImpl::VariationInternal( if (executor) { hooks::EvaluationSeriesContext series_context( key, context, default_value, method_name, hook_context, - std::nullopt); + data_system_->EnvironmentId()); executor->AfterEvaluation(series_context, detail); } diff --git a/libs/server-sdk/src/data_components/change_notifier/change_notifier.cpp b/libs/server-sdk/src/data_components/change_notifier/change_notifier.cpp index 76daefe10..511b0fab3 100644 --- a/libs/server-sdk/src/data_components/change_notifier/change_notifier.cpp +++ b/libs/server-sdk/src/data_components/change_notifier/change_notifier.cpp @@ -143,6 +143,10 @@ void ChangeNotifier::Apply( } } +void ChangeNotifier::SetEnvironmentId(std::string environment_id) { + sink_.SetEnvironmentId(std::move(environment_id)); +} + bool ChangeNotifier::HasListeners() const { std::lock_guard lock{signal_mutex_}; return !signals_.empty(); diff --git a/libs/server-sdk/src/data_components/change_notifier/change_notifier.hpp b/libs/server-sdk/src/data_components/change_notifier/change_notifier.hpp index ed0ca1c64..faf491cbd 100644 --- a/libs/server-sdk/src/data_components/change_notifier/change_notifier.hpp +++ b/libs/server-sdk/src/data_components/change_notifier/change_notifier.hpp @@ -38,6 +38,7 @@ class ChangeNotifier final : public data_interfaces::ITransactionalDestination, data_model::SegmentDescriptor segment) override; void Apply(data_model::ChangeSet change_set) override; + void SetEnvironmentId(std::string environment_id) override; [[nodiscard]] std::string const& Identity() const override; diff --git a/libs/server-sdk/src/data_components/memory_store/memory_store.cpp b/libs/server-sdk/src/data_components/memory_store/memory_store.cpp index 2eee8ff03..23385c309 100644 --- a/libs/server-sdk/src/data_components/memory_store/memory_store.cpp +++ b/libs/server-sdk/src/data_components/memory_store/memory_store.cpp @@ -74,6 +74,19 @@ void MemoryStore::Upsert(std::string const& key, std::make_shared(std::move(segment)); } +void MemoryStore::SetEnvironmentId(std::string environment_id) { + std::lock_guard lock{data_mutex_}; + environment_id_ = std::move(environment_id); +} + +std::optional MemoryStore::EnvironmentId() const { + std::lock_guard lock{data_mutex_}; + if (!initialized_) { + return std::nullopt; + } + return environment_id_; +} + bool MemoryStore::RemoveFlag(std::string const& key) { std::lock_guard lock{data_mutex_}; return flags_.erase(key) == 1; diff --git a/libs/server-sdk/src/data_components/memory_store/memory_store.hpp b/libs/server-sdk/src/data_components/memory_store/memory_store.hpp index 3dc73272c..be5e01c51 100644 --- a/libs/server-sdk/src/data_components/memory_store/memory_store.hpp +++ b/libs/server-sdk/src/data_components/memory_store/memory_store.hpp @@ -8,6 +8,7 @@ #include #include +#include #include #include @@ -43,6 +44,14 @@ class MemoryStore final : public data_interfaces::IStore, void Upsert(std::string const& key, data_model::SegmentDescriptor segment) override; + void SetEnvironmentId(std::string environment_id) override; + + /** + * @return The environment ID reported by LaunchDarkly, if any has been + * received. + */ + [[nodiscard]] std::optional EnvironmentId() const; + bool RemoveFlag(std::string const& key); bool RemoveSegment(std::string const& key); @@ -66,6 +75,7 @@ class MemoryStore final : public data_interfaces::IStore, std::shared_ptr> segments_; bool initialized_ = false; + std::optional environment_id_; mutable std::mutex data_mutex_; }; diff --git a/libs/server-sdk/src/data_components/serialization_adapters/json_destination.cpp b/libs/server-sdk/src/data_components/serialization_adapters/json_destination.cpp index b85f2f978..ac121bf71 100644 --- a/libs/server-sdk/src/data_components/serialization_adapters/json_destination.cpp +++ b/libs/server-sdk/src/data_components/serialization_adapters/json_destination.cpp @@ -111,6 +111,8 @@ void JsonDestination::Upsert(std::string const& key, dest_.Upsert(Kinds::Segment, key, Serialize(key, segment))); } +void JsonDestination::SetEnvironmentId(std::string) {} + void JsonDestination::LogUpsertResult( std::string const& key, std::string const& data_type, diff --git a/libs/server-sdk/src/data_components/serialization_adapters/json_destination.hpp b/libs/server-sdk/src/data_components/serialization_adapters/json_destination.hpp index 77f4e6a01..fff0c7756 100644 --- a/libs/server-sdk/src/data_components/serialization_adapters/json_destination.hpp +++ b/libs/server-sdk/src/data_components/serialization_adapters/json_destination.hpp @@ -70,6 +70,11 @@ class JsonDestination final : public data_interfaces::IDestination { void Upsert(std::string const& key, data_model::SegmentDescriptor segment) override; + /** + * @brief No-op; serialized destinations do not store the environment ID. + */ + void SetEnvironmentId(std::string environment_id) override; + /** * @return Identity of this destination. Used in logs. */ diff --git a/libs/server-sdk/src/data_interfaces/destination/idestination.hpp b/libs/server-sdk/src/data_interfaces/destination/idestination.hpp index 342acf49c..0bed95354 100644 --- a/libs/server-sdk/src/data_interfaces/destination/idestination.hpp +++ b/libs/server-sdk/src/data_interfaces/destination/idestination.hpp @@ -35,6 +35,13 @@ class IDestination { virtual void Upsert(std::string const& key, data_model::SegmentDescriptor segment) = 0; + /** + * \brief Record the environment ID that LaunchDarkly reported alongside + * the data. Destinations which do not track it ignore the value. + * \param environment_id The environment ID. + */ + virtual void SetEnvironmentId(std::string environment_id) = 0; + /** * \return Identity of the destination. Used in logs. */ diff --git a/libs/server-sdk/src/data_interfaces/source/fdv2_source_result.hpp b/libs/server-sdk/src/data_interfaces/source/fdv2_source_result.hpp index 53a0cd442..12cb39da7 100644 --- a/libs/server-sdk/src/data_interfaces/source/fdv2_source_result.hpp +++ b/libs/server-sdk/src/data_interfaces/source/fdv2_source_result.hpp @@ -98,6 +98,12 @@ struct FDv2SourceResult { * Set if the underlying transport observed an FDv1 fallback directive. */ std::optional fdv1_fallback; + + /** + * Set if the underlying transport reported the environment ID (e.g. an + * X-LD-EnvID response header). + */ + std::optional environment_id; }; } // namespace launchdarkly::server_side::data_interfaces diff --git a/libs/server-sdk/src/data_interfaces/system/idata_system.hpp b/libs/server-sdk/src/data_interfaces/system/idata_system.hpp index 0edf778db..f7aaba819 100644 --- a/libs/server-sdk/src/data_interfaces/system/idata_system.hpp +++ b/libs/server-sdk/src/data_interfaces/system/idata_system.hpp @@ -2,6 +2,9 @@ #include "../store/istore.hpp" +#include +#include + namespace launchdarkly::server_side::data_interfaces { /** @@ -21,6 +24,12 @@ class IDataSystem : public IStore { */ virtual void Initialize() = 0; + /** + * @return The environment ID reported by LaunchDarkly alongside the data, + * if the system has received one. + */ + [[nodiscard]] virtual std::optional EnvironmentId() const = 0; + virtual ~IDataSystem() override = default; IDataSystem(IDataSystem const& item) = delete; IDataSystem(IDataSystem&& item) = delete; diff --git a/libs/server-sdk/src/data_systems/background_sync/background_sync_system.cpp b/libs/server-sdk/src/data_systems/background_sync/background_sync_system.cpp index 1a6de5c0a..7a1c7d7e9 100644 --- a/libs/server-sdk/src/data_systems/background_sync/background_sync_system.cpp +++ b/libs/server-sdk/src/data_systems/background_sync/background_sync_system.cpp @@ -47,6 +47,10 @@ std::string const& BackgroundSync::Identity() const { return id; } +std::optional BackgroundSync::EnvironmentId() const { + return store_.EnvironmentId(); +} + std::shared_ptr BackgroundSync::GetFlag( std::string const& key) const { return store_.GetFlag(key); diff --git a/libs/server-sdk/src/data_systems/background_sync/background_sync_system.hpp b/libs/server-sdk/src/data_systems/background_sync/background_sync_system.hpp index a302b7d0d..620eb3c48 100644 --- a/libs/server-sdk/src/data_systems/background_sync/background_sync_system.hpp +++ b/libs/server-sdk/src/data_systems/background_sync/background_sync_system.hpp @@ -55,6 +55,8 @@ class BackgroundSync final : public data_interfaces::IDataSystem { bool Initialized() const override; + std::optional EnvironmentId() const override; + private: data_components::MemoryStore store_; data_components::ChangeNotifier change_notifier_; diff --git a/libs/server-sdk/src/data_systems/background_sync/sources/polling/polling_data_source.cpp b/libs/server-sdk/src/data_systems/background_sync/sources/polling/polling_data_source.cpp index abb2a5478..cdff2b6b1 100644 --- a/libs/server-sdk/src/data_systems/background_sync/sources/polling/polling_data_source.cpp +++ b/libs/server-sdk/src/data_systems/background_sync/sources/polling/polling_data_source.cpp @@ -3,8 +3,8 @@ #include #include -#include #include +#include #include #include @@ -102,6 +102,13 @@ void PollingDataSource::DoPoll() { } void PollingDataSource::HandlePollResult(network::HttpResult const& res) { + if (!res.IsError() && (res.Status() == 200 || res.Status() == 304)) { + if (auto const it = res.Headers().find("X-LD-EnvID"); + it != res.Headers().end() && !it->second.empty()) { + sink_->SetEnvironmentId(it->second); + } + } + auto header_etag = res.Headers().find("etag"); bool has_etag = header_etag != res.Headers().end(); diff --git a/libs/server-sdk/src/data_systems/background_sync/sources/streaming/streaming_data_source.cpp b/libs/server-sdk/src/data_systems/background_sync/sources/streaming/streaming_data_source.cpp index 5f7a27a7b..8da169959 100644 --- a/libs/server-sdk/src/data_systems/background_sync/sources/streaming/streaming_data_source.cpp +++ b/libs/server-sdk/src/data_systems/background_sync/sources/streaming/streaming_data_source.cpp @@ -47,6 +47,7 @@ void StreamingDataSource::StartAsync( data_model::SDKDataSet const* bootstrap_data) { boost::ignore_unused(bootstrap_data); + sink_ = dest; event_handler_.emplace(*dest, logger_, status_manager_); status_manager_.SetState(DataSourceStatus::DataSourceState::kInitializing); @@ -125,16 +126,31 @@ void StreamingDataSource::StartAsync( auto weak_self = weak_from_this(); + client_builder.on_response( + [weak_self](boost::beast::http::response_header<> const& headers) { + auto self = weak_self.lock(); + if (!self || headers.result_int() != 200) { + return; + } + if (auto const it = headers.find("X-LD-EnvID"); + it != headers.end() && !it->value().empty()) { + self->sink_->SetEnvironmentId( + std::string(it->value().data(), it->value().size())); + } + }); + client_builder.receiver([weak_self](launchdarkly::sse::Event const& event) { if (auto self = weak_self.lock()) { auto status = self->event_handler_->HandleMessage(event.type(), event.data()); - if (status == DataSourceEventHandler::MessageStatus::kInvalidMessage) { + if (status == + DataSourceEventHandler::MessageStatus::kInvalidMessage) { // Invalid data received - restart the connection with backoff // to get a fresh stream. The backoff mechanism prevents rapid // reconnection attempts. LD_LOG(self->logger_, LogLevel::kWarn) - << "Received invalid data from stream, restarting connection"; + << "Received invalid data from stream, restarting " + "connection"; if (self->client_) { self->client_->async_restart("invalid data in stream"); } diff --git a/libs/server-sdk/src/data_systems/background_sync/sources/streaming/streaming_data_source.hpp b/libs/server-sdk/src/data_systems/background_sync/sources/streaming/streaming_data_source.hpp index f606686c1..e51509449 100644 --- a/libs/server-sdk/src/data_systems/background_sync/sources/streaming/streaming_data_source.hpp +++ b/libs/server-sdk/src/data_systems/background_sync/sources/streaming/streaming_data_source.hpp @@ -46,6 +46,9 @@ class StreamingDataSource final config::built::BackgroundSyncConfig::StreamingConfig streaming_config_; + // Destination for all data obtained via streaming. Set by StartAsync. + data_interfaces::IDestination* sink_ = nullptr; + std::shared_ptr client_; }; } // namespace launchdarkly::server_side::data_systems diff --git a/libs/server-sdk/src/data_systems/fdv2/fdv1_adapter_synchronizer.cpp b/libs/server-sdk/src/data_systems/fdv2/fdv1_adapter_synchronizer.cpp index 67bb98766..260e6f60b 100644 --- a/libs/server-sdk/src/data_systems/fdv2/fdv1_adapter_synchronizer.cpp +++ b/libs/server-sdk/src/data_systems/fdv2/fdv1_adapter_synchronizer.cpp @@ -44,6 +44,7 @@ void FDv1AdapterSynchronizer::State::Notify(FDv2SourceResult result) { if (closed_future_.IsFinished()) { return; } + result.environment_id = environment_id_; if (pending_promise_) { promise = std::move(pending_promise_); pending_promise_.reset(); @@ -57,6 +58,12 @@ void FDv1AdapterSynchronizer::State::Notify(FDv2SourceResult result) { promise->Resolve(std::move(result)); } +void FDv1AdapterSynchronizer::State::SetEnvironmentId( + std::string environment_id) { + std::lock_guard lock(mutex_); + environment_id_ = std::move(environment_id); +} + // ----- ConvertingDestination ----- FDv1AdapterSynchronizer::ConvertingDestination::ConvertingDestination( @@ -113,6 +120,13 @@ void FDv1AdapterSynchronizer::ConvertingDestination::Upsert( data_model::Selector{}}}}); } +void FDv1AdapterSynchronizer::ConvertingDestination::SetEnvironmentId( + std::string environment_id) { + if (auto state = state_.lock()) { + state->SetEnvironmentId(std::move(environment_id)); + } +} + std::string const& FDv1AdapterSynchronizer::ConvertingDestination::Identity() const { static std::string const identity = "FDv1 adapter destination"; diff --git a/libs/server-sdk/src/data_systems/fdv2/fdv1_adapter_synchronizer.hpp b/libs/server-sdk/src/data_systems/fdv2/fdv1_adapter_synchronizer.hpp index a382ff90a..2ca25e1c2 100644 --- a/libs/server-sdk/src/data_systems/fdv2/fdv1_adapter_synchronizer.hpp +++ b/libs/server-sdk/src/data_systems/fdv2/fdv1_adapter_synchronizer.hpp @@ -73,6 +73,9 @@ class FDv1AdapterSynchronizer final void Notify(data_interfaces::FDv2SourceResult result); + // Stamped onto subsequently notified results. + void SetEnvironmentId(std::string environment_id); + private: // Finished once the owning FDv1AdapterSynchronizer's close_promise_ // is resolved. Read in Notify to drop late results. @@ -83,6 +86,7 @@ class FDv1AdapterSynchronizer final std::optional> pending_promise_; std::deque result_queue_; + std::optional environment_id_; }; /** @@ -97,6 +101,7 @@ class FDv1AdapterSynchronizer final data_model::FlagDescriptor flag) override; void Upsert(std::string const& key, data_model::SegmentDescriptor segment) override; + void SetEnvironmentId(std::string environment_id) override; [[nodiscard]] std::string const& Identity() const override; private: diff --git a/libs/server-sdk/src/data_systems/fdv2/fdv2_data_system.cpp b/libs/server-sdk/src/data_systems/fdv2/fdv2_data_system.cpp index 354fb8850..f28465537 100644 --- a/libs/server-sdk/src/data_systems/fdv2/fdv2_data_system.cpp +++ b/libs/server-sdk/src/data_systems/fdv2/fdv2_data_system.cpp @@ -147,6 +147,10 @@ void FDv2DataSystem::OnInitializerResult( data_interfaces::FDv2SourceResult result) { using Result = data_interfaces::FDv2SourceResult; + if (result.environment_id) { + change_notifier_.SetEnvironmentId(*result.environment_id); + } + bool got_basis = false; bool got_shutdown = false; bool disconnected = false; @@ -350,6 +354,10 @@ void FDv2DataSystem::OnSynchronizerResult( } } + if (result.environment_id) { + change_notifier_.SetEnvironmentId(*result.environment_id); + } + bool got_shutdown = false; bool advance = false; bool disconnected = false; @@ -487,4 +495,8 @@ bool FDv2DataSystem::Initialized() const { return store_.Initialized(); } +std::optional FDv2DataSystem::EnvironmentId() const { + return store_.EnvironmentId(); +} + } // namespace launchdarkly::server_side::data_systems diff --git a/libs/server-sdk/src/data_systems/fdv2/fdv2_data_system.hpp b/libs/server-sdk/src/data_systems/fdv2/fdv2_data_system.hpp index 3c7147ec1..f97259cdb 100644 --- a/libs/server-sdk/src/data_systems/fdv2/fdv2_data_system.hpp +++ b/libs/server-sdk/src/data_systems/fdv2/fdv2_data_system.hpp @@ -229,6 +229,12 @@ class FDv2DataSystem final : public data_interfaces::IDataSystem { */ bool Initialized() const override; + /** + * Returns the environment ID reported by LaunchDarkly alongside the data, + * if data has been received and the transport reported one. + */ + std::optional EnvironmentId() const override; + private: /** * Signals the orchestration loop to stop and closes any active source. diff --git a/libs/server-sdk/src/data_systems/fdv2/fdv2_polling_impl.cpp b/libs/server-sdk/src/data_systems/fdv2/fdv2_polling_impl.cpp index 0b668851b..f972eb492 100644 --- a/libs/server-sdk/src/data_systems/fdv2/fdv2_polling_impl.cpp +++ b/libs/server-sdk/src/data_systems/fdv2/fdv2_polling_impl.cpp @@ -12,6 +12,7 @@ namespace launchdarkly::server_side::data_systems { +static char const* const kEnvironmentIdHeader = "X-LD-EnvID"; static char const* const kFDv1FallbackHeader = "X-LD-FD-Fallback"; static char const* const kFDv1FallbackTtlHeader = "X-LD-FD-Fallback-TTL"; @@ -53,6 +54,15 @@ ReadFDv1FallbackDirective(network::HttpResult::HeadersType const& headers) { return directive; } +static std::optional ReadEnvironmentId( + network::HttpResult::HeadersType const& headers) { + auto const it = headers.find(kEnvironmentIdHeader); + if (it == headers.end() || it->second.empty()) { + return std::nullopt; + } + return it->second; +} + network::HttpRequest MakeFDv2PollRequest( std::string const& polling_base_url, config::built::HttpProperties const& http_properties, @@ -210,7 +220,7 @@ data_interfaces::FDv2SourceResult HandleFDv2PollResponse( data_model::ChangeSetType::kNone, {}, data_model::Selector{}}}, - fdv1_fallback}; + fdv1_fallback, ReadEnvironmentId(res.Headers())}; } if (res.Status() == 200) { @@ -238,6 +248,7 @@ data_interfaces::FDv2SourceResult HandleFDv2PollResponse( if (!result.fdv1_fallback) { result.fdv1_fallback = fdv1_fallback; } + result.environment_id = ReadEnvironmentId(res.Headers()); return result; } diff --git a/libs/server-sdk/src/data_systems/fdv2/streaming_synchronizer.cpp b/libs/server-sdk/src/data_systems/fdv2/streaming_synchronizer.cpp index 0f09ef5cf..c0d584ccf 100644 --- a/libs/server-sdk/src/data_systems/fdv2/streaming_synchronizer.cpp +++ b/libs/server-sdk/src/data_systems/fdv2/streaming_synchronizer.cpp @@ -186,6 +186,15 @@ void FDv2StreamingSynchronizer::State::OnConnect(HttpRequest* req) { void FDv2StreamingSynchronizer::State::OnResponse( HttpResponseHeader const& headers) { + if (headers.result_int() == 200) { + if (auto const env_it = headers.find("X-LD-EnvID"); + env_it != headers.end() && !env_it->value().empty()) { + std::lock_guard lock(mutex_); + environment_id_ = + std::string(env_it->value().data(), env_it->value().size()); + } + } + auto const it = headers.find("X-LD-FD-Fallback"); if (it == headers.end() || !boost::iequals(it->value(), "true")) { std::lock_guard lock(mutex_); @@ -337,6 +346,7 @@ void FDv2StreamingSynchronizer::State::Notify(FDv2SourceResult result) { if (!result.fdv1_fallback) { result.fdv1_fallback = latest_fdv1_fallback_; } + result.environment_id = environment_id_; if (pending_promise_) { promise = std::move(pending_promise_); pending_promise_.reset(); diff --git a/libs/server-sdk/src/data_systems/fdv2/streaming_synchronizer.hpp b/libs/server-sdk/src/data_systems/fdv2/streaming_synchronizer.hpp index f1e69ae8b..885456e39 100644 --- a/libs/server-sdk/src/data_systems/fdv2/streaming_synchronizer.hpp +++ b/libs/server-sdk/src/data_systems/fdv2/streaming_synchronizer.hpp @@ -149,6 +149,8 @@ class FDv2StreamingSynchronizer final // FDv1 fallback directive from the most recent SSE response. std::optional latest_fdv1_fallback_; + // Environment ID reported by the most recent successful SSE response. + std::optional environment_id_; data_model::Selector latest_selector_; std::optional base_url_; std::shared_ptr sse_client_; diff --git a/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.cpp b/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.cpp index 4235b9676..b9d775e58 100644 --- a/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.cpp +++ b/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.cpp @@ -142,6 +142,10 @@ bool LazyLoad::Initialized() const { return initialized_.value_or(false); } +std::optional LazyLoad::EnvironmentId() const { + return std::nullopt; +} + void LazyLoad::RefreshAllFlags() const { RefreshAll(Keys::kAllFlags, data_components::DataKind::kFlag, diff --git a/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.hpp b/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.hpp index 9584f60d6..d15c46f01 100644 --- a/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.hpp +++ b/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.hpp @@ -58,6 +58,8 @@ class LazyLoad final : public data_interfaces::IDataSystem { bool Initialized() const override; + std::optional EnvironmentId() const override; + // Public for usage in tests. struct Kinds { static integrations::FlagKind const Flag; diff --git a/libs/server-sdk/src/data_systems/offline.cpp b/libs/server-sdk/src/data_systems/offline.cpp index 148c10a2d..73cbfd926 100644 --- a/libs/server-sdk/src/data_systems/offline.cpp +++ b/libs/server-sdk/src/data_systems/offline.cpp @@ -41,4 +41,8 @@ std::string const& OfflineSystem::Identity() const { return ident; } +std::optional OfflineSystem::EnvironmentId() const { + return std::nullopt; +} + } // namespace launchdarkly::server_side::data_systems diff --git a/libs/server-sdk/src/data_systems/offline.hpp b/libs/server-sdk/src/data_systems/offline.hpp index c9355bd24..ae869de2f 100644 --- a/libs/server-sdk/src/data_systems/offline.hpp +++ b/libs/server-sdk/src/data_systems/offline.hpp @@ -22,6 +22,7 @@ class OfflineSystem final : public data_interfaces::IDataSystem { [[nodiscard]] bool Initialized() const override; [[nodiscard]] std::string const& Identity() const override; void Initialize() override; + [[nodiscard]] std::optional EnvironmentId() const override; private: data_components::DataSourceStatusManager& status_manager_; diff --git a/libs/server-sdk/tests/fdv1_adapter_synchronizer_test.cpp b/libs/server-sdk/tests/fdv1_adapter_synchronizer_test.cpp index 26de674b6..f232bbdc8 100644 --- a/libs/server-sdk/tests/fdv1_adapter_synchronizer_test.cpp +++ b/libs/server-sdk/tests/fdv1_adapter_synchronizer_test.cpp @@ -115,6 +115,20 @@ TEST(FDv1AdapterSynchronizerTest, FDv1InitProducesFullChangeSet) { EXPECT_FALSE(result->fdv1_fallback); } +TEST(FDv1AdapterSynchronizerTest, FDv1EnvironmentIdIsReportedWithChangeSet) { + MockFDv1Source* source = nullptr; + FDv1AdapterSynchronizer adapter(MakeMockBuilder(&source)); + + auto future = adapter.Next(data_model::Selector{}); + + source->destination_->SetEnvironmentId("env-123"); + source->destination_->Init(data_model::SDKDataSet{}); + + auto result = future.WaitForResult(1s); + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(std::optional{"env-123"}, result->environment_id); +} + TEST(FDv1AdapterSynchronizerTest, FDv1UpsertProducesPartialChangeSet) { MockFDv1Source* source = nullptr; FDv1AdapterSynchronizer adapter(MakeMockBuilder(&source)); diff --git a/libs/server-sdk/tests/fdv2_polling_impl_test.cpp b/libs/server-sdk/tests/fdv2_polling_impl_test.cpp index d4ecd9ee6..f87e14c1b 100644 --- a/libs/server-sdk/tests/fdv2_polling_impl_test.cpp +++ b/libs/server-sdk/tests/fdv2_polling_impl_test.cpp @@ -162,6 +162,34 @@ TEST(MakeFDv2PollRequestTest, ValidFilterKeyIsIncluded) { EXPECT_EQ(req.Url(), "http://example.com/sdk/poll?filter=my-filter_1.0"); } +TEST(HandleFDv2PollResponseTest, OkReportsEnvironmentId) { + auto result = + HandleResponse(200, R"({"events":[]})", {{"X-LD-EnvID", "env-123"}}); + EXPECT_EQ(std::optional{"env-123"}, result.environment_id); +} + +TEST(HandleFDv2PollResponseTest, NotModifiedReportsEnvironmentId) { + auto result = + HandleResponse(304, std::nullopt, {{"X-LD-EnvID", "env-123"}}); + EXPECT_EQ(std::optional{"env-123"}, result.environment_id); +} + +TEST(HandleFDv2PollResponseTest, ErrorStatusDoesNotReportEnvironmentId) { + auto result = + HandleResponse(503, std::nullopt, {{"X-LD-EnvID", "env-123"}}); + EXPECT_FALSE(result.environment_id); +} + +TEST(HandleFDv2PollResponseTest, MissingHeaderDoesNotReportEnvironmentId) { + auto result = HandleResponse(200, R"({"events":[]})", {}); + EXPECT_FALSE(result.environment_id); +} + +TEST(HandleFDv2PollResponseTest, EmptyHeaderDoesNotReportEnvironmentId) { + auto result = HandleResponse(200, R"({"events":[]})", {{"X-LD-EnvID", ""}}); + EXPECT_FALSE(result.environment_id); +} + TEST(MakeFDv2PollRequestTest, InvalidFilterKeyIsDropped) { auto logger = MakeNullLogger(); auto props = diff --git a/libs/server-sdk/tests/memory_store_test.cpp b/libs/server-sdk/tests/memory_store_test.cpp index 901282ea2..1ebd30c7e 100644 --- a/libs/server-sdk/tests/memory_store_test.cpp +++ b/libs/server-sdk/tests/memory_store_test.cpp @@ -299,6 +299,29 @@ TEST(MemoryStoreTest, CanDeleteExistingSegment) { ASSERT_FALSE(store.RemoveSegment("segmentA")); } +TEST(MemoryStoreTest, HasNoEnvironmentIdByDefault) { + MemoryStore store; + store.Init(SDKDataSet()); + EXPECT_FALSE(store.EnvironmentId()); +} + +TEST(MemoryStoreTest, EnvironmentIdIsNotVisibleBeforeInit) { + MemoryStore store; + store.SetEnvironmentId("env-123"); + EXPECT_FALSE(store.EnvironmentId()); + + store.Init(SDKDataSet()); + EXPECT_EQ(std::optional{"env-123"}, store.EnvironmentId()); +} + +TEST(MemoryStoreTest, EnvironmentIdIsRetainedAcrossInit) { + MemoryStore store; + store.SetEnvironmentId("env-123"); + store.Init(SDKDataSet()); + store.Init(SDKDataSet()); + EXPECT_EQ(std::optional{"env-123"}, store.EnvironmentId()); +} + TEST(MemoryStoreTest, CanDeleteExistingFlag) { MemoryStore store;