launchdarkly_server_sdk/
evaluation.rs

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/// Configuration struct to control the type of data returned from the [crate::Client::all_flags_detail]
12/// method. By default, each of the options default to false. However, you can selectively enable
13/// them by calling the appropriate functions.
14///
15/// ```
16/// # use launchdarkly_server_sdk::FlagDetailConfig;
17/// # fn main() {
18///     let mut config = FlagDetailConfig::new();
19///     config.client_side_only()
20///         .with_reasons()
21///         .details_only_for_tracked_flags();
22/// # }
23/// ```
24#[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    /// Create a [FlagDetailConfig] with default values.
33    ///
34    /// By default, this config will include al flags and will not include reasons.
35    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    /// Limit to only flags that are marked for use with the client-side SDK (by
44    /// default, all flags are included)
45    pub fn client_side_only(&mut self) -> &mut Self {
46        self.client_side_only = true;
47        self
48    }
49
50    /// Include evaluation reasons in the state
51    pub fn with_reasons(&mut self) -> &mut Self {
52        self.with_reasons = true;
53        self
54    }
55
56    /// Omit any metadata that is normally only used for event generation, such as flag versions
57    /// and evaluation reasons, unless the flag has event tracking or debugging turned on
58    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/// FlagDetail is a snapshot of the state of multiple feature flags with regard to a specific user.
90/// This is the return type of [crate::Client::all_flags_detail].
91///
92/// Serializing this object to JSON will produce the appropriate data structure for bootstrapping
93/// the LaunchDarkly JavaScript client.
94#[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
106/// DirectPrerequisiteRecorder records only the direct (top-level) prerequisites of a
107/// flag.
108struct DirectPrerequisiteRecorder {
109    target_flag_key: String,
110    prerequisites: RefCell<Vec<String>>,
111}
112
113impl DirectPrerequisiteRecorder {
114    /// Creates a new instance of [DirectPrerequisiteRecorder] for a given target flag. The
115    /// direct prerequisites of the flag will be available in the prerequisites field of the
116    /// recorder.
117    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    /// Create a new empty instance of FlagDetail.
136    pub fn new(valid: bool) -> Self {
137        Self {
138            evaluations: HashMap::new(),
139            flag_state: HashMap::new(),
140            valid,
141        }
142    }
143
144    /// Populate the FlagDetail struct with the results of every flag found within the provided
145    /// store, evaluated for the specified context.
146    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            // Here we are applying the same logic used in EventFactory.new_feature_request_event
160            // to determine whether the evaluation involved an experiment, in which case both
161            // track_events and track_reason should be overridden.
162            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        // These two prerequisites won't be visible to clients (environment ID) SDKs.
515        let prereq1 = basic_flag_with_visibility("prereq1", false);
516        let prereq2 = basic_flag_with_visibility("prereq2", false);
517
518        // But, the top-level flag will.
519        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        // Even though the two prereqs are omitted, we should still see their metadata in the
542        // toplevel flag.
543        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        // Since prereq1 will be listed as the first prerequisite, and it is off,
566        // evaluation will short circuit and we shouldn't see the second prerequisite.
567        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}