1use super::stores::store::DataStore;
2use bitflags::bitflags;
3use serde::Serialize;
4use std::cell::RefCell;
5
6use launchdarkly_server_sdk_evaluation::{
7 evaluate, Context, FlagValue, PrerequisiteEvent, PrerequisiteEventRecorder, Reason,
8};
9use std::collections::HashMap;
10use std::time::SystemTime;
11
12bitflags! {
13 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
33 pub struct FlagFilter: u8 {
34 const CLIENT = 0b01;
37
38 const MOBILE = 0b10;
41 }
42}
43
44impl Default for FlagFilter {
45 fn default() -> Self {
46 Self::empty()
48 }
49}
50
51#[derive(Clone, Copy, Default)]
65pub struct FlagDetailConfig {
66 flag_filter: FlagFilter,
67 with_reasons: bool,
68 details_only_for_tracked_flags: bool,
69}
70
71impl FlagDetailConfig {
72 pub fn new() -> Self {
76 Self {
77 flag_filter: FlagFilter::default(),
78 with_reasons: false,
79 details_only_for_tracked_flags: false,
80 }
81 }
82
83 pub fn flag_filter(&mut self, filter: FlagFilter) -> &mut Self {
88 self.flag_filter = filter;
89 self
90 }
91
92 pub fn with_reasons(&mut self) -> &mut Self {
94 self.with_reasons = true;
95 self
96 }
97
98 pub fn details_only_for_tracked_flags(&mut self) -> &mut Self {
101 self.details_only_for_tracked_flags = true;
102 self
103 }
104}
105
106#[derive(Serialize, Default, Debug, Clone)]
107#[serde(rename_all = "camelCase")]
108pub struct FlagState {
109 #[serde(skip_serializing_if = "Option::is_none")]
110 version: Option<u64>,
111
112 #[serde(skip_serializing_if = "Option::is_none")]
113 variation: Option<isize>,
114
115 #[serde(skip_serializing_if = "Option::is_none")]
116 reason: Option<Reason>,
117
118 #[serde(skip_serializing_if = "std::ops::Not::not")]
119 track_events: bool,
120
121 #[serde(skip_serializing_if = "std::ops::Not::not")]
122 track_reason: bool,
123
124 #[serde(skip_serializing_if = "Option::is_none")]
125 debug_events_until_date: Option<u64>,
126
127 #[serde(skip_serializing_if = "Vec::is_empty")]
128 prerequisites: Vec<String>,
129}
130
131#[derive(Serialize, Clone, Debug)]
137pub struct FlagDetail {
138 #[serde(flatten)]
139 evaluations: HashMap<String, Option<FlagValue>>,
140
141 #[serde(rename = "$flagsState")]
142 flag_state: HashMap<String, FlagState>,
143
144 #[serde(rename = "$valid")]
145 valid: bool,
146}
147
148struct DirectPrerequisiteRecorder {
151 target_flag_key: String,
152 prerequisites: RefCell<Vec<String>>,
153}
154
155impl DirectPrerequisiteRecorder {
156 pub fn new(target_flag_key: impl Into<String>) -> Self {
160 Self {
161 target_flag_key: target_flag_key.into(),
162 prerequisites: RefCell::new(Vec::new()),
163 }
164 }
165}
166impl PrerequisiteEventRecorder for DirectPrerequisiteRecorder {
167 fn record(&self, event: PrerequisiteEvent) {
168 if event.target_flag_key == self.target_flag_key {
169 self.prerequisites
170 .borrow_mut()
171 .push(event.prerequisite_flag.key)
172 }
173 }
174}
175
176impl FlagDetail {
177 pub fn new(valid: bool) -> Self {
179 Self {
180 evaluations: HashMap::new(),
181 flag_state: HashMap::new(),
182 valid,
183 }
184 }
185
186 pub fn populate(&mut self, store: &dyn DataStore, context: &Context, config: FlagDetailConfig) {
189 let mut evaluations = HashMap::new();
190 let mut flag_state = HashMap::new();
191
192 for (key, flag) in store.all_flags() {
193 if !config.flag_filter.is_empty() {
194 let matches_filter = (config.flag_filter.contains(FlagFilter::CLIENT)
195 && flag.using_environment_id())
196 || (config.flag_filter.contains(FlagFilter::MOBILE) && flag.using_mobile_key());
197
198 if !matches_filter {
199 continue;
200 }
201 }
202
203 let event_recorder = DirectPrerequisiteRecorder::new(key.clone());
204
205 let detail = evaluate(store.to_store(), &flag, context, Some(&event_recorder));
206
207 let require_experiment_data = flag.is_experimentation_enabled(&detail.reason);
211 let track_events = flag.track_events || require_experiment_data;
212 let track_reason = require_experiment_data;
213
214 let currently_debugging = match flag.debug_events_until_date {
215 Some(time) => {
216 let today = SystemTime::now();
217 let today_millis = today
218 .duration_since(SystemTime::UNIX_EPOCH)
219 .unwrap()
220 .as_millis();
221 (time as u128) > today_millis
222 }
223 None => false,
224 };
225
226 let mut omit_details = false;
227 if config.details_only_for_tracked_flags
228 && !(track_events
229 || track_reason
230 || flag.debug_events_until_date.is_some() && currently_debugging)
231 {
232 omit_details = true;
233 }
234
235 let mut reason = if !config.with_reasons && !track_reason {
236 None
237 } else {
238 Some(detail.reason)
239 };
240
241 let mut version = Some(flag.version);
242 if omit_details {
243 reason = None;
244 version = None;
245 }
246
247 evaluations.insert(key.clone(), detail.value.cloned());
248
249 flag_state.insert(
250 key,
251 FlagState {
252 version,
253 variation: detail.variation_index,
254 reason,
255 track_events,
256 track_reason,
257 debug_events_until_date: flag.debug_events_until_date,
258 prerequisites: event_recorder.prerequisites.take(),
259 },
260 );
261 }
262
263 self.evaluations = evaluations;
264 self.flag_state = flag_state;
265 }
266}
267
268#[cfg(test)]
269mod tests {
270 use crate::evaluation::{FlagDetail, FlagFilter};
271 use crate::stores::store::DataStore;
272 use crate::stores::store::InMemoryDataStore;
273 use crate::stores::store_types::{PatchTarget, StorageItem};
274 use crate::test_common::{
275 basic_flag, basic_flag_with_prereqs_and_visibility, basic_flag_with_visibility,
276 basic_off_flag,
277 };
278 use crate::FlagDetailConfig;
279 use assert_json_diff::assert_json_eq;
280 use launchdarkly_server_sdk_evaluation::ContextBuilder;
281 use test_case::test_case;
282
283 #[test]
284 fn flag_detail_handles_default_configuration() {
285 let context = ContextBuilder::new("bob")
286 .build()
287 .expect("Failed to create context");
288 let mut store = InMemoryDataStore::new();
289
290 store
291 .upsert(
292 "myFlag",
293 PatchTarget::Flag(StorageItem::Item(basic_flag("myFlag"))),
294 )
295 .expect("patch should apply");
296
297 let mut flag_detail = FlagDetail::new(true);
298 flag_detail.populate(&store, &context, FlagDetailConfig::new());
299
300 let expected = json!({
301 "myFlag": true,
302 "$flagsState": {
303 "myFlag": {
304 "version": 42,
305 "variation": 1
306 }
307 },
308 "$valid": true
309 });
310
311 assert_eq!(
312 serde_json::to_string_pretty(&flag_detail).unwrap(),
313 serde_json::to_string_pretty(&expected).unwrap(),
314 );
315 }
316
317 #[test]
318 fn flag_detail_handles_experimentation_reasons_correctly() {
319 let context = ContextBuilder::new("bob")
320 .build()
321 .expect("Failed to create context");
322 let mut store = InMemoryDataStore::new();
323
324 let mut flag = basic_flag("myFlag");
325 flag.track_events = false;
326 flag.track_events_fallthrough = true;
327
328 store
329 .upsert("myFlag", PatchTarget::Flag(StorageItem::Item(flag)))
330 .expect("patch should apply");
331
332 let mut flag_detail = FlagDetail::new(true);
333 flag_detail.populate(&store, &context, FlagDetailConfig::new());
334
335 let expected = json!({
336 "myFlag": true,
337 "$flagsState": {
338 "myFlag": {
339 "version": 42,
340 "variation": 1,
341 "reason": {
342 "kind": "FALLTHROUGH",
343 },
344 "trackEvents": true,
345 "trackReason": true,
346 }
347 },
348 "$valid": true
349 });
350
351 assert_eq!(
352 serde_json::to_string_pretty(&flag_detail).unwrap(),
353 serde_json::to_string_pretty(&expected).unwrap(),
354 );
355 }
356
357 #[test]
358 fn flag_detail_with_reasons_should_include_reason() {
359 let context = ContextBuilder::new("bob")
360 .build()
361 .expect("Failed to create context");
362 let mut store = InMemoryDataStore::new();
363
364 store
365 .upsert(
366 "myFlag",
367 PatchTarget::Flag(StorageItem::Item(basic_flag("myFlag"))),
368 )
369 .expect("patch should apply");
370
371 let mut config = FlagDetailConfig::new();
372 config.with_reasons();
373
374 let mut flag_detail = FlagDetail::new(true);
375 flag_detail.populate(&store, &context, config);
376
377 let expected = json!({
378 "myFlag": true,
379 "$flagsState": {
380 "myFlag": {
381 "version": 42,
382 "variation": 1,
383 "reason": {
384 "kind": "FALLTHROUGH"
385 }
386 }
387 },
388 "$valid": true
389 });
390
391 assert_eq!(
392 serde_json::to_string_pretty(&flag_detail).unwrap(),
393 serde_json::to_string_pretty(&expected).unwrap(),
394 );
395 }
396
397 #[test]
398 fn flag_detail_details_only_should_exclude_reason() {
399 let context = ContextBuilder::new("bob")
400 .build()
401 .expect("Failed to create context");
402 let mut store = InMemoryDataStore::new();
403
404 store
405 .upsert(
406 "myFlag",
407 PatchTarget::Flag(StorageItem::Item(basic_flag("myFlag"))),
408 )
409 .expect("patch should apply");
410
411 let mut config = FlagDetailConfig::new();
412 config.details_only_for_tracked_flags();
413
414 let mut flag_detail = FlagDetail::new(true);
415 flag_detail.populate(&store, &context, config);
416
417 let expected = json!({
418 "myFlag": true,
419 "$flagsState": {
420 "myFlag": {
421 "variation": 1,
422 }
423 },
424 "$valid": true
425 });
426
427 assert_eq!(
428 serde_json::to_string_pretty(&flag_detail).unwrap(),
429 serde_json::to_string_pretty(&expected).unwrap(),
430 );
431 }
432
433 #[test]
434 fn flag_detail_details_only_with_tracked_events_includes_version() {
435 let context = ContextBuilder::new("bob")
436 .build()
437 .expect("Failed to create context");
438 let mut store = InMemoryDataStore::new();
439 let mut flag = basic_flag("myFlag");
440 flag.track_events = true;
441
442 store
443 .upsert("myFlag", PatchTarget::Flag(StorageItem::Item(flag)))
444 .expect("patch should apply");
445
446 let mut config = FlagDetailConfig::new();
447 config.details_only_for_tracked_flags();
448
449 let mut flag_detail = FlagDetail::new(true);
450 flag_detail.populate(&store, &context, config);
451
452 let expected = json!({
453 "myFlag": true,
454 "$flagsState": {
455 "myFlag": {
456 "version": 42,
457 "variation": 1,
458 "trackEvents": true,
459 }
460 },
461 "$valid": true
462 });
463
464 assert_eq!(
465 serde_json::to_string_pretty(&flag_detail).unwrap(),
466 serde_json::to_string_pretty(&expected).unwrap(),
467 );
468 }
469
470 #[test]
471 fn flag_detail_with_default_config_but_tracked_event_should_include_version() {
472 let context = ContextBuilder::new("bob")
473 .build()
474 .expect("Failed to create context");
475 let mut store = InMemoryDataStore::new();
476 let mut flag = basic_flag("myFlag");
477 flag.track_events = true;
478
479 store
480 .upsert("myFlag", PatchTarget::Flag(StorageItem::Item(flag)))
481 .expect("patch should apply");
482
483 let mut flag_detail = FlagDetail::new(true);
484 flag_detail.populate(&store, &context, FlagDetailConfig::new());
485
486 let expected = json!({
487 "myFlag": true,
488 "$flagsState": {
489 "myFlag": {
490 "version": 42,
491 "variation": 1,
492 "trackEvents": true,
493 }
494 },
495 "$valid": true
496 });
497
498 assert_eq!(
499 serde_json::to_string_pretty(&flag_detail).unwrap(),
500 serde_json::to_string_pretty(&expected).unwrap(),
501 );
502 }
503
504 #[test]
505 fn flag_prerequisites_should_be_exposed() {
506 let context = ContextBuilder::new("bob")
507 .build()
508 .expect("Failed to create context");
509 let mut store = InMemoryDataStore::new();
510
511 let prereq1 = basic_flag("prereq1");
512 let prereq2 = basic_flag("prereq2");
513 let toplevel = basic_flag_with_prereqs_and_visibility(
514 "toplevel",
515 &["prereq1", "prereq2"],
516 false,
517 false,
518 );
519
520 store
521 .upsert("prereq1", PatchTarget::Flag(StorageItem::Item(prereq1)))
522 .expect("patch should apply");
523
524 store
525 .upsert("prereq2", PatchTarget::Flag(StorageItem::Item(prereq2)))
526 .expect("patch should apply");
527
528 store
529 .upsert("toplevel", PatchTarget::Flag(StorageItem::Item(toplevel)))
530 .expect("patch should apply");
531
532 let mut flag_detail = FlagDetail::new(true);
533 flag_detail.populate(&store, &context, FlagDetailConfig::new());
534
535 let expected = json!({
536 "prereq1": true,
537 "prereq2": true,
538 "toplevel": true,
539 "$flagsState": {
540 "toplevel": {
541 "version": 42,
542 "variation": 1,
543 "prerequisites": ["prereq1", "prereq2"]
544 },
545 "prereq2": {
546 "version": 42,
547 "variation": 1
548 },
549 "prereq1": {
550 "version": 42,
551 "variation": 1,
552 },
553 },
554 "$valid": true
555 });
556
557 assert_json_eq!(expected, flag_detail);
558 }
559
560 #[test]
561 fn flag_prerequisites_should_be_exposed_even_if_not_available_to_clients() {
562 let context = ContextBuilder::new("bob")
563 .build()
564 .expect("Failed to create context");
565 let mut store = InMemoryDataStore::new();
566
567 let prereq1 = basic_flag_with_visibility("prereq1", false, false);
569 let prereq2 = basic_flag_with_visibility("prereq2", false, false);
570
571 let toplevel = basic_flag_with_prereqs_and_visibility(
573 "toplevel",
574 &["prereq1", "prereq2"],
575 true,
576 false,
577 );
578
579 store
580 .upsert("prereq1", PatchTarget::Flag(StorageItem::Item(prereq1)))
581 .expect("patch should apply");
582
583 store
584 .upsert("prereq2", PatchTarget::Flag(StorageItem::Item(prereq2)))
585 .expect("patch should apply");
586
587 store
588 .upsert("toplevel", PatchTarget::Flag(StorageItem::Item(toplevel)))
589 .expect("patch should apply");
590
591 let mut flag_detail = FlagDetail::new(true);
592
593 let mut config = FlagDetailConfig::new();
594 config.flag_filter(FlagFilter::CLIENT);
595
596 flag_detail.populate(&store, &context, config);
597
598 let expected = json!({
601 "toplevel": true,
602 "$flagsState": {
603 "toplevel": {
604 "version": 42,
605 "variation": 1,
606 "prerequisites": ["prereq1", "prereq2"]
607 },
608 },
609 "$valid": true
610 });
611
612 assert_json_eq!(expected, flag_detail);
613 }
614
615 #[test]
616 fn flag_prerequisites_should_be_in_evaluation_order() {
617 let context = ContextBuilder::new("bob")
618 .build()
619 .expect("Failed to create context");
620 let mut store = InMemoryDataStore::new();
621
622 let prereq1 = basic_off_flag("prereq1");
625 let prereq2 = basic_flag("prereq2");
626
627 let toplevel = basic_flag_with_prereqs_and_visibility(
628 "toplevel",
629 &["prereq1", "prereq2"],
630 true,
631 false,
632 );
633
634 store
635 .upsert("prereq1", PatchTarget::Flag(StorageItem::Item(prereq1)))
636 .expect("patch should apply");
637
638 store
639 .upsert("prereq2", PatchTarget::Flag(StorageItem::Item(prereq2)))
640 .expect("patch should apply");
641
642 store
643 .upsert("toplevel", PatchTarget::Flag(StorageItem::Item(toplevel)))
644 .expect("patch should apply");
645
646 let mut flag_detail = FlagDetail::new(true);
647
648 flag_detail.populate(&store, &context, FlagDetailConfig::new());
649
650 let expected = json!({
651 "prereq1": null,
652 "prereq2": true,
653 "toplevel": false,
654 "$flagsState": {
655 "toplevel": {
656 "version": 42,
657 "variation": 0,
658 "prerequisites": ["prereq1"]
659 },
660 "prereq2": {
661 "version": 42,
662 "variation": 1
663 },
664 "prereq1": {
665 "version": 42
666 }
667
668 },
669 "$valid": true
670 });
671
672 assert_json_eq!(expected, flag_detail);
673 }
674
675 #[test_case(FlagFilter::empty(), &["server-flag", "client-flag", "mobile-flag", "both-flag"] ; "empty filter includes all flags")]
676 #[test_case(FlagFilter::CLIENT, &["client-flag", "both-flag"] ; "client filter includes only client flags")]
677 #[test_case(FlagFilter::MOBILE, &["mobile-flag", "both-flag"] ; "mobile filter includes only mobile flags")]
678 #[test_case(FlagFilter::CLIENT | FlagFilter::MOBILE, &["client-flag", "mobile-flag", "both-flag"] ; "combined filter includes client or mobile flags")]
679 fn flag_filter_includes_correct_flags(filter: FlagFilter, expected_flags: &[&str]) {
680 let context = ContextBuilder::new("bob")
681 .build()
682 .expect("Failed to create context");
683 let mut store = InMemoryDataStore::new();
684
685 store
687 .upsert(
688 "server-flag",
689 PatchTarget::Flag(StorageItem::Item(basic_flag_with_visibility(
690 "server-flag",
691 false,
692 false,
693 ))),
694 )
695 .expect("patch should apply");
696
697 store
698 .upsert(
699 "client-flag",
700 PatchTarget::Flag(StorageItem::Item(basic_flag_with_visibility(
701 "client-flag",
702 true,
703 false,
704 ))),
705 )
706 .expect("patch should apply");
707
708 store
709 .upsert(
710 "mobile-flag",
711 PatchTarget::Flag(StorageItem::Item(basic_flag_with_visibility(
712 "mobile-flag",
713 false,
714 true,
715 ))),
716 )
717 .expect("patch should apply");
718
719 store
720 .upsert(
721 "both-flag",
722 PatchTarget::Flag(StorageItem::Item(basic_flag_with_visibility(
723 "both-flag",
724 true,
725 true,
726 ))),
727 )
728 .expect("patch should apply");
729
730 let mut flag_detail = FlagDetail::new(true);
731 let mut config = FlagDetailConfig::new();
732 if !filter.is_empty() {
733 config.flag_filter(filter);
734 }
735 flag_detail.populate(&store, &context, config);
736
737 for expected_flag in expected_flags {
739 assert!(
740 flag_detail.evaluations.contains_key(*expected_flag),
741 "Expected flag '{expected_flag}' to be present"
742 );
743 }
744
745 assert_eq!(
747 flag_detail.evaluations.len(),
748 expected_flags.len(),
749 "Expected {} flags, got {}",
750 expected_flags.len(),
751 flag_detail.evaluations.len()
752 );
753 }
754}