Skip to content

Commit c62dcf6

Browse files
authored
feat: generate analytic events from evaluations (#36)
* 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
1 parent 71759de commit c62dcf6

23 files changed

+465
-120
lines changed

apps/hello-cpp/main.cpp

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ using launchdarkly::client_side::flag_manager::detail::FlagManager;
2222
using launchdarkly::client_side::flag_manager::detail::FlagUpdater;
2323

2424
int main() {
25-
Logger logger(std::make_unique<ConsoleBackend>(LogLevel::kDebug, "Hello"));
25+
Logger logger(std::make_unique<ConsoleBackend>("Hello"));
2626

2727
net::io_context ioc;
2828

@@ -45,6 +45,8 @@ int main() {
4545
std::chrono::seconds{30}))
4646
.WithReasons(true)
4747
.UseReport(true))
48+
.Events(launchdarkly::client_side::EventsBuilder().FlushInterval(
49+
std::chrono::seconds(5)))
4850
.Build()
4951
.value(),
5052
ContextBuilder().kind("user", "ryan").build());
@@ -58,8 +60,9 @@ int main() {
5860

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

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

6467
// Sit around.
6568
std::cout << "Press enter to exit" << std::endl;

libs/client-sdk/include/launchdarkly/client_side/api.hpp

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,11 @@
77
#include <memory>
88
#include <optional>
99
#include <thread>
10-
1110
#include <tl/expected.hpp>
11+
#include <tuple>
1212
#include "config/client.hpp"
1313
#include "context.hpp"
14+
#include "data/evaluation_detail.hpp"
1415
#include "error.hpp"
1516
#include "launchdarkly/client_side/data_source.hpp"
1617
#include "launchdarkly/client_side/data_sources/detail/data_source_status_manager.hpp"
@@ -30,6 +31,8 @@ class Client {
3031
Client& operator=(Client) = delete;
3132
Client& operator=(Client&& other) = delete;
3233

34+
bool Initialized() const;
35+
3336
using FlagKey = std::string;
3437
[[nodiscard]] std::unordered_map<FlagKey, Value> AllFlags() const;
3538

@@ -45,28 +48,48 @@ class Client {
4548

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

51+
EvaluationDetail<bool> BoolVariationDetail(FlagKey const& key,
52+
bool default_value);
53+
4854
std::string StringVariation(FlagKey const& key, std::string default_value);
4955

56+
EvaluationDetail<std::string> StringVariationDetail(
57+
FlagKey const& key,
58+
std::string default_value);
59+
5060
double DoubleVariation(FlagKey const& key, double default_value);
5161

62+
EvaluationDetail<double> DoubleVariationDetail(FlagKey const& key,
63+
double default_value);
64+
5265
int IntVariation(FlagKey const& key, int default_value);
5366

67+
EvaluationDetail<int> IntVariationDetail(FlagKey const& key,
68+
int default_value);
69+
5470
Value JsonVariation(FlagKey const& key, Value default_value);
5571

72+
EvaluationDetail<Value> JsonVariationDetail(FlagKey const& key,
73+
Value default_value);
74+
5675
data_sources::IDataSourceStatusProvider& DataSourceStatus();
5776

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

6079
~Client();
6180

6281
private:
63-
Value VariationInternal(FlagKey const& key, Value default_value);
82+
template <typename T>
83+
[[nodiscard]] EvaluationDetail<T> VariationInternal(FlagKey const& key,
84+
Value default_value,
85+
bool check_type,
86+
bool detailed);
6487
void TrackInternal(std::string event_name,
6588
std::optional<Value> data,
6689
std::optional<double> metric_value);
6790

6891
bool initialized_;
69-
std::mutex init_mutex_;
92+
mutable std::mutex init_mutex_;
7093
std::condition_variable init_waiter_;
7194

7295
data_sources::detail::DataSourceStatusManager status_manager_;
@@ -80,6 +103,8 @@ class Client {
80103
std::unique_ptr<IEventProcessor> event_processor_;
81104
std::unique_ptr<IDataSource> data_source_;
82105
std::thread run_thread_;
106+
107+
bool eval_reasons_available_;
83108
};
84109

85110
} // namespace launchdarkly::client_side

libs/client-sdk/src/api.cpp

Lines changed: 133 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,8 @@ Client::Client(Config config, Context context)
4141
flag_updater_,
4242
status_manager_,
4343
logger_)),
44-
initialized_(false) {
44+
initialized_(false),
45+
eval_reasons_available_(config.DataSourceConfig().with_reasons) {
4546
if (config.Events().Enabled()) {
4647
event_processor_ = std::make_unique<detail::EventProcessor>(
4748
ioc_.get_executor(), config, logger_);
@@ -67,6 +68,11 @@ Client::Client(Config config, Context context)
6768
run_thread_ = std::move(std::thread([&]() { ioc_.run(); }));
6869
}
6970

71+
bool Client::Initialized() const {
72+
std::unique_lock lock(init_mutex_);
73+
return initialized_;
74+
}
75+
7076
std::unordered_map<Client::FlagKey, Value> Client::AllFlags() const {
7177
return {};
7278
}
@@ -100,34 +106,150 @@ void Client::AsyncIdentify(Context context) {
100106
std::chrono::system_clock::now(), std::move(context)});
101107
}
102108

103-
Value Client::VariationInternal(FlagKey const& key, Value default_value) {
104-
auto res = flag_manager_.Get(key);
105-
if (res && res->flag) {
106-
return res->flag->detail().value();
109+
// TODO(cwaldren): refactor VariationInternal so it isn't so long and mixing up
110+
// multiple concerns.
111+
template <typename T>
112+
EvaluationDetail<T> Client::VariationInternal(FlagKey const& key,
113+
Value default_value,
114+
bool check_type,
115+
bool detailed) {
116+
auto desc = flag_manager_.Get(key);
117+
118+
events::client::FeatureEventParams event = {
119+
std::chrono::system_clock::now(),
120+
key,
121+
context_,
122+
default_value,
123+
default_value,
124+
std::nullopt,
125+
std::nullopt,
126+
std::nullopt,
127+
false,
128+
std::nullopt,
129+
};
130+
131+
if (!desc || !desc->flag) {
132+
if (!Initialized()) {
133+
LD_LOG(logger_, LogLevel::kWarn)
134+
<< "LaunchDarkly client has not yet been initialized. "
135+
"Returning default value";
136+
137+
// TODO: SC-199918
138+
auto error_reason = EvaluationReason("CLIENT_NOT_READY");
139+
if (eval_reasons_available_) {
140+
event.reason = error_reason;
141+
}
142+
event_processor_->AsyncSend(std::move(event));
143+
return EvaluationDetail<T>(default_value, std::nullopt,
144+
std::move(error_reason));
145+
}
146+
147+
LD_LOG(logger_, LogLevel::kInfo)
148+
<< "Unknown feature flag " << key << "; returning default value";
149+
150+
auto error_reason = EvaluationReason("FLAG_NOT_FOUND");
151+
if (eval_reasons_available_) {
152+
event.reason = error_reason;
153+
}
154+
event_processor_->AsyncSend(std::move(event));
155+
return EvaluationDetail<T>(default_value, std::nullopt,
156+
std::move(error_reason));
157+
158+
} else if (!Initialized()) {
159+
LD_LOG(logger_, LogLevel::kWarn)
160+
<< "LaunchDarkly client has not yet been initialized. "
161+
"Returning cached value";
162+
}
163+
164+
assert(desc->flag);
165+
166+
auto const& flag = *(desc->flag);
167+
auto const& detail = flag.detail();
168+
169+
if (check_type && default_value.type() != Value::Type::kNull &&
170+
detail.value().type() != default_value.type()) {
171+
auto error_reason = EvaluationReason("WRONG_TYPE");
172+
if (eval_reasons_available_) {
173+
event.reason = error_reason;
174+
}
175+
event_processor_->AsyncSend(std::move(event));
176+
return EvaluationDetail<T>(default_value, std::nullopt, error_reason);
177+
}
178+
179+
event.value = detail.value();
180+
event.variation = detail.variation_index();
181+
182+
if (detailed || flag.track_reason()) {
183+
event.reason = detail.reason();
184+
}
185+
186+
event.version = flag.flag_version().value_or(flag.version());
187+
event.require_full_event = flag.track_events();
188+
if (auto date = flag.debug_events_until_date()) {
189+
event.debug_events_until_date = events::Date{*date};
107190
}
108-
return default_value;
191+
192+
event_processor_->AsyncSend(std::move(event));
193+
194+
// TODO: this isn't a valid error, figure out how to handle if reason is
195+
// missing.
196+
EvaluationReason returned_reason("UNKNOWN");
197+
if (detail.reason()) {
198+
returned_reason = detail.reason()->get();
199+
}
200+
return EvaluationDetail<T>(detail.value(), detail.variation_index(),
201+
returned_reason);
202+
}
203+
204+
EvaluationDetail<bool> Client::BoolVariationDetail(Client::FlagKey const& key,
205+
bool default_value) {
206+
return VariationInternal<bool>(key, default_value, true, true);
109207
}
110208

111209
bool Client::BoolVariation(Client::FlagKey const& key, bool default_value) {
112-
return VariationInternal(key, default_value).as_bool();
210+
return *VariationInternal<bool>(key, default_value, true, false);
211+
}
212+
213+
EvaluationDetail<std::string> Client::StringVariationDetail(
214+
Client::FlagKey const& key,
215+
std::string default_value) {
216+
return VariationInternal<std::string>(key, std::move(default_value), true,
217+
true);
113218
}
114219

115220
std::string Client::StringVariation(Client::FlagKey const& key,
116221
std::string default_value) {
117-
return VariationInternal(key, std::move(default_value)).as_string();
222+
return *VariationInternal<std::string>(key, std::move(default_value), true,
223+
false);
224+
}
225+
226+
EvaluationDetail<double> Client::DoubleVariationDetail(
227+
Client::FlagKey const& key,
228+
double default_value) {
229+
return VariationInternal<double>(key, default_value, true, true);
118230
}
119231

120232
double Client::DoubleVariation(Client::FlagKey const& key,
121233
double default_value) {
122-
return VariationInternal(key, default_value).as_double();
234+
return *VariationInternal<double>(key, default_value, true, false);
123235
}
124236

237+
EvaluationDetail<int> Client::IntVariationDetail(Client::FlagKey const& key,
238+
int default_value) {
239+
return VariationInternal<int>(key, default_value, true, true);
240+
}
125241
int Client::IntVariation(Client::FlagKey const& key, int default_value) {
126-
return VariationInternal(key, default_value).as_int();
242+
return *VariationInternal<int>(key, default_value, true, false);
243+
}
244+
245+
EvaluationDetail<Value> Client::JsonVariationDetail(Client::FlagKey const& key,
246+
Value default_value) {
247+
return VariationInternal<Value>(key, std::move(default_value), false, true);
127248
}
128249

129250
Value Client::JsonVariation(Client::FlagKey const& key, Value default_value) {
130-
return VariationInternal(key, std::move(default_value));
251+
return *VariationInternal<Value>(key, std::move(default_value), false,
252+
false);
131253
}
132254

133255
data_sources::IDataSourceStatusProvider& Client::DataSourceStatus() {

libs/client-sdk/tests/client_test.cpp

Lines changed: 70 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,82 @@
11
#include <gtest/gtest.h>
22
#include <launchdarkly/client_side/api.hpp>
3+
#include <map>
34
#include "context_builder.hpp"
45

56
using namespace launchdarkly;
7+
using namespace launchdarkly::client_side;
68

7-
TEST(ClientTest, ConstructClientWithConfig) {
8-
tl::expected<client_side::Config, Error> config =
9-
client_side::ConfigBuilder("sdk-123").Build();
10-
9+
TEST(ClientTest, ClientConstructedWithMinimalConfigAndContext) {
10+
tl::expected<Config, Error> config = ConfigBuilder("sdk-123").Build();
1111
ASSERT_TRUE(config);
1212

13-
auto context = ContextBuilder().kind("cat", "shadow").build();
13+
Context context = ContextBuilder().kind("cat", "shadow").build();
14+
15+
Client client(std::move(*config), context);
16+
}
1417

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

1722
ASSERT_TRUE(client.AllFlags().empty());
18-
ASSERT_TRUE(client.BoolVariation("cat-food", true));
23+
}
24+
25+
TEST(ClientTest, BoolVariationDefaultPassesThrough) {
26+
Client client(ConfigBuilder("sdk-123").Build().value(),
27+
ContextBuilder().kind("cat", "shadow").build());
28+
29+
const std::string flag = "extra-cat-food";
30+
std::vector<bool> values = {true, false};
31+
for (auto const& v : values) {
32+
ASSERT_EQ(client.BoolVariation(flag, v), v);
33+
ASSERT_EQ(*client.BoolVariationDetail(flag, v), v);
34+
}
35+
}
36+
37+
TEST(ClientTest, StringVariationDefaultPassesThrough) {
38+
Client client(ConfigBuilder("sdk-123").Build().value(),
39+
ContextBuilder().kind("cat", "shadow").build());
40+
const std::string flag = "treat";
41+
std::vector<std::string> values = {"chicken", "fish", "cat-grass"};
42+
for (auto const& v : values) {
43+
ASSERT_EQ(client.StringVariation(flag, v), v);
44+
ASSERT_EQ(*client.StringVariationDetail(flag, v), v);
45+
}
46+
}
47+
48+
TEST(ClientTest, IntVariationDefaultPassesThrough) {
49+
Client client(ConfigBuilder("sdk-123").Build().value(),
50+
ContextBuilder().kind("cat", "shadow").build());
51+
const std::string flag = "weight";
52+
std::vector<int> values = {0, 12, 13, 24, 1000};
53+
for (auto const& v : values) {
54+
ASSERT_EQ(client.IntVariation("weight", v), v);
55+
ASSERT_EQ(*client.IntVariationDetail("weight", v), v);
56+
}
57+
}
58+
59+
TEST(ClientTest, DoubleVariationDefaultPassesThrough) {
60+
Client client(ConfigBuilder("sdk-123").Build().value(),
61+
ContextBuilder().kind("cat", "shadow").build());
62+
const std::string flag = "weight";
63+
std::vector<double> values = {0.0, 12.0, 13.0, 24.0, 1000.0};
64+
for (auto const& v : values) {
65+
ASSERT_EQ(client.DoubleVariation(flag, v), v);
66+
ASSERT_EQ(*client.DoubleVariationDetail(flag, v), v);
67+
}
68+
}
69+
70+
TEST(ClientTest, JsonVariationDefaultPassesThrough) {
71+
Client client(ConfigBuilder("sdk-123").Build().value(),
72+
ContextBuilder().kind("cat", "shadow").build());
73+
74+
const std::string flag = "assorted-values";
75+
std::vector<Value> values = {
76+
Value({"running", "jumping"}), Value(3), Value(1.0), Value(true),
77+
Value(std::map<std::string, Value>{{"weight", 20}})};
78+
for (auto const& v : values) {
79+
ASSERT_EQ(client.JsonVariation(flag, v), v);
80+
ASSERT_EQ(*client.JsonVariationDetail(flag, v), v);
81+
}
1982
}

0 commit comments

Comments
 (0)