launchdarkly_server_sdk_evaluation/
flag.rs

1use std::convert::TryFrom;
2use std::fmt;
3
4use log::warn;
5use serde::de::{MapAccess, Visitor};
6use serde::{
7    ser::{SerializeMap, SerializeStruct},
8    Deserialize, Deserializer, Serialize, Serializer,
9};
10
11use crate::contexts::context::Kind;
12use crate::eval::{self, Detail, Reason};
13use crate::flag_value::FlagValue;
14use crate::rule::FlagRule;
15use crate::variation::{VariationIndex, VariationOrRollout};
16use crate::{BucketResult, Context, Versioned};
17
18/// Flag describes an individual feature flag.
19#[derive(Clone, Debug, Serialize, Deserialize)]
20#[serde(rename_all = "camelCase")]
21pub struct Flag {
22    /// The unique string key of the feature flag.
23    pub key: String,
24
25    /// Version is an integer that is incremented by LaunchDarkly every time the configuration of the flag is
26    /// changed.
27    #[serde(default)]
28    pub version: u64,
29
30    pub(crate) on: bool,
31
32    pub(crate) targets: Vec<Target>,
33
34    #[serde(default)]
35    pub(crate) context_targets: Vec<Target>,
36    pub(crate) rules: Vec<FlagRule>,
37    pub(crate) prerequisites: Vec<Prereq>,
38
39    pub(crate) fallthrough: VariationOrRollout,
40    pub(crate) off_variation: Option<VariationIndex>,
41    variations: Vec<FlagValue>,
42
43    /// Indicates whether a flag is available using each of the client-side authentication methods.
44    #[serde(flatten)]
45    client_visibility: ClientVisibility,
46
47    salt: String,
48
49    /// Used internally by the SDK analytics event system.
50    ///
51    /// This field is true if the current LaunchDarkly account has data export enabled, and has turned on
52    /// the "send detailed event information for this flag" option for this flag. This tells the SDK to
53    /// send full event data for each flag evaluation, rather than only aggregate data in a summary event.
54    ///
55    /// The launchdarkly-server-sdk-evaluation crate does not implement that behavior; it is only
56    /// in the data model for use by the SDK.
57    #[serde(default)]
58    pub track_events: bool,
59
60    /// Used internally by the SDK analytics event system.
61    ///
62    /// This field is true if the current LaunchDarkly account has experimentation enabled, has associated
63    /// this flag with an experiment, and has enabled "default rule" for the experiment. This tells the
64    /// SDK to send full event data for any evaluation where this flag had targeting turned on but the
65    /// context did not match any targets or rules.
66    ///
67    /// The launchdarkly-server-sdk-evaluation package does not implement that behavior; it is only
68    /// in the data model for use by the SDK.
69    #[serde(default)]
70    pub track_events_fallthrough: bool,
71
72    /// Used internally by the SDK analytics event system.
73    ///
74    /// This field is non-zero if debugging for this flag has been turned on temporarily in the
75    /// LaunchDarkly dashboard. Debugging always is for a limited time, so the field specifies a Unix
76    /// millisecond timestamp when this mode should expire. Until then, the SDK will send full event data
77    /// for each evaluation of this flag.
78    ///
79    /// The launchdarkly-server-sdk-evaluation package does not implement that behavior; it is only in the data
80    /// model for use by the SDK.
81    #[serde(default)]
82    pub debug_events_until_date: Option<u64>,
83
84    /// Contains migration-related flag parameters. If this flag is for migration purposes, this
85    /// property is guaranteed to be set.
86    #[serde(
87        default,
88        rename = "migration",
89        skip_serializing_if = "is_default_migration_settings"
90    )]
91    pub migration_settings: Option<MigrationFlagParameters>,
92
93    /// Controls the rate at which feature and debug events are emitted from the SDK for this
94    /// particular flag. If this value is not defined, it is assumed to be 1.
95    ///
96    /// LaunchDarkly may modify this value to prevent poorly performing applications from adversely
97    /// affecting upstream service health.
98    #[serde(default, skip_serializing_if = "is_default_ratio")]
99    pub sampling_ratio: Option<u32>,
100
101    /// Determines whether or not this flag will be excluded from the event summarization process.
102    ///
103    /// LaunchDarkly may change this value to prevent poorly performing applications from adversely
104    /// affecting upstream service health.
105    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
106    pub exclude_from_summaries: bool,
107}
108
109impl Versioned for Flag {
110    fn version(&self) -> u64 {
111        self.version
112    }
113}
114
115// Used strictly for serialization to determine if a ratio should be included in the JSON.
116fn is_default_ratio(sampling_ratio: &Option<u32>) -> bool {
117    sampling_ratio.unwrap_or(1) == 1
118}
119
120// Used strictly for serialization to determine if migration settings should be included in the JSON.
121fn is_default_migration_settings(settings: &Option<MigrationFlagParameters>) -> bool {
122    match settings {
123        Some(settings) => settings.is_default(),
124        None => true,
125    }
126}
127
128/// MigrationFlagParameters are used to control flag-specific migration configuration.
129#[derive(Clone, Debug, Serialize, Deserialize)]
130#[serde(rename_all = "camelCase")]
131pub struct MigrationFlagParameters {
132    /// Controls the rate at which consistency checks are performing during a migration-influenced
133    /// read or write operation. This value can be controlled through the LaunchDarkly UI and
134    /// propagated downstream to the SDKs.
135    #[serde(skip_serializing_if = "is_default_ratio")]
136    pub check_ratio: Option<u32>,
137}
138
139impl MigrationFlagParameters {
140    fn is_default(&self) -> bool {
141        is_default_ratio(&self.check_ratio)
142    }
143}
144
145#[derive(Clone, Debug)]
146struct ClientVisibility {
147    client_side_availability: ClientSideAvailability,
148}
149
150impl<'de> Deserialize<'de> for ClientVisibility {
151    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
152    where
153        D: Deserializer<'de>,
154    {
155        #[derive(Deserialize)]
156        #[serde(field_identifier, rename_all = "camelCase")]
157        enum Field {
158            ClientSide,
159            ClientSideAvailability,
160        }
161
162        struct ClientVisibilityVisitor;
163
164        impl<'de> Visitor<'de> for ClientVisibilityVisitor {
165            type Value = ClientVisibility;
166
167            fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
168                formatter.write_str("struct ClientVisibility")
169            }
170
171            fn visit_map<V>(self, mut map: V) -> Result<ClientVisibility, V::Error>
172            where
173                V: MapAccess<'de>,
174            {
175                let mut client_side = None;
176                let mut client_side_availability: Option<ClientSideAvailability> = None;
177
178                while let Some(k) = map.next_key()? {
179                    match k {
180                        Field::ClientSide => client_side = Some(map.next_value()?),
181                        Field::ClientSideAvailability => {
182                            client_side_availability = Some(map.next_value()?)
183                        }
184                    }
185                }
186
187                let client_side_availability = match client_side_availability {
188                    Some(mut csa) => {
189                        csa.explicit = true;
190                        csa
191                    }
192                    _ => ClientSideAvailability {
193                        using_environment_id: client_side.unwrap_or_default(),
194                        using_mobile_key: true,
195                        explicit: false,
196                    },
197                };
198
199                Ok(ClientVisibility {
200                    client_side_availability,
201                })
202            }
203        }
204
205        const FIELDS: &[&str] = &["clientSide", "clientSideAvailability"];
206        deserializer.deserialize_struct("ClientVisibility", FIELDS, ClientVisibilityVisitor)
207    }
208}
209
210impl Serialize for ClientVisibility {
211    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
212    where
213        S: Serializer,
214    {
215        if self.client_side_availability.explicit {
216            let mut state = serializer.serialize_struct("ClientSideAvailability", 1)?;
217            state.serialize_field("clientSideAvailability", &self.client_side_availability)?;
218            state.end()
219        } else {
220            let mut map = serializer.serialize_map(Some(1))?;
221            map.serialize_entry(
222                "clientSide",
223                &self.client_side_availability.using_environment_id,
224            )?;
225            map.end()
226        }
227    }
228}
229
230/// Prereq describes a requirement that another feature flag return a specific variation.
231///
232/// A prerequisite condition is met if the specified prerequisite flag has targeting turned on and
233/// returns the specified variation.
234#[derive(Clone, Debug, Serialize, Deserialize)]
235pub struct Prereq {
236    pub(crate) key: String,
237    pub(crate) variation: VariationIndex,
238}
239
240#[derive(Clone, Debug, Serialize, Deserialize)]
241#[serde(rename_all = "camelCase")]
242pub(crate) struct Target {
243    #[serde(default)]
244    pub(crate) context_kind: Kind,
245
246    pub(crate) values: Vec<String>,
247    pub(crate) variation: VariationIndex,
248}
249
250/// ClientSideAvailability describes whether a flag is available to client-side SDKs.
251///
252/// This field can be used by a server-side client to determine whether to include an individual flag in
253/// bootstrapped set of flag data (see [Bootstrapping the Javascript SDK](https://docs.launchdarkly.com/sdk/client-side/javascript#bootstrapping)).
254#[derive(Clone, Debug, Serialize, Deserialize)]
255#[serde(rename_all = "camelCase")]
256pub struct ClientSideAvailability {
257    /// Indicates that this flag is available to clients using the mobile key for
258    /// authorization (includes most desktop and mobile clients).
259    pub using_mobile_key: bool,
260    /// Indicates that this flag is available to clients using the environment
261    /// id to identify an environment (includes client-side javascript clients).
262    pub using_environment_id: bool,
263
264    // This field determines if ClientSideAvailability was explicitly included in the JSON payload.
265    //
266    // If it was, we will use the properities of this new schema over the dated
267    // [ClientVisibility::client_side] field.
268    #[serde(skip)]
269    explicit: bool,
270}
271
272impl Flag {
273    /// Generate a [crate::Detail] response with the given variation and reason.
274    pub fn variation(&self, index: VariationIndex, reason: Reason) -> Detail<&FlagValue> {
275        let (value, variation_index) = match usize::try_from(index) {
276            Ok(u) => (self.variations.get(u), Some(index)),
277            Err(e) => {
278                warn!(
279                    "Flag variation index could not be converted to usize. {}",
280                    e
281                );
282                (None, None)
283            }
284        };
285
286        Detail {
287            value,
288            variation_index,
289            reason,
290        }
291        .should_have_value(eval::Error::MalformedFlag)
292    }
293
294    /// Generate a [crate::Detail] response using the flag's off variation.
295    ///
296    /// If a flag has an off_variation specified, a [crate::Detail] will be created using that
297    /// variation. If the flag does not have an off_variation specified, an empty [crate::Detail]
298    /// will be returned. See [crate::Detail::empty].
299    pub fn off_value(&self, reason: Reason) -> Detail<&FlagValue> {
300        match self.off_variation {
301            Some(index) => self.variation(index, reason),
302            None => Detail::empty(reason),
303        }
304    }
305
306    /// Indicates that this flag is available to clients using the environment id to identify an
307    /// environment (includes client-side javascript clients).
308    pub fn using_environment_id(&self) -> bool {
309        self.client_visibility
310            .client_side_availability
311            .using_environment_id
312    }
313
314    /// Indicates that this flag is available to clients using the mobile key for authorization
315    /// (includes most desktop and mobile clients).
316    pub fn using_mobile_key(&self) -> bool {
317        self.client_visibility
318            .client_side_availability
319            .using_mobile_key
320    }
321
322    pub(crate) fn resolve_variation_or_rollout(
323        &self,
324        vr: &VariationOrRollout,
325        context: &Context,
326    ) -> Result<BucketResult, eval::Error> {
327        vr.variation(&self.key, context, &self.salt)
328            .map_err(|_| eval::Error::MalformedFlag)?
329            .ok_or(eval::Error::MalformedFlag)
330    }
331
332    /// Returns true if, based on the [crate::Reason] returned by the flag evaluation, an event for
333    /// that evaluation should have full tracking enabled and always report the reason even if the
334    /// application didn't explicitly request this. For instance, this is true if a rule was
335    /// matched that had tracking enabled for that specific rule.
336    pub fn is_experimentation_enabled(&self, reason: &Reason) -> bool {
337        match reason {
338            _ if reason.is_in_experiment() => true,
339            Reason::Fallthrough { .. } => self.track_events_fallthrough,
340            Reason::RuleMatch { rule_index, .. } => self
341                .rules
342                .get(*rule_index)
343                .map(|rule| rule.track_events)
344                .unwrap_or(false),
345            _ => false,
346        }
347    }
348
349    #[cfg(test)]
350    pub(crate) fn new_boolean_flag_with_segment_match(segment_keys: Vec<&str>, kind: Kind) -> Self {
351        Self {
352            key: "feature".to_string(),
353            version: 1,
354            on: true,
355            targets: vec![],
356            rules: vec![FlagRule::new_segment_match(segment_keys, kind)],
357            prerequisites: vec![],
358            fallthrough: VariationOrRollout::Variation { variation: 0 },
359            off_variation: Some(0),
360            variations: vec![FlagValue::Bool(false), FlagValue::Bool(true)],
361            client_visibility: ClientVisibility {
362                client_side_availability: ClientSideAvailability {
363                    using_mobile_key: false,
364                    using_environment_id: false,
365                    explicit: true,
366                },
367            },
368            salt: "xyz".to_string(),
369            track_events: false,
370            track_events_fallthrough: false,
371            debug_events_until_date: None,
372            context_targets: vec![],
373            migration_settings: None,
374            sampling_ratio: None,
375            exclude_from_summaries: false,
376        }
377    }
378}
379
380#[cfg(test)]
381mod tests {
382    use crate::store::Store;
383    use crate::test_common::TestStore;
384    use crate::MigrationFlagParameters;
385    use spectral::prelude::*;
386
387    use super::Flag;
388    use crate::eval::Reason::*;
389    use test_case::test_case;
390
391    #[test_case(true)]
392    #[test_case(false)]
393    fn handles_client_side_schema(client_side: bool) {
394        let json = &format!(
395            r#"{{
396            "key": "flag",
397            "version": 42,
398            "on": false,
399            "targets": [],
400            "rules": [],
401            "prerequisites": [],
402            "fallthrough": {{"variation": 1}},
403            "offVariation": 0,
404            "variations": [false, true],
405            "clientSide": {},
406            "salt": "salty"
407        }}"#,
408            client_side
409        );
410
411        let flag: Flag = serde_json::from_str(json).unwrap();
412        let client_side_availability = &flag.client_visibility.client_side_availability;
413        assert_eq!(client_side_availability.using_environment_id, client_side);
414        assert!(client_side_availability.using_mobile_key);
415        assert!(!client_side_availability.explicit);
416
417        assert_eq!(flag.using_environment_id(), client_side);
418    }
419
420    #[test_case(true)]
421    #[test_case(false)]
422    fn can_deserialize_and_reserialize_to_old_schema(client_side: bool) {
423        let json = &format!(
424            r#"{{
425  "key": "flag",
426  "version": 42,
427  "on": false,
428  "targets": [],
429  "contextTargets": [],
430  "rules": [],
431  "prerequisites": [],
432  "fallthrough": {{
433    "variation": 1
434  }},
435  "offVariation": 0,
436  "variations": [
437    false,
438    true
439  ],
440  "clientSide": {},
441  "salt": "salty",
442  "trackEvents": false,
443  "trackEventsFallthrough": false,
444  "debugEventsUntilDate": null
445}}"#,
446            client_side
447        );
448
449        let flag: Flag = serde_json::from_str(json).unwrap();
450        let restored = serde_json::to_string_pretty(&flag).unwrap();
451
452        assert_eq!(json, &restored);
453    }
454
455    #[test_case(true)]
456    #[test_case(false)]
457    fn handles_client_side_availability_schema(using_environment_id: bool) {
458        let json = &format!(
459            r#"{{
460            "key": "flag",
461            "version": 42,
462            "on": false,
463            "targets": [],
464            "rules": [],
465            "prerequisites": [],
466            "fallthrough": {{"variation": 1}},
467            "offVariation": 0,
468            "variations": [false, true],
469            "clientSideAvailability": {{
470                "usingEnvironmentId": {},
471                "usingMobileKey": false
472            }},
473            "salt": "salty"
474        }}"#,
475            using_environment_id
476        );
477
478        let flag: Flag = serde_json::from_str(json).unwrap();
479        let client_side_availability = &flag.client_visibility.client_side_availability;
480        assert_eq!(
481            client_side_availability.using_environment_id,
482            using_environment_id
483        );
484        assert!(!client_side_availability.using_mobile_key);
485        assert!(client_side_availability.explicit);
486
487        assert_eq!(flag.using_environment_id(), using_environment_id);
488    }
489
490    #[test_case(true)]
491    #[test_case(false)]
492    fn handles_context_target_schema(using_environment_id: bool) {
493        let json = &format!(
494            r#"{{
495            "key": "flag",
496            "version": 42,
497            "on": false,
498            "targets": [{{
499                "values": ["Bob"],
500                "variation": 1
501            }}],
502            "contextTargets": [{{
503                "contextKind": "org",
504                "values": ["LaunchDarkly"],
505                "variation": 0
506            }}],
507            "rules": [],
508            "prerequisites": [],
509            "fallthrough": {{"variation": 1}},
510            "offVariation": 0,
511            "variations": [false, true],
512            "clientSideAvailability": {{
513                "usingEnvironmentId": {},
514                "usingMobileKey": false
515            }},
516            "salt": "salty"
517        }}"#,
518            using_environment_id
519        );
520
521        let flag: Flag = serde_json::from_str(json).unwrap();
522        assert_eq!(1, flag.targets.len());
523        assert!(flag.targets[0].context_kind.is_user());
524
525        assert_eq!(1, flag.context_targets.len());
526        assert_eq!("org", flag.context_targets[0].context_kind.as_ref());
527    }
528
529    #[test]
530    fn getting_variation_with_invalid_index_is_handled_appropriately() {
531        let store = TestStore::new();
532        let flag = store.flag("flag").unwrap();
533
534        let detail = flag.variation(-1, Off);
535
536        assert!(detail.value.is_none());
537        assert!(detail.variation_index.is_none());
538        assert_eq!(
539            detail.reason,
540            Error {
541                error: crate::Error::MalformedFlag
542            }
543        );
544    }
545
546    #[test_case(true, true)]
547    #[test_case(true, false)]
548    #[test_case(false, true)]
549    #[test_case(false, false)]
550    fn can_deserialize_and_reserialize_to_new_schema(
551        using_environment_id: bool,
552        using_mobile_key: bool,
553    ) {
554        let json = &format!(
555            r#"{{
556  "key": "flag",
557  "version": 42,
558  "on": false,
559  "targets": [],
560  "contextTargets": [],
561  "rules": [],
562  "prerequisites": [],
563  "fallthrough": {{
564    "variation": 1
565  }},
566  "offVariation": 0,
567  "variations": [
568    false,
569    true
570  ],
571  "clientSideAvailability": {{
572    "usingMobileKey": {},
573    "usingEnvironmentId": {}
574  }},
575  "salt": "salty",
576  "trackEvents": false,
577  "trackEventsFallthrough": false,
578  "debugEventsUntilDate": null
579}}"#,
580            using_environment_id, using_mobile_key
581        );
582
583        let flag: Flag = serde_json::from_str(json).unwrap();
584        let restored = serde_json::to_string_pretty(&flag).unwrap();
585
586        assert_eq!(json, &restored);
587    }
588
589    #[test]
590    fn is_experimentation_enabled() {
591        let store = TestStore::new();
592
593        let flag = store.flag("flag").unwrap();
594        asserting!("defaults to false")
595            .that(&flag.is_experimentation_enabled(&Off))
596            .is_false();
597        asserting!("false for fallthrough if trackEventsFallthrough is false")
598            .that(&flag.is_experimentation_enabled(&Fallthrough {
599                in_experiment: false,
600            }))
601            .is_false();
602
603        let flag = store.flag("flagWithRuleExclusion").unwrap();
604        asserting!("true for fallthrough if trackEventsFallthrough is true")
605            .that(&flag.is_experimentation_enabled(&Fallthrough {
606                in_experiment: false,
607            }))
608            .is_true();
609        asserting!("true for rule if rule.trackEvents is true")
610            .that(&flag.is_experimentation_enabled(&RuleMatch {
611                rule_index: 0,
612                rule_id: flag.rules.first().unwrap().id.clone(),
613                in_experiment: false,
614            }))
615            .is_true();
616
617        let flag = store.flag("flagWithExperiment").unwrap();
618        asserting!("true for fallthrough if reason says it is")
619            .that(&flag.is_experimentation_enabled(&Fallthrough {
620                in_experiment: true,
621            }))
622            .is_true();
623        asserting!("false for fallthrough if reason says it is")
624            .that(&flag.is_experimentation_enabled(&Fallthrough {
625                in_experiment: false,
626            }))
627            .is_false();
628        // note this flag doesn't even have a rule - doesn't matter, we go by the reason
629        asserting!("true for rule if reason says it is")
630            .that(&flag.is_experimentation_enabled(&RuleMatch {
631                rule_index: 42,
632                rule_id: "lol".into(),
633                in_experiment: true,
634            }))
635            .is_true();
636        asserting!("false for rule if reason says it is")
637            .that(&flag.is_experimentation_enabled(&RuleMatch {
638                rule_index: 42,
639                rule_id: "lol".into(),
640                in_experiment: false,
641            }))
642            .is_false();
643    }
644
645    #[test]
646    fn sampling_ratio_is_ignored_appropriately() {
647        let store = TestStore::new();
648        let mut flag = store.flag("flag").unwrap();
649
650        flag.sampling_ratio = Some(42);
651        let with_low_sampling_ratio = serde_json::to_string_pretty(&flag).unwrap();
652        assert!(with_low_sampling_ratio.contains("\"samplingRatio\": 42"));
653
654        flag.sampling_ratio = Some(1);
655        let with_highest_ratio = serde_json::to_string_pretty(&flag).unwrap();
656        assert!(!with_highest_ratio.contains("\"samplingRatio\""));
657
658        flag.sampling_ratio = None;
659        let with_no_ratio = serde_json::to_string_pretty(&flag).unwrap();
660        assert!(!with_no_ratio.contains("\"samplingRatio\""));
661    }
662
663    #[test]
664    fn exclude_from_summaries_is_ignored_appropriately() {
665        let store = TestStore::new();
666        let mut flag = store.flag("flag").unwrap();
667
668        flag.exclude_from_summaries = true;
669        let with_exclude = serde_json::to_string_pretty(&flag).unwrap();
670        assert!(with_exclude.contains("\"excludeFromSummaries\": true"));
671
672        flag.exclude_from_summaries = false;
673        let without_exclude = serde_json::to_string_pretty(&flag).unwrap();
674        assert!(!without_exclude.contains("\"excludeFromSummaries\""));
675    }
676
677    #[test]
678    fn migration_settings_included_appropriately() {
679        let store = TestStore::new();
680        let mut flag = store.flag("flag").unwrap();
681
682        flag.migration_settings = None;
683        let without_migration_settings = serde_json::to_string_pretty(&flag).unwrap();
684        assert!(!without_migration_settings.contains("\"migration\""));
685
686        flag.migration_settings = Some(MigrationFlagParameters { check_ratio: None });
687        let without_empty_migration_settings = serde_json::to_string_pretty(&flag).unwrap();
688        assert!(!without_empty_migration_settings.contains("\"migration\""));
689
690        flag.migration_settings = Some(MigrationFlagParameters {
691            check_ratio: Some(1),
692        });
693        let with_default_ratio = serde_json::to_string_pretty(&flag).unwrap();
694        assert!(!with_default_ratio.contains("\"migration\""));
695
696        flag.migration_settings = Some(MigrationFlagParameters {
697            check_ratio: Some(42),
698        });
699        let with_specific_ratio = serde_json::to_string_pretty(&flag).unwrap();
700        assert!(with_specific_ratio.contains("\"migration\": {"));
701        assert!(with_specific_ratio.contains("\"checkRatio\": 42"));
702    }
703}