Skip to content

Commit

Permalink
feat: generate analytic events from evaluations (#36)
Browse files Browse the repository at this point in the history
* feat: generate events from variation methods

* add log tag to flush workers

* update value with conversion operators

* add tests for Value conversion operators and Value::Array comparison ops

* fix: add operator== and operator!= for Value::Array and Value::Object
  • Loading branch information
cwaldren-ld authored May 4, 2023
1 parent 71759de commit c62dcf6
Show file tree
Hide file tree
Showing 23 changed files with 465 additions and 120 deletions.
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;
}
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

0 comments on commit c62dcf6

Please sign in to comment.