1use super::stores::store::DataStore;
2use serde::Serialize;
3use std::cell::RefCell;
4
5use launchdarkly_server_sdk_evaluation::{
6 evaluate, Context, FlagValue, PrerequisiteEvent, PrerequisiteEventRecorder, Reason,
7};
8use std::collections::HashMap;
9use std::time::SystemTime;
10
11#[derive(Clone, Copy, Default)]
25pub struct FlagDetailConfig {
26 client_side_only: bool,
27 with_reasons: bool,
28 details_only_for_tracked_flags: bool,
29}
30
31impl FlagDetailConfig {
32 pub fn new() -> Self {
36 Self {
37 client_side_only: false,
38 with_reasons: false,
39 details_only_for_tracked_flags: false,
40 }
41 }
42
43 pub fn client_side_only(&mut self) -> &mut Self {
46 self.client_side_only = true;
47 self
48 }
49
50 pub fn with_reasons(&mut self) -> &mut Self {
52 self.with_reasons = true;
53 self
54 }
55
56 pub fn details_only_for_tracked_flags(&mut self) -> &mut Self {
59 self.details_only_for_tracked_flags = true;
60 self
61 }
62}
63
64#[derive(Serialize, Default, Debug, Clone)]
65#[serde(rename_all = "camelCase")]
66pub struct FlagState {
67 #[serde(skip_serializing_if = "Option::is_none")]
68 version: Option<u64>,
69
70 #[serde(skip_serializing_if = "Option::is_none")]
71 variation: Option<isize>,
72
73 #[serde(skip_serializing_if = "Option::is_none")]
74 reason: Option<Reason>,
75
76 #[serde(skip_serializing_if = "std::ops::Not::not")]
77 track_events: bool,
78
79 #[serde(skip_serializing_if = "std::ops::Not::not")]
80 track_reason: bool,
81
82 #[serde(skip_serializing_if = "Option::is_none")]
83 debug_events_until_date: Option<u64>,
84
85 #[serde(skip_serializing_if = "Vec::is_empty")]
86 prerequisites: Vec<String>,
87}
88
89#[derive(Serialize, Clone, Debug)]
95pub struct FlagDetail {
96 #[serde(flatten)]
97 evaluations: HashMap<String, Option<FlagValue>>,
98
99 #[serde(rename = "$flagsState")]
100 flag_state: HashMap<String, FlagState>,
101
102 #[serde(rename = "$valid")]
103 valid: bool,
104}
105
106struct DirectPrerequisiteRecorder {
109 target_flag_key: String,
110 prerequisites: RefCell<Vec<String>>,
111}
112
113impl DirectPrerequisiteRecorder {
114 pub fn new(target_flag_key: impl Into<String>) -> Self {
118 Self {
119 target_flag_key: target_flag_key.into(),
120 prerequisites: RefCell::new(Vec::new()),
121 }
122 }
123}
124impl PrerequisiteEventRecorder for DirectPrerequisiteRecorder {
125 fn record(&self, event: PrerequisiteEvent) {
126 if event.target_flag_key == self.target_flag_key {
127 self.prerequisites
128 .borrow_mut()
129 .push(event.prerequisite_flag.key)
130 }
131 }
132}
133
134impl FlagDetail {
135 pub fn new(valid: bool) -> Self {
137 Self {
138 evaluations: HashMap::new(),
139 flag_state: HashMap::new(),
140 valid,
141 }
142 }
143
144 pub fn populate(&mut self, store: &dyn DataStore, context: &Context, config: FlagDetailConfig) {
147 let mut evaluations = HashMap::new();
148 let mut flag_state = HashMap::new();
149
150 for (key, flag) in store.all_flags() {
151 if config.client_side_only && !flag.using_environment_id() {
152 continue;
153 }
154
155 let event_recorder = DirectPrerequisiteRecorder::new(key.clone());
156
157 let detail = evaluate(store.to_store(), &flag, context, Some(&event_recorder));
158
159 let require_experiment_data = flag.is_experimentation_enabled(&detail.reason);
163 let track_events = flag.track_events || require_experiment_data;
164 let track_reason = require_experiment_data;
165
166 let currently_debugging = match flag.debug_events_until_date {
167 Some(time) => {
168 let today = SystemTime::now();
169 let today_millis = today
170 .duration_since(SystemTime::UNIX_EPOCH)
171 .unwrap()
172 .as_millis();
173 (time as u128) > today_millis
174 }
175 None => false,
176 };
177
178 let mut omit_details = false;
179 if config.details_only_for_tracked_flags
180 && !(track_events
181 || track_reason
182 || flag.debug_events_until_date.is_some() && currently_debugging)
183 {
184 omit_details = true;
185 }
186
187 let mut reason = if !config.with_reasons && !track_reason {
188 None
189 } else {
190 Some(detail.reason)
191 };
192
193 let mut version = Some(flag.version);
194 if omit_details {
195 reason = None;
196 version = None;
197 }
198
199 evaluations.insert(key.clone(), detail.value.cloned());
200
201 flag_state.insert(
202 key,
203 FlagState {
204 version,
205 variation: detail.variation_index,
206 reason,
207 track_events,
208 track_reason,
209 debug_events_until_date: flag.debug_events_until_date,
210 prerequisites: event_recorder.prerequisites.take(),
211 },
212 );
213 }
214
215 self.evaluations = evaluations;
216 self.flag_state = flag_state;
217 }
218}
219
220#[cfg(test)]
221mod tests {
222 use crate::evaluation::FlagDetail;
223 use crate::stores::store::DataStore;
224 use crate::stores::store::InMemoryDataStore;
225 use crate::stores::store_types::{PatchTarget, StorageItem};
226 use crate::test_common::{
227 basic_flag, basic_flag_with_prereqs_and_visibility, basic_flag_with_visibility,
228 basic_off_flag,
229 };
230 use crate::FlagDetailConfig;
231 use assert_json_diff::assert_json_eq;
232 use launchdarkly_server_sdk_evaluation::ContextBuilder;
233
234 #[test]
235 fn flag_detail_handles_default_configuration() {
236 let context = ContextBuilder::new("bob")
237 .build()
238 .expect("Failed to create context");
239 let mut store = InMemoryDataStore::new();
240
241 store
242 .upsert(
243 "myFlag",
244 PatchTarget::Flag(StorageItem::Item(basic_flag("myFlag"))),
245 )
246 .expect("patch should apply");
247
248 let mut flag_detail = FlagDetail::new(true);
249 flag_detail.populate(&store, &context, FlagDetailConfig::new());
250
251 let expected = json!({
252 "myFlag": true,
253 "$flagsState": {
254 "myFlag": {
255 "version": 42,
256 "variation": 1
257 }
258 },
259 "$valid": true
260 });
261
262 assert_eq!(
263 serde_json::to_string_pretty(&flag_detail).unwrap(),
264 serde_json::to_string_pretty(&expected).unwrap(),
265 );
266 }
267
268 #[test]
269 fn flag_detail_handles_experimentation_reasons_correctly() {
270 let context = ContextBuilder::new("bob")
271 .build()
272 .expect("Failed to create context");
273 let mut store = InMemoryDataStore::new();
274
275 let mut flag = basic_flag("myFlag");
276 flag.track_events = false;
277 flag.track_events_fallthrough = true;
278
279 store
280 .upsert("myFlag", PatchTarget::Flag(StorageItem::Item(flag)))
281 .expect("patch should apply");
282
283 let mut flag_detail = FlagDetail::new(true);
284 flag_detail.populate(&store, &context, FlagDetailConfig::new());
285
286 let expected = json!({
287 "myFlag": true,
288 "$flagsState": {
289 "myFlag": {
290 "version": 42,
291 "variation": 1,
292 "reason": {
293 "kind": "FALLTHROUGH",
294 },
295 "trackEvents": true,
296 "trackReason": true,
297 }
298 },
299 "$valid": true
300 });
301
302 assert_eq!(
303 serde_json::to_string_pretty(&flag_detail).unwrap(),
304 serde_json::to_string_pretty(&expected).unwrap(),
305 );
306 }
307
308 #[test]
309 fn flag_detail_with_reasons_should_include_reason() {
310 let context = ContextBuilder::new("bob")
311 .build()
312 .expect("Failed to create context");
313 let mut store = InMemoryDataStore::new();
314
315 store
316 .upsert(
317 "myFlag",
318 PatchTarget::Flag(StorageItem::Item(basic_flag("myFlag"))),
319 )
320 .expect("patch should apply");
321
322 let mut config = FlagDetailConfig::new();
323 config.with_reasons();
324
325 let mut flag_detail = FlagDetail::new(true);
326 flag_detail.populate(&store, &context, config);
327
328 let expected = json!({
329 "myFlag": true,
330 "$flagsState": {
331 "myFlag": {
332 "version": 42,
333 "variation": 1,
334 "reason": {
335 "kind": "FALLTHROUGH"
336 }
337 }
338 },
339 "$valid": true
340 });
341
342 assert_eq!(
343 serde_json::to_string_pretty(&flag_detail).unwrap(),
344 serde_json::to_string_pretty(&expected).unwrap(),
345 );
346 }
347
348 #[test]
349 fn flag_detail_details_only_should_exclude_reason() {
350 let context = ContextBuilder::new("bob")
351 .build()
352 .expect("Failed to create context");
353 let mut store = InMemoryDataStore::new();
354
355 store
356 .upsert(
357 "myFlag",
358 PatchTarget::Flag(StorageItem::Item(basic_flag("myFlag"))),
359 )
360 .expect("patch should apply");
361
362 let mut config = FlagDetailConfig::new();
363 config.details_only_for_tracked_flags();
364
365 let mut flag_detail = FlagDetail::new(true);
366 flag_detail.populate(&store, &context, config);
367
368 let expected = json!({
369 "myFlag": true,
370 "$flagsState": {
371 "myFlag": {
372 "variation": 1,
373 }
374 },
375 "$valid": true
376 });
377
378 assert_eq!(
379 serde_json::to_string_pretty(&flag_detail).unwrap(),
380 serde_json::to_string_pretty(&expected).unwrap(),
381 );
382 }
383
384 #[test]
385 fn flag_detail_details_only_with_tracked_events_includes_version() {
386 let context = ContextBuilder::new("bob")
387 .build()
388 .expect("Failed to create context");
389 let mut store = InMemoryDataStore::new();
390 let mut flag = basic_flag("myFlag");
391 flag.track_events = true;
392
393 store
394 .upsert("myFlag", PatchTarget::Flag(StorageItem::Item(flag)))
395 .expect("patch should apply");
396
397 let mut config = FlagDetailConfig::new();
398 config.details_only_for_tracked_flags();
399
400 let mut flag_detail = FlagDetail::new(true);
401 flag_detail.populate(&store, &context, config);
402
403 let expected = json!({
404 "myFlag": true,
405 "$flagsState": {
406 "myFlag": {
407 "version": 42,
408 "variation": 1,
409 "trackEvents": true,
410 }
411 },
412 "$valid": true
413 });
414
415 assert_eq!(
416 serde_json::to_string_pretty(&flag_detail).unwrap(),
417 serde_json::to_string_pretty(&expected).unwrap(),
418 );
419 }
420
421 #[test]
422 fn flag_detail_with_default_config_but_tracked_event_should_include_version() {
423 let context = ContextBuilder::new("bob")
424 .build()
425 .expect("Failed to create context");
426 let mut store = InMemoryDataStore::new();
427 let mut flag = basic_flag("myFlag");
428 flag.track_events = true;
429
430 store
431 .upsert("myFlag", PatchTarget::Flag(StorageItem::Item(flag)))
432 .expect("patch should apply");
433
434 let mut flag_detail = FlagDetail::new(true);
435 flag_detail.populate(&store, &context, FlagDetailConfig::new());
436
437 let expected = json!({
438 "myFlag": true,
439 "$flagsState": {
440 "myFlag": {
441 "version": 42,
442 "variation": 1,
443 "trackEvents": true,
444 }
445 },
446 "$valid": true
447 });
448
449 assert_eq!(
450 serde_json::to_string_pretty(&flag_detail).unwrap(),
451 serde_json::to_string_pretty(&expected).unwrap(),
452 );
453 }
454
455 #[test]
456 fn flag_prerequisites_should_be_exposed() {
457 let context = ContextBuilder::new("bob")
458 .build()
459 .expect("Failed to create context");
460 let mut store = InMemoryDataStore::new();
461
462 let prereq1 = basic_flag("prereq1");
463 let prereq2 = basic_flag("prereq2");
464 let toplevel =
465 basic_flag_with_prereqs_and_visibility("toplevel", &["prereq1", "prereq2"], false);
466
467 store
468 .upsert("prereq1", PatchTarget::Flag(StorageItem::Item(prereq1)))
469 .expect("patch should apply");
470
471 store
472 .upsert("prereq2", PatchTarget::Flag(StorageItem::Item(prereq2)))
473 .expect("patch should apply");
474
475 store
476 .upsert("toplevel", PatchTarget::Flag(StorageItem::Item(toplevel)))
477 .expect("patch should apply");
478
479 let mut flag_detail = FlagDetail::new(true);
480 flag_detail.populate(&store, &context, FlagDetailConfig::new());
481
482 let expected = json!({
483 "prereq1": true,
484 "prereq2": true,
485 "toplevel": true,
486 "$flagsState": {
487 "toplevel": {
488 "version": 42,
489 "variation": 1,
490 "prerequisites": ["prereq1", "prereq2"]
491 },
492 "prereq2": {
493 "version": 42,
494 "variation": 1
495 },
496 "prereq1": {
497 "version": 42,
498 "variation": 1,
499 },
500 },
501 "$valid": true
502 });
503
504 assert_json_eq!(expected, flag_detail);
505 }
506
507 #[test]
508 fn flag_prerequisites_should_be_exposed_even_if_not_available_to_clients() {
509 let context = ContextBuilder::new("bob")
510 .build()
511 .expect("Failed to create context");
512 let mut store = InMemoryDataStore::new();
513
514 let prereq1 = basic_flag_with_visibility("prereq1", false);
516 let prereq2 = basic_flag_with_visibility("prereq2", false);
517
518 let toplevel =
520 basic_flag_with_prereqs_and_visibility("toplevel", &["prereq1", "prereq2"], true);
521
522 store
523 .upsert("prereq1", PatchTarget::Flag(StorageItem::Item(prereq1)))
524 .expect("patch should apply");
525
526 store
527 .upsert("prereq2", PatchTarget::Flag(StorageItem::Item(prereq2)))
528 .expect("patch should apply");
529
530 store
531 .upsert("toplevel", PatchTarget::Flag(StorageItem::Item(toplevel)))
532 .expect("patch should apply");
533
534 let mut flag_detail = FlagDetail::new(true);
535
536 let mut config = FlagDetailConfig::new();
537 config.client_side_only();
538
539 flag_detail.populate(&store, &context, config);
540
541 let expected = json!({
544 "toplevel": true,
545 "$flagsState": {
546 "toplevel": {
547 "version": 42,
548 "variation": 1,
549 "prerequisites": ["prereq1", "prereq2"]
550 },
551 },
552 "$valid": true
553 });
554
555 assert_json_eq!(expected, flag_detail);
556 }
557
558 #[test]
559 fn flag_prerequisites_should_be_in_evaluation_order() {
560 let context = ContextBuilder::new("bob")
561 .build()
562 .expect("Failed to create context");
563 let mut store = InMemoryDataStore::new();
564
565 let prereq1 = basic_off_flag("prereq1");
568 let prereq2 = basic_flag("prereq2");
569
570 let toplevel =
571 basic_flag_with_prereqs_and_visibility("toplevel", &["prereq1", "prereq2"], true);
572
573 store
574 .upsert("prereq1", PatchTarget::Flag(StorageItem::Item(prereq1)))
575 .expect("patch should apply");
576
577 store
578 .upsert("prereq2", PatchTarget::Flag(StorageItem::Item(prereq2)))
579 .expect("patch should apply");
580
581 store
582 .upsert("toplevel", PatchTarget::Flag(StorageItem::Item(toplevel)))
583 .expect("patch should apply");
584
585 let mut flag_detail = FlagDetail::new(true);
586
587 flag_detail.populate(&store, &context, FlagDetailConfig::new());
588
589 let expected = json!({
590 "prereq1": null,
591 "prereq2": true,
592 "toplevel": false,
593 "$flagsState": {
594 "toplevel": {
595 "version": 42,
596 "variation": 0,
597 "prerequisites": ["prereq1"]
598 },
599 "prereq2": {
600 "version": 42,
601 "variation": 1
602 },
603 "prereq1": {
604 "version": 42
605 }
606
607 },
608 "$valid": true
609 });
610
611 assert_json_eq!(expected, flag_detail);
612 }
613}