Skip to main content

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    pub(crate) variations: Vec<FlagValue>,
42
43    /// Indicates whether a flag is available using each of the client-side authentication methods.
44    #[serde(flatten)]
45    pub(crate) client_visibility: ClientVisibility,
46
47    pub(crate) 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, Default)]
146pub(crate) struct ClientVisibility {
147    pub(crate) 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")]
256#[derive(Default)]
257pub struct ClientSideAvailability {
258    /// Indicates that this flag is available to clients using the mobile key for
259    /// authorization (includes most desktop and mobile clients).
260    pub using_mobile_key: bool,
261    /// Indicates that this flag is available to clients using the environment
262    /// id to identify an environment (includes client-side javascript clients).
263    pub using_environment_id: bool,
264
265    // This field determines if ClientSideAvailability was explicitly included in the JSON payload.
266    //
267    // If it was, we will use the properities of this new schema over the dated
268    // [ClientVisibility::client_side] field.
269    #[serde(skip)]
270    explicit: bool,
271}
272
273impl Flag {
274    /// Generate a [crate::Detail] response with the given variation and reason.
275    pub fn variation(&self, index: VariationIndex, reason: Reason) -> Detail<&FlagValue> {
276        let (value, variation_index) = match usize::try_from(index) {
277            Ok(u) => (self.variations.get(u), Some(index)),
278            Err(e) => {
279                warn!("Flag variation index could not be converted to usize. {e}");
280                (None, None)
281            }
282        };
283
284        Detail {
285            value,
286            variation_index,
287            reason,
288        }
289        .should_have_value(eval::Error::MalformedFlag)
290    }
291
292    /// Generate a [crate::Detail] response using the flag's off variation.
293    ///
294    /// If a flag has an off_variation specified, a [crate::Detail] will be created using that
295    /// variation. If the flag does not have an off_variation specified, an empty [crate::Detail]
296    /// will be returned. See [crate::Detail::empty].
297    pub fn off_value(&self, reason: Reason) -> Detail<&FlagValue> {
298        match self.off_variation {
299            Some(index) => self.variation(index, reason),
300            None => Detail::empty(reason),
301        }
302    }
303
304    /// Indicates that this flag is available to clients using the environment id to identify an
305    /// environment (includes client-side javascript clients).
306    pub fn using_environment_id(&self) -> bool {
307        self.client_visibility
308            .client_side_availability
309            .using_environment_id
310    }
311
312    /// Indicates that this flag is available to clients using the mobile key for authorization
313    /// (includes most desktop and mobile clients).
314    pub fn using_mobile_key(&self) -> bool {
315        self.client_visibility
316            .client_side_availability
317            .using_mobile_key
318    }
319
320    pub(crate) fn resolve_variation_or_rollout(
321        &self,
322        vr: &VariationOrRollout,
323        context: &Context,
324    ) -> Result<BucketResult, eval::Error> {
325        vr.variation(&self.key, context, &self.salt)
326            .map_err(|_| eval::Error::MalformedFlag)?
327            .ok_or(eval::Error::MalformedFlag)
328    }
329
330    /// Returns true if, based on the [crate::Reason] returned by the flag evaluation, an event for
331    /// that evaluation should have full tracking enabled and always report the reason even if the
332    /// application didn't explicitly request this. For instance, this is true if a rule was
333    /// matched that had tracking enabled for that specific rule.
334    pub fn is_experimentation_enabled(&self, reason: &Reason) -> bool {
335        match reason {
336            _ if reason.is_in_experiment() => true,
337            Reason::Fallthrough { .. } => self.track_events_fallthrough,
338            Reason::RuleMatch { rule_index, .. } => self
339                .rules
340                .get(*rule_index)
341                .map(|rule| rule.track_events)
342                .unwrap_or(false),
343            _ => false,
344        }
345    }
346
347    #[cfg(test)]
348    pub(crate) fn new_boolean_flag_with_segment_match(segment_keys: Vec<&str>, kind: Kind) -> Self {
349        Self {
350            key: "feature".to_string(),
351            version: 1,
352            on: true,
353            targets: vec![],
354            rules: vec![FlagRule::new_segment_match(segment_keys, kind)],
355            prerequisites: vec![],
356            fallthrough: VariationOrRollout::Variation { variation: 0 },
357            off_variation: Some(0),
358            variations: vec![FlagValue::Bool(false), FlagValue::Bool(true)],
359            client_visibility: ClientVisibility {
360                client_side_availability: ClientSideAvailability {
361                    using_mobile_key: false,
362                    using_environment_id: false,
363                    explicit: true,
364                },
365            },
366            salt: "xyz".to_string(),
367            track_events: false,
368            track_events_fallthrough: false,
369            debug_events_until_date: None,
370            context_targets: vec![],
371            migration_settings: None,
372            sampling_ratio: None,
373            exclude_from_summaries: false,
374        }
375    }
376}
377
378#[cfg(test)]
379mod tests {
380    use crate::store::Store;
381    use crate::test_common::TestStore;
382    use crate::MigrationFlagParameters;
383    use spectral::prelude::*;
384
385    use super::Flag;
386    use crate::eval::Reason::*;
387    use test_case::test_case;
388
389    #[test_case(true)]
390    #[test_case(false)]
391    fn handles_client_side_schema(client_side: bool) {
392        let json = &format!(
393            r#"{{
394            "key": "flag",
395            "version": 42,
396            "on": false,
397            "targets": [],
398            "rules": [],
399            "prerequisites": [],
400            "fallthrough": {{"variation": 1}},
401            "offVariation": 0,
402            "variations": [false, true],
403            "clientSide": {client_side},
404            "salt": "salty"
405        }}"#
406        );
407
408        let flag: Flag = serde_json::from_str(json).unwrap();
409        let client_side_availability = &flag.client_visibility.client_side_availability;
410        assert_eq!(client_side_availability.using_environment_id, client_side);
411        assert!(client_side_availability.using_mobile_key);
412        assert!(!client_side_availability.explicit);
413
414        assert_eq!(flag.using_environment_id(), client_side);
415    }
416
417    #[test_case(true)]
418    #[test_case(false)]
419    fn can_deserialize_and_reserialize_to_old_schema(client_side: bool) {
420        let json = &format!(
421            r#"{{
422  "key": "flag",
423  "version": 42,
424  "on": false,
425  "targets": [],
426  "contextTargets": [],
427  "rules": [],
428  "prerequisites": [],
429  "fallthrough": {{
430    "variation": 1
431  }},
432  "offVariation": 0,
433  "variations": [
434    false,
435    true
436  ],
437  "clientSide": {client_side},
438  "salt": "salty",
439  "trackEvents": false,
440  "trackEventsFallthrough": false,
441  "debugEventsUntilDate": null
442}}"#
443        );
444
445        let flag: Flag = serde_json::from_str(json).unwrap();
446        let restored = serde_json::to_string_pretty(&flag).unwrap();
447
448        assert_eq!(json, &restored);
449    }
450
451    #[test_case(true)]
452    #[test_case(false)]
453    fn handles_client_side_availability_schema(using_environment_id: bool) {
454        let json = &format!(
455            r#"{{
456            "key": "flag",
457            "version": 42,
458            "on": false,
459            "targets": [],
460            "rules": [],
461            "prerequisites": [],
462            "fallthrough": {{"variation": 1}},
463            "offVariation": 0,
464            "variations": [false, true],
465            "clientSideAvailability": {{
466                "usingEnvironmentId": {using_environment_id},
467                "usingMobileKey": false
468            }},
469            "salt": "salty"
470        }}"#
471        );
472
473        let flag: Flag = serde_json::from_str(json).unwrap();
474        let client_side_availability = &flag.client_visibility.client_side_availability;
475        assert_eq!(
476            client_side_availability.using_environment_id,
477            using_environment_id
478        );
479        assert!(!client_side_availability.using_mobile_key);
480        assert!(client_side_availability.explicit);
481
482        assert_eq!(flag.using_environment_id(), using_environment_id);
483    }
484
485    #[test_case(true)]
486    #[test_case(false)]
487    fn handles_context_target_schema(using_environment_id: bool) {
488        let json = &format!(
489            r#"{{
490            "key": "flag",
491            "version": 42,
492            "on": false,
493            "targets": [{{
494                "values": ["Bob"],
495                "variation": 1
496            }}],
497            "contextTargets": [{{
498                "contextKind": "org",
499                "values": ["LaunchDarkly"],
500                "variation": 0
501            }}],
502            "rules": [],
503            "prerequisites": [],
504            "fallthrough": {{"variation": 1}},
505            "offVariation": 0,
506            "variations": [false, true],
507            "clientSideAvailability": {{
508                "usingEnvironmentId": {using_environment_id},
509                "usingMobileKey": false
510            }},
511            "salt": "salty"
512        }}"#
513        );
514
515        let flag: Flag = serde_json::from_str(json).unwrap();
516        assert_eq!(1, flag.targets.len());
517        assert!(flag.targets[0].context_kind.is_user());
518
519        assert_eq!(1, flag.context_targets.len());
520        assert_eq!("org", flag.context_targets[0].context_kind.as_ref());
521    }
522
523    #[test]
524    fn getting_variation_with_invalid_index_is_handled_appropriately() {
525        let store = TestStore::new();
526        let flag = store.flag("flag").unwrap();
527
528        let detail = flag.variation(-1, Off);
529
530        assert!(detail.value.is_none());
531        assert!(detail.variation_index.is_none());
532        assert_eq!(
533            detail.reason,
534            Error {
535                error: crate::Error::MalformedFlag
536            }
537        );
538    }
539
540    #[test_case(true, true)]
541    #[test_case(true, false)]
542    #[test_case(false, true)]
543    #[test_case(false, false)]
544    fn can_deserialize_and_reserialize_to_new_schema(
545        using_environment_id: bool,
546        using_mobile_key: bool,
547    ) {
548        let json = &format!(
549            r#"{{
550  "key": "flag",
551  "version": 42,
552  "on": false,
553  "targets": [],
554  "contextTargets": [],
555  "rules": [],
556  "prerequisites": [],
557  "fallthrough": {{
558    "variation": 1
559  }},
560  "offVariation": 0,
561  "variations": [
562    false,
563    true
564  ],
565  "clientSideAvailability": {{
566    "usingMobileKey": {using_environment_id},
567    "usingEnvironmentId": {using_mobile_key}
568  }},
569  "salt": "salty",
570  "trackEvents": false,
571  "trackEventsFallthrough": false,
572  "debugEventsUntilDate": null
573}}"#
574        );
575
576        let flag: Flag = serde_json::from_str(json).unwrap();
577        let restored = serde_json::to_string_pretty(&flag).unwrap();
578
579        assert_eq!(json, &restored);
580    }
581
582    #[test]
583    fn is_experimentation_enabled() {
584        let store = TestStore::new();
585
586        let flag = store.flag("flag").unwrap();
587        asserting!("defaults to false")
588            .that(&flag.is_experimentation_enabled(&Off))
589            .is_false();
590        asserting!("false for fallthrough if trackEventsFallthrough is false")
591            .that(&flag.is_experimentation_enabled(&Fallthrough {
592                in_experiment: false,
593            }))
594            .is_false();
595
596        let flag = store.flag("flagWithRuleExclusion").unwrap();
597        asserting!("true for fallthrough if trackEventsFallthrough is true")
598            .that(&flag.is_experimentation_enabled(&Fallthrough {
599                in_experiment: false,
600            }))
601            .is_true();
602        asserting!("true for rule if rule.trackEvents is true")
603            .that(&flag.is_experimentation_enabled(&RuleMatch {
604                rule_index: 0,
605                rule_id: flag.rules.first().unwrap().id.clone(),
606                in_experiment: false,
607            }))
608            .is_true();
609
610        let flag = store.flag("flagWithExperiment").unwrap();
611        asserting!("true for fallthrough if reason says it is")
612            .that(&flag.is_experimentation_enabled(&Fallthrough {
613                in_experiment: true,
614            }))
615            .is_true();
616        asserting!("false for fallthrough if reason says it is")
617            .that(&flag.is_experimentation_enabled(&Fallthrough {
618                in_experiment: false,
619            }))
620            .is_false();
621        // note this flag doesn't even have a rule - doesn't matter, we go by the reason
622        asserting!("true for rule if reason says it is")
623            .that(&flag.is_experimentation_enabled(&RuleMatch {
624                rule_index: 42,
625                rule_id: "lol".into(),
626                in_experiment: true,
627            }))
628            .is_true();
629        asserting!("false 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: false,
634            }))
635            .is_false();
636    }
637
638    #[test]
639    fn sampling_ratio_is_ignored_appropriately() {
640        let store = TestStore::new();
641        let mut flag = store.flag("flag").unwrap();
642
643        flag.sampling_ratio = Some(42);
644        let with_low_sampling_ratio = serde_json::to_string_pretty(&flag).unwrap();
645        assert!(with_low_sampling_ratio.contains("\"samplingRatio\": 42"));
646
647        flag.sampling_ratio = Some(1);
648        let with_highest_ratio = serde_json::to_string_pretty(&flag).unwrap();
649        assert!(!with_highest_ratio.contains("\"samplingRatio\""));
650
651        flag.sampling_ratio = None;
652        let with_no_ratio = serde_json::to_string_pretty(&flag).unwrap();
653        assert!(!with_no_ratio.contains("\"samplingRatio\""));
654    }
655
656    #[test]
657    fn exclude_from_summaries_is_ignored_appropriately() {
658        let store = TestStore::new();
659        let mut flag = store.flag("flag").unwrap();
660
661        flag.exclude_from_summaries = true;
662        let with_exclude = serde_json::to_string_pretty(&flag).unwrap();
663        assert!(with_exclude.contains("\"excludeFromSummaries\": true"));
664
665        flag.exclude_from_summaries = false;
666        let without_exclude = serde_json::to_string_pretty(&flag).unwrap();
667        assert!(!without_exclude.contains("\"excludeFromSummaries\""));
668    }
669
670    #[test]
671    fn migration_settings_included_appropriately() {
672        let store = TestStore::new();
673        let mut flag = store.flag("flag").unwrap();
674
675        flag.migration_settings = None;
676        let without_migration_settings = serde_json::to_string_pretty(&flag).unwrap();
677        assert!(!without_migration_settings.contains("\"migration\""));
678
679        flag.migration_settings = Some(MigrationFlagParameters { check_ratio: None });
680        let without_empty_migration_settings = serde_json::to_string_pretty(&flag).unwrap();
681        assert!(!without_empty_migration_settings.contains("\"migration\""));
682
683        flag.migration_settings = Some(MigrationFlagParameters {
684            check_ratio: Some(1),
685        });
686        let with_default_ratio = serde_json::to_string_pretty(&flag).unwrap();
687        assert!(!with_default_ratio.contains("\"migration\""));
688
689        flag.migration_settings = Some(MigrationFlagParameters {
690            check_ratio: Some(42),
691        });
692        let with_specific_ratio = serde_json::to_string_pretty(&flag).unwrap();
693        assert!(with_specific_ratio.contains("\"migration\": {"));
694        assert!(with_specific_ratio.contains("\"checkRatio\": 42"));
695    }
696}