Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions apps/hello-cpp/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ using launchdarkly::client_side::flag_manager::detail::FlagManager;
using launchdarkly::client_side::flag_manager::detail::FlagUpdater;

int main() {
Logger logger(std::make_unique<ConsoleBackend>(LogLevel::kDebug, "Hello"));
Logger logger(std::make_unique<ConsoleBackend>("Hello"));

net::io_context ioc;

Expand All @@ -45,6 +45,8 @@ int main() {
std::chrono::seconds{30}))
.WithReasons(true)
.UseReport(true))
.Events(launchdarkly::client_side::EventsBuilder().FlushInterval(
std::chrono::seconds(5)))
.Build()
.value(),
ContextBuilder().kind("user", "ryan").build());
Expand All @@ -58,8 +60,9 @@ int main() {

client.WaitForReadySync(std::chrono::seconds(30));

auto value = client.BoolVariation("my-boolean-flag", false);
LD_LOG(logger, LogLevel::kInfo) << "Value was: " << value;
auto value = client.BoolVariationDetail("my-bool-flag", false);
LD_LOG(logger, LogLevel::kInfo) << "Value was: " << *value;
LD_LOG(logger, LogLevel::kInfo) << "Reason was: " << value.Reason();

// Sit around.
std::cout << "Press enter to exit" << std::endl;
Expand Down
31 changes: 28 additions & 3 deletions libs/client-sdk/include/launchdarkly/client_side/api.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,11 @@
#include <memory>
#include <optional>
#include <thread>

#include <tl/expected.hpp>
#include <tuple>
#include "config/client.hpp"
#include "context.hpp"
#include "data/evaluation_detail.hpp"
#include "error.hpp"
#include "launchdarkly/client_side/data_source.hpp"
#include "launchdarkly/client_side/data_sources/detail/data_source_status_manager.hpp"
Expand All @@ -30,6 +31,8 @@ class Client {
Client& operator=(Client) = delete;
Client& operator=(Client&& other) = delete;

bool Initialized() const;

using FlagKey = std::string;
[[nodiscard]] std::unordered_map<FlagKey, Value> AllFlags() const;

Expand All @@ -45,28 +48,48 @@ class Client {

bool BoolVariation(FlagKey const& key, bool default_value);

EvaluationDetail<bool> BoolVariationDetail(FlagKey const& key,
bool default_value);

std::string StringVariation(FlagKey const& key, std::string default_value);

EvaluationDetail<std::string> StringVariationDetail(
FlagKey const& key,
std::string default_value);

double DoubleVariation(FlagKey const& key, double default_value);

EvaluationDetail<double> DoubleVariationDetail(FlagKey const& key,
double default_value);

int IntVariation(FlagKey const& key, int default_value);

EvaluationDetail<int> IntVariationDetail(FlagKey const& key,
int default_value);

Value JsonVariation(FlagKey const& key, Value default_value);

EvaluationDetail<Value> JsonVariationDetail(FlagKey const& key,
Value default_value);

data_sources::IDataSourceStatusProvider& DataSourceStatus();

void WaitForReadySync(std::chrono::seconds timeout);

~Client();

private:
Value VariationInternal(FlagKey const& key, Value default_value);
template <typename T>
[[nodiscard]] EvaluationDetail<T> VariationInternal(FlagKey const& key,
Value default_value,
bool check_type,
bool detailed);
void TrackInternal(std::string event_name,
std::optional<Value> data,
std::optional<double> metric_value);

bool initialized_;
std::mutex init_mutex_;
mutable std::mutex init_mutex_;
std::condition_variable init_waiter_;

data_sources::detail::DataSourceStatusManager status_manager_;
Expand All @@ -80,6 +103,8 @@ class Client {
std::unique_ptr<IEventProcessor> event_processor_;
std::unique_ptr<IDataSource> data_source_;
std::thread run_thread_;

bool eval_reasons_available_;
};

} // namespace launchdarkly::client_side
144 changes: 133 additions & 11 deletions libs/client-sdk/src/api.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@ Client::Client(Config config, Context context)
flag_updater_,
status_manager_,
logger_)),
initialized_(false) {
initialized_(false),
eval_reasons_available_(config.DataSourceConfig().with_reasons) {
if (config.Events().Enabled()) {
event_processor_ = std::make_unique<detail::EventProcessor>(
ioc_.get_executor(), config, logger_);
Expand All @@ -67,6 +68,11 @@ Client::Client(Config config, Context context)
run_thread_ = std::move(std::thread([&]() { ioc_.run(); }));
}

bool Client::Initialized() const {
std::unique_lock lock(init_mutex_);
return initialized_;
}

std::unordered_map<Client::FlagKey, Value> Client::AllFlags() const {
return {};
}
Expand Down Expand Up @@ -100,34 +106,150 @@ void Client::AsyncIdentify(Context context) {
std::chrono::system_clock::now(), std::move(context)});
}

Value Client::VariationInternal(FlagKey const& key, Value default_value) {
auto res = flag_manager_.Get(key);
if (res && res->flag) {
return res->flag->detail().value();
// TODO(cwaldren): refactor VariationInternal so it isn't so long and mixing up
// multiple concerns.
template <typename T>
EvaluationDetail<T> Client::VariationInternal(FlagKey const& key,
Value default_value,
bool check_type,
bool detailed) {
auto desc = flag_manager_.Get(key);

events::client::FeatureEventParams event = {
std::chrono::system_clock::now(),
key,
context_,
default_value,
default_value,
std::nullopt,
std::nullopt,
std::nullopt,
false,
std::nullopt,
};

if (!desc || !desc->flag) {
if (!Initialized()) {
LD_LOG(logger_, LogLevel::kWarn)
<< "LaunchDarkly client has not yet been initialized. "
"Returning default value";

// TODO: SC-199918
auto error_reason = EvaluationReason("CLIENT_NOT_READY");
if (eval_reasons_available_) {
event.reason = error_reason;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm copying the if (reasons) part from the Go client SDK, but I'm not sure the justification and I might have it wrong - will check other SDKs. The spec says:

The reason property is only set if any of the following is true:

    The application used the VariationDetail operation rather than Variation.

    For server-side SDKs, any of the conditions described in the previous “for server-side SDKs” bullet list are true.

    For client-side SDKs, the flag’s trackReasons property is true.

So I'd think it should only be set if flag.track_reason() or if detailed is true. Now, we can't use the former because the flag isn't present, so then it's up to detailed. But the Go SDK is using the fact that the user requested withReasons...

}
event_processor_->AsyncSend(std::move(event));
return EvaluationDetail<T>(default_value, std::nullopt,
std::move(error_reason));
}

LD_LOG(logger_, LogLevel::kInfo)
<< "Unknown feature flag " << key << "; returning default value";

auto error_reason = EvaluationReason("FLAG_NOT_FOUND");
if (eval_reasons_available_) {
event.reason = error_reason;
}
event_processor_->AsyncSend(std::move(event));
return EvaluationDetail<T>(default_value, std::nullopt,
std::move(error_reason));

} else if (!Initialized()) {
LD_LOG(logger_, LogLevel::kWarn)
<< "LaunchDarkly client has not yet been initialized. "
"Returning cached value";
}

assert(desc->flag);

auto const& flag = *(desc->flag);
auto const& detail = flag.detail();

if (check_type && default_value.type() != Value::Type::kNull &&
detail.value().type() != default_value.type()) {
auto error_reason = EvaluationReason("WRONG_TYPE");
if (eval_reasons_available_) {
event.reason = error_reason;
}
event_processor_->AsyncSend(std::move(event));
return EvaluationDetail<T>(default_value, std::nullopt, error_reason);
}

event.value = detail.value();
event.variation = detail.variation_index();

if (detailed || flag.track_reason()) {
event.reason = detail.reason();
}

event.version = flag.flag_version().value_or(flag.version());
event.require_full_event = flag.track_events();
if (auto date = flag.debug_events_until_date()) {
event.debug_events_until_date = events::Date{*date};
}
return default_value;

event_processor_->AsyncSend(std::move(event));

// TODO: this isn't a valid error, figure out how to handle if reason is
// missing.
EvaluationReason returned_reason("UNKNOWN");
if (detail.reason()) {
returned_reason = detail.reason()->get();
}
return EvaluationDetail<T>(detail.value(), detail.variation_index(),
returned_reason);
}

EvaluationDetail<bool> Client::BoolVariationDetail(Client::FlagKey const& key,
bool default_value) {
return VariationInternal<bool>(key, default_value, true, true);
}

bool Client::BoolVariation(Client::FlagKey const& key, bool default_value) {
return VariationInternal(key, default_value).as_bool();
return *VariationInternal<bool>(key, default_value, true, false);
}

EvaluationDetail<std::string> Client::StringVariationDetail(
Client::FlagKey const& key,
std::string default_value) {
return VariationInternal<std::string>(key, std::move(default_value), true,
true);
}

std::string Client::StringVariation(Client::FlagKey const& key,
std::string default_value) {
return VariationInternal(key, std::move(default_value)).as_string();
return *VariationInternal<std::string>(key, std::move(default_value), true,
false);
}

EvaluationDetail<double> Client::DoubleVariationDetail(
Client::FlagKey const& key,
double default_value) {
return VariationInternal<double>(key, default_value, true, true);
}

double Client::DoubleVariation(Client::FlagKey const& key,
double default_value) {
return VariationInternal(key, default_value).as_double();
return *VariationInternal<double>(key, default_value, true, false);
}

EvaluationDetail<int> Client::IntVariationDetail(Client::FlagKey const& key,
int default_value) {
return VariationInternal<int>(key, default_value, true, true);
}
int Client::IntVariation(Client::FlagKey const& key, int default_value) {
return VariationInternal(key, default_value).as_int();
return *VariationInternal<int>(key, default_value, true, false);
}

EvaluationDetail<Value> Client::JsonVariationDetail(Client::FlagKey const& key,
Value default_value) {
return VariationInternal<Value>(key, std::move(default_value), false, true);
}

Value Client::JsonVariation(Client::FlagKey const& key, Value default_value) {
return VariationInternal(key, std::move(default_value));
return *VariationInternal<Value>(key, std::move(default_value), false,
false);
}

data_sources::IDataSourceStatusProvider& Client::DataSourceStatus() {
Expand Down
77 changes: 70 additions & 7 deletions libs/client-sdk/tests/client_test.cpp
Original file line number Diff line number Diff line change
@@ -1,19 +1,82 @@
#include <gtest/gtest.h>
#include <launchdarkly/client_side/api.hpp>
#include <map>
#include "context_builder.hpp"

using namespace launchdarkly;
using namespace launchdarkly::client_side;

TEST(ClientTest, ConstructClientWithConfig) {
tl::expected<client_side::Config, Error> config =
client_side::ConfigBuilder("sdk-123").Build();

TEST(ClientTest, ClientConstructedWithMinimalConfigAndContext) {
tl::expected<Config, Error> config = ConfigBuilder("sdk-123").Build();
ASSERT_TRUE(config);

auto context = ContextBuilder().kind("cat", "shadow").build();
Context context = ContextBuilder().kind("cat", "shadow").build();

Client client(std::move(*config), context);
}

client_side::Client client(std::move(*config), context);
TEST(ClientTest, AllFlagsIsEmpty) {
Client client(ConfigBuilder("sdk-123").Build().value(),
ContextBuilder().kind("cat", "shadow").build());

ASSERT_TRUE(client.AllFlags().empty());
ASSERT_TRUE(client.BoolVariation("cat-food", true));
}

TEST(ClientTest, BoolVariationDefaultPassesThrough) {
Client client(ConfigBuilder("sdk-123").Build().value(),
ContextBuilder().kind("cat", "shadow").build());

const std::string flag = "extra-cat-food";
std::vector<bool> values = {true, false};
for (auto const& v : values) {
ASSERT_EQ(client.BoolVariation(flag, v), v);
ASSERT_EQ(*client.BoolVariationDetail(flag, v), v);
}
}

TEST(ClientTest, StringVariationDefaultPassesThrough) {
Client client(ConfigBuilder("sdk-123").Build().value(),
ContextBuilder().kind("cat", "shadow").build());
const std::string flag = "treat";
std::vector<std::string> values = {"chicken", "fish", "cat-grass"};
for (auto const& v : values) {
ASSERT_EQ(client.StringVariation(flag, v), v);
ASSERT_EQ(*client.StringVariationDetail(flag, v), v);
}
}

TEST(ClientTest, IntVariationDefaultPassesThrough) {
Client client(ConfigBuilder("sdk-123").Build().value(),
ContextBuilder().kind("cat", "shadow").build());
const std::string flag = "weight";
std::vector<int> values = {0, 12, 13, 24, 1000};
for (auto const& v : values) {
ASSERT_EQ(client.IntVariation("weight", v), v);
ASSERT_EQ(*client.IntVariationDetail("weight", v), v);
}
}

TEST(ClientTest, DoubleVariationDefaultPassesThrough) {
Client client(ConfigBuilder("sdk-123").Build().value(),
ContextBuilder().kind("cat", "shadow").build());
const std::string flag = "weight";
std::vector<double> values = {0.0, 12.0, 13.0, 24.0, 1000.0};
for (auto const& v : values) {
ASSERT_EQ(client.DoubleVariation(flag, v), v);
ASSERT_EQ(*client.DoubleVariationDetail(flag, v), v);
}
}

TEST(ClientTest, JsonVariationDefaultPassesThrough) {
Client client(ConfigBuilder("sdk-123").Build().value(),
ContextBuilder().kind("cat", "shadow").build());

const std::string flag = "assorted-values";
std::vector<Value> values = {
Value({"running", "jumping"}), Value(3), Value(1.0), Value(true),
Value(std::map<std::string, Value>{{"weight", 20}})};
for (auto const& v : values) {
ASSERT_EQ(client.JsonVariation(flag, v), v);
ASSERT_EQ(*client.JsonVariationDetail(flag, v), v);
}
}
Loading