Skip to main content

launchdarkly_server_sdk/
evaluation.rs

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    /// Controls which flags are included based on their client-side availability settings.
14    ///
15    /// Use this with [FlagDetailConfig] to filter flags returned by [crate::Client::all_flags_detail].
16    ///
17    /// # Examples
18    ///
19    /// ```
20    /// # use launchdarkly_server_sdk::{FlagDetailConfig, FlagFilter};
21    /// // Include only web/JavaScript client flags
22    /// let mut config = FlagDetailConfig::new();
23    /// config.flag_filter(FlagFilter::CLIENT);
24    ///
25    /// // Include both web and mobile client flags
26    /// let mut config = FlagDetailConfig::new();
27    /// config.flag_filter(FlagFilter::CLIENT | FlagFilter::MOBILE);
28    ///
29    /// // Include all flags (default)
30    /// let config = FlagDetailConfig::new(); // empty filter = no filtering
31    /// ```
32    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
33    pub struct FlagFilter: u8 {
34        /// Include flags available to JavaScript/web client-side SDKs.
35        /// Filters to flags where `using_environment_id()` returns true.
36        const CLIENT = 0b01;
37
38        /// Include flags available to mobile/desktop client-side SDKs.
39        /// Filters to flags where `using_mobile_key()` returns true.
40        const MOBILE = 0b10;
41    }
42}
43
44impl Default for FlagFilter {
45    fn default() -> Self {
46        // Empty filter = include all flags (no filtering)
47        Self::empty()
48    }
49}
50
51/// Configuration struct to control the type of data returned from the [crate::Client::all_flags_detail]
52/// method. By default, each of the options default to false. However, you can selectively enable
53/// them by calling the appropriate functions.
54///
55/// ```
56/// # use launchdarkly_server_sdk::{FlagDetailConfig, FlagFilter};
57/// # fn main() {
58///     let mut config = FlagDetailConfig::new();
59///     config.flag_filter(FlagFilter::CLIENT)
60///         .with_reasons()
61///         .details_only_for_tracked_flags();
62/// # }
63/// ```
64#[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    /// Create a [FlagDetailConfig] with default values.
73    ///
74    /// By default, this config will include al flags and will not include reasons.
75    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    /// Set the flag filter to control which flags are included.
84    ///
85    /// Pass an empty filter (default) to include all flags.
86    /// Use `FlagFilter::CLIENT`, `FlagFilter::MOBILE`, or combine them.
87    pub fn flag_filter(&mut self, filter: FlagFilter) -> &mut Self {
88        self.flag_filter = filter;
89        self
90    }
91
92    /// Include evaluation reasons in the state
93    pub fn with_reasons(&mut self) -> &mut Self {
94        self.with_reasons = true;
95        self
96    }
97
98    /// Omit any metadata that is normally only used for event generation, such as flag versions
99    /// and evaluation reasons, unless the flag has event tracking or debugging turned on
100    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/// FlagDetail is a snapshot of the state of multiple feature flags with regard to a specific user.
132/// This is the return type of [crate::Client::all_flags_detail].
133///
134/// Serializing this object to JSON will produce the appropriate data structure for bootstrapping
135/// the LaunchDarkly JavaScript client.
136#[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
148/// DirectPrerequisiteRecorder records only the direct (top-level) prerequisites of a
149/// flag.
150struct DirectPrerequisiteRecorder {
151    target_flag_key: String,
152    prerequisites: RefCell<Vec<String>>,
153}
154
155impl DirectPrerequisiteRecorder {
156    /// Creates a new instance of [DirectPrerequisiteRecorder] for a given target flag. The
157    /// direct prerequisites of the flag will be available in the prerequisites field of the
158    /// recorder.
159    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    /// Create a new empty instance of FlagDetail.
178    pub fn new(valid: bool) -> Self {
179        Self {
180            evaluations: HashMap::new(),
181            flag_state: HashMap::new(),
182            valid,
183        }
184    }
185
186    /// Populate the FlagDetail struct with the results of every flag found within the provided
187    /// store, evaluated for the specified context.
188    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            // Here we are applying the same logic used in EventFactory.new_feature_request_event
208            // to determine whether the evaluation involved an experiment, in which case both
209            // track_events and track_reason should be overridden.
210            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        // These two prerequisites won't be visible to clients (environment ID) SDKs.
568        let prereq1 = basic_flag_with_visibility("prereq1", false, false);
569        let prereq2 = basic_flag_with_visibility("prereq2", false, false);
570
571        // But, the top-level flag will.
572        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        // Even though the two prereqs are omitted, we should still see their metadata in the
599        // toplevel flag.
600        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        // Since prereq1 will be listed as the first prerequisite, and it is off,
623        // evaluation will short circuit and we shouldn't see the second prerequisite.
624        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        // Add different types of flags
686        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        // Assert expected flags are present
738        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 count matches
746        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}