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