1use std::cmp::{max, min};
2use std::collections::{HashMap, HashSet};
3use std::fmt::{self, Display, Formatter};
4use std::time::Duration;
5
6use launchdarkly_server_sdk_evaluation::{
7 Context, ContextAttributes, Detail, Flag, FlagValue, Kind, Reason, Reference, VariationIndex,
8};
9use serde::ser::SerializeStruct;
10use serde::{Serialize, Serializer};
11
12use crate::migrations::{Operation, Origin, Stage};
13
14#[derive(Clone, Debug, PartialEq)]
15pub struct BaseEvent {
16 pub creation_date: u64,
17 pub context: Context,
18
19 inline: bool,
22 all_attribute_private: bool,
23 redact_anonymous: bool,
24 global_private_attributes: HashSet<Reference>,
25}
26
27impl Serialize for BaseEvent {
28 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
29 where
30 S: Serializer,
31 {
32 let mut state = serializer.serialize_struct("BaseEvent", 2)?;
33 state.serialize_field("creationDate", &self.creation_date)?;
34
35 if self.inline {
36 let context_attribute: ContextAttributes = if self.redact_anonymous {
37 ContextAttributes::from_context_with_anonymous_redaction(
38 self.context.clone(),
39 self.all_attribute_private,
40 self.global_private_attributes.clone(),
41 )
42 } else {
43 ContextAttributes::from_context(
44 self.context.clone(),
45 self.all_attribute_private,
46 self.global_private_attributes.clone(),
47 )
48 };
49 state.serialize_field("context", &context_attribute)?;
50 } else {
51 state.serialize_field("contextKeys", &self.context.context_keys())?;
52 }
53
54 state.end()
55 }
56}
57
58impl BaseEvent {
59 pub fn new(creation_date: u64, context: Context) -> Self {
60 Self {
61 creation_date,
62 context,
63 inline: false,
64 all_attribute_private: false,
65 global_private_attributes: HashSet::new(),
66 redact_anonymous: false,
67 }
68 }
69
70 pub(crate) fn into_inline(
71 self,
72 all_attribute_private: bool,
73 global_private_attributes: HashSet<Reference>,
74 ) -> Self {
75 Self {
76 inline: true,
77 all_attribute_private,
78 global_private_attributes,
79 ..self
80 }
81 }
82
83 pub(crate) fn into_inline_with_anonymous_redaction(
84 self,
85 all_attribute_private: bool,
86 global_private_attributes: HashSet<Reference>,
87 ) -> Self {
88 Self {
89 inline: true,
90 all_attribute_private,
91 global_private_attributes,
92 redact_anonymous: true,
93 ..self
94 }
95 }
96}
97
98#[derive(Clone, Debug)]
100pub struct MigrationOpEvent {
101 pub(crate) base: BaseEvent,
102 pub(crate) key: String,
103 pub(crate) version: Option<u64>,
104 pub(crate) operation: Operation,
105 pub(crate) default_stage: Stage,
106 pub(crate) evaluation: Detail<Stage>,
107 pub(crate) sampling_ratio: Option<u32>,
108 pub(crate) invoked: HashSet<Origin>,
109 pub(crate) consistency_check: Option<bool>,
110 pub(crate) consistency_check_ratio: Option<u32>,
111 pub(crate) errors: HashSet<Origin>,
112 pub(crate) latency: HashMap<Origin, Duration>,
113}
114
115impl MigrationOpEvent {
116 pub(crate) fn into_inline_with_anonymous_redaction(
117 self,
118 all_attribute_private: bool,
119 global_private_attributes: HashSet<Reference>,
120 ) -> Self {
121 Self {
122 base: self.base.into_inline_with_anonymous_redaction(
123 all_attribute_private,
124 global_private_attributes,
125 ),
126 ..self
127 }
128 }
129}
130
131impl Serialize for MigrationOpEvent {
132 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
133 where
134 S: Serializer,
135 {
136 let mut state = serializer.serialize_struct("MigrationOpEvent", 10)?;
137 state.serialize_field("kind", "migration_op")?;
138 state.serialize_field("creationDate", &self.base.creation_date)?;
139 state.serialize_field("context", &self.base.context)?;
140 state.serialize_field("operation", &self.operation)?;
141
142 if !is_default_ratio(&self.sampling_ratio) {
143 state.serialize_field("samplingRatio", &self.sampling_ratio.unwrap_or(1))?;
144 }
145
146 let evaluation = MigrationOpEvaluation {
147 key: self.key.clone(),
148 value: self.evaluation.value,
149 default: self.default_stage,
150 reason: self.evaluation.reason.clone(),
151 variation_index: self.evaluation.variation_index,
152 version: self.version,
153 };
154 state.serialize_field("evaluation", &evaluation)?;
155
156 let mut measurements = vec![];
157 if !self.invoked.is_empty() {
158 measurements.push(MigrationOpMeasurement::Invoked(&self.invoked));
159 }
160
161 if let Some(consistency_check) = self.consistency_check {
162 measurements.push(MigrationOpMeasurement::ConsistencyCheck(
163 consistency_check,
164 self.consistency_check_ratio,
165 ));
166 }
167
168 if !self.errors.is_empty() {
169 measurements.push(MigrationOpMeasurement::Errors(&self.errors));
170 }
171
172 if !self.latency.is_empty() {
173 measurements.push(MigrationOpMeasurement::Latency(&self.latency));
174 }
175
176 if !measurements.is_empty() {
177 state.serialize_field("measurements", &measurements)?;
178 }
179
180 state.end()
181 }
182}
183
184#[derive(Serialize)]
185#[serde(rename_all = "camelCase")]
186struct MigrationOpEvaluation {
187 pub key: String,
188
189 #[serde(skip_serializing_if = "Option::is_none")]
190 pub value: Option<Stage>,
191
192 pub(crate) default: Stage,
193
194 pub reason: Reason,
195
196 #[serde(rename = "variation", skip_serializing_if = "Option::is_none")]
197 pub variation_index: Option<VariationIndex>,
198
199 #[serde(skip_serializing_if = "Option::is_none")]
200 pub version: Option<u64>,
201}
202
203enum MigrationOpMeasurement<'a> {
204 Invoked(&'a HashSet<Origin>),
205 ConsistencyCheck(bool, Option<u32>),
206 Errors(&'a HashSet<Origin>),
207 Latency(&'a HashMap<Origin, Duration>),
208}
209
210impl Serialize for MigrationOpMeasurement<'_> {
211 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
212 where
213 S: Serializer,
214 {
215 match self {
216 MigrationOpMeasurement::Invoked(invoked) => {
217 let mut state = serializer.serialize_struct("invoked", 2)?;
218 state.serialize_field("key", "invoked")?;
219
220 let invoked = invoked
221 .iter()
222 .map(|origin| (origin, true))
223 .collect::<HashMap<_, _>>();
224 state.serialize_field("values", &invoked)?;
225 state.end()
226 }
227 MigrationOpMeasurement::ConsistencyCheck(consistency_check, consistency_ratio) => {
228 let mut state = serializer.serialize_struct("consistency", 2)?;
229 state.serialize_field("key", "consistent")?;
230 state.serialize_field("value", &consistency_check)?;
231
232 match consistency_ratio {
233 None | Some(1) => (),
234 Some(ratio) => state.serialize_field("samplingRatio", &ratio)?,
235 }
236
237 state.end()
238 }
239 MigrationOpMeasurement::Errors(errors) => {
240 let mut state = serializer.serialize_struct("errors", 2)?;
241 state.serialize_field("key", "error")?;
242
243 let errors = errors
244 .iter()
245 .map(|origin| (origin, true))
246 .collect::<HashMap<_, _>>();
247 state.serialize_field("values", &errors)?;
248 state.end()
249 }
250 MigrationOpMeasurement::Latency(latency) => {
251 let mut state = serializer.serialize_struct("latencies", 2)?;
252 state.serialize_field("key", "latency_ms")?;
253 let latencies = latency
254 .iter()
255 .map(|(origin, duration)| (origin, duration.as_millis() as u64))
256 .collect::<HashMap<_, _>>();
257 state.serialize_field("values", &latencies)?;
258 state.end()
259 }
260 }
261 }
262}
263
264#[derive(Clone, Debug, PartialEq, Serialize)]
265#[serde(rename_all = "camelCase")]
266pub struct FeatureRequestEvent {
267 #[serde(flatten)]
268 pub(crate) base: BaseEvent,
269 key: String,
270 value: FlagValue,
271 variation: Option<VariationIndex>,
272 default: FlagValue,
273 #[serde(skip_serializing_if = "Option::is_none")]
274 reason: Option<Reason>,
275 version: Option<u64>,
276 #[serde(skip_serializing_if = "Option::is_none")]
277 prereq_of: Option<String>,
278
279 #[serde(skip)]
280 pub(crate) track_events: bool,
281
282 #[serde(skip)]
283 pub(crate) debug_events_until_date: Option<u64>,
284
285 #[serde(skip_serializing_if = "is_default_ratio")]
286 pub(crate) sampling_ratio: Option<u32>,
287
288 #[serde(skip_serializing_if = "std::ops::Not::not")]
289 pub(crate) exclude_from_summaries: bool,
290}
291
292impl FeatureRequestEvent {
293 pub fn to_index_event(
294 &self,
295 all_attribute_private: bool,
296 global_private_attributes: HashSet<Reference>,
297 ) -> IndexEvent {
298 self.base
299 .clone()
300 .into_inline(all_attribute_private, global_private_attributes)
301 .into()
302 }
303
304 pub(crate) fn into_inline(
305 self,
306 all_attribute_private: bool,
307 global_private_attributes: HashSet<Reference>,
308 ) -> Self {
309 Self {
310 base: self
311 .base
312 .into_inline(all_attribute_private, global_private_attributes),
313 ..self
314 }
315 }
316
317 pub(crate) fn into_inline_with_anonymous_redaction(
318 self,
319 all_attribute_private: bool,
320 global_private_attributes: HashSet<Reference>,
321 ) -> Self {
322 Self {
323 base: self.base.into_inline_with_anonymous_redaction(
324 all_attribute_private,
325 global_private_attributes,
326 ),
327 ..self
328 }
329 }
330}
331
332#[derive(Clone, Debug, PartialEq, Serialize)]
333pub struct IndexEvent {
334 #[serde(flatten)]
335 base: BaseEvent,
336}
337
338impl From<BaseEvent> for IndexEvent {
339 fn from(base: BaseEvent) -> Self {
340 let base = BaseEvent {
341 inline: true,
342 ..base
343 };
344
345 Self { base }
346 }
347}
348
349#[derive(Clone, Debug, PartialEq, Serialize)]
350pub struct IdentifyEvent {
351 #[serde(flatten)]
352 pub(crate) base: BaseEvent,
353 key: String,
354 #[serde(skip_serializing_if = "is_default_ratio")]
355 pub(crate) sampling_ratio: Option<u32>,
356}
357
358impl IdentifyEvent {
359 pub(crate) fn into_inline(
360 self,
361 all_attribute_private: bool,
362 global_private_attributes: HashSet<Reference>,
363 ) -> Self {
364 Self {
365 base: self
366 .base
367 .into_inline(all_attribute_private, global_private_attributes),
368 ..self
369 }
370 }
371}
372
373#[derive(Clone, Debug, PartialEq, Serialize)]
374#[serde(rename_all = "camelCase")]
375pub struct CustomEvent {
376 #[serde(flatten)]
377 pub(crate) base: BaseEvent,
378 key: String,
379 #[serde(skip_serializing_if = "Option::is_none")]
380 metric_value: Option<f64>,
381 #[serde(skip_serializing_if = "serde_json::Value::is_null")]
382 data: serde_json::Value,
383 #[serde(skip_serializing_if = "is_default_ratio")]
384 pub(crate) sampling_ratio: Option<u32>,
385}
386
387impl CustomEvent {
388 pub(crate) fn into_inline_with_anonymous_redaction(
389 self,
390 all_attribute_private: bool,
391 global_private_attributes: HashSet<Reference>,
392 ) -> Self {
393 Self {
394 base: self.base.into_inline_with_anonymous_redaction(
395 all_attribute_private,
396 global_private_attributes,
397 ),
398 ..self
399 }
400 }
401
402 pub fn to_index_event(
403 &self,
404 all_attribute_private: bool,
405 global_private_attributes: HashSet<Reference>,
406 ) -> IndexEvent {
407 self.base
408 .clone()
409 .into_inline(all_attribute_private, global_private_attributes)
410 .into()
411 }
412}
413
414#[derive(Clone, Debug, Serialize)]
415#[serde(tag = "kind")]
416#[allow(clippy::large_enum_variant)]
417pub enum OutputEvent {
418 #[serde(rename = "index")]
419 Index(IndexEvent),
420
421 #[serde(rename = "debug")]
422 Debug(FeatureRequestEvent),
423
424 #[serde(rename = "feature")]
425 FeatureRequest(FeatureRequestEvent),
426
427 #[serde(rename = "identify")]
428 Identify(IdentifyEvent),
429
430 #[serde(rename = "custom")]
431 Custom(CustomEvent),
432
433 #[serde(rename = "summary")]
434 Summary(EventSummary),
435
436 #[serde(rename = "migration_op")]
437 MigrationOp(MigrationOpEvent),
438}
439
440impl OutputEvent {
441 #[cfg(test)]
442 pub fn kind(&self) -> &'static str {
443 match self {
444 OutputEvent::Index { .. } => "index",
445 OutputEvent::Debug { .. } => "debug",
446 OutputEvent::FeatureRequest { .. } => "feature",
447 OutputEvent::Identify { .. } => "identify",
448 OutputEvent::Custom { .. } => "custom",
449 OutputEvent::Summary { .. } => "summary",
450 OutputEvent::MigrationOp { .. } => "migration_op",
451 }
452 }
453}
454
455#[allow(clippy::large_enum_variant)]
456#[derive(Clone, Debug, Serialize)]
457pub enum InputEvent {
458 FeatureRequest(FeatureRequestEvent),
459 Identify(IdentifyEvent),
460 Custom(CustomEvent),
461 MigrationOp(MigrationOpEvent),
462}
463
464impl InputEvent {
465 #[cfg(test)]
466 pub fn base_mut(&mut self) -> Option<&mut BaseEvent> {
467 match self {
468 InputEvent::FeatureRequest(FeatureRequestEvent { base, .. }) => Some(base),
469 InputEvent::Identify(IdentifyEvent { base, .. }) => Some(base),
470 InputEvent::Custom(CustomEvent { base, .. }) => Some(base),
471 InputEvent::MigrationOp(MigrationOpEvent { base, .. }) => Some(base),
472 }
473 }
474}
475
476impl Display for InputEvent {
477 fn fmt(&self, f: &mut Formatter) -> fmt::Result {
478 let json = serde_json::to_string_pretty(self)
479 .unwrap_or_else(|e| format!("JSON serialization failed ({}): {:?}", e, self));
480 write!(f, "{}", json)
481 }
482}
483
484pub struct EventFactory {
485 send_reason: bool,
486}
487
488impl EventFactory {
489 pub fn new(send_reason: bool) -> Self {
490 Self { send_reason }
491 }
492
493 pub(crate) fn now() -> u64 {
494 std::time::SystemTime::now()
495 .duration_since(std::time::UNIX_EPOCH)
496 .unwrap()
497 .as_millis() as u64
498 }
499
500 pub fn new_unknown_flag_event(
501 &self,
502 flag_key: &str,
503 context: Context,
504 detail: Detail<FlagValue>,
505 default: FlagValue,
506 ) -> InputEvent {
507 self.new_feature_request_event(flag_key, context, None, detail, default, None)
508 }
509
510 pub fn new_eval_event(
511 &self,
512 flag_key: &str,
513 context: Context,
514 flag: &Flag,
515 detail: Detail<FlagValue>,
516 default: FlagValue,
517 prereq_of: Option<String>,
518 ) -> InputEvent {
519 self.new_feature_request_event(flag_key, context, Some(flag), detail, default, prereq_of)
520 }
521
522 fn new_feature_request_event(
523 &self,
524 flag_key: &str,
525 context: Context,
526 flag: Option<&Flag>,
527 detail: Detail<FlagValue>,
528 default: FlagValue,
529 prereq_of: Option<String>,
530 ) -> InputEvent {
531 let value = detail
532 .value
533 .unwrap_or(FlagValue::Json(serde_json::Value::Null));
534
535 let flag_track_events;
536 let require_experiment_data;
537 let debug_events_until_date;
538 let sampling_ratio;
539 let exclude_from_summaries;
540
541 if let Some(f) = flag {
542 flag_track_events = f.track_events;
543 require_experiment_data = f.is_experimentation_enabled(&detail.reason);
544 debug_events_until_date = f.debug_events_until_date;
545 sampling_ratio = f.sampling_ratio;
546 exclude_from_summaries = f.exclude_from_summaries;
547 } else {
548 flag_track_events = false;
549 require_experiment_data = false;
550 debug_events_until_date = None;
551 sampling_ratio = None;
552 exclude_from_summaries = false;
553 }
554
555 let reason = if self.send_reason || require_experiment_data {
556 Some(detail.reason)
557 } else {
558 None
559 };
560
561 InputEvent::FeatureRequest(FeatureRequestEvent {
562 base: BaseEvent::new(Self::now(), context),
563 key: flag_key.to_owned(),
564 default,
565 reason,
566 value,
567 variation: detail.variation_index,
568 version: flag.map(|f| f.version),
569 prereq_of,
570 track_events: flag_track_events || require_experiment_data,
571 debug_events_until_date,
572 sampling_ratio,
573 exclude_from_summaries,
574 })
575 }
576
577 pub fn new_identify(&self, context: Context) -> InputEvent {
578 InputEvent::Identify(IdentifyEvent {
579 key: context.key().to_owned(),
580 base: BaseEvent::new(Self::now(), context),
581 sampling_ratio: None,
582 })
583 }
584
585 pub(crate) fn new_migration_op(&self, event: MigrationOpEvent) -> InputEvent {
586 InputEvent::MigrationOp(event)
587 }
588
589 pub fn new_custom(
590 &self,
591 context: Context,
592 key: impl Into<String>,
593 metric_value: Option<f64>,
594 data: impl Serialize,
595 ) -> serde_json::Result<InputEvent> {
596 let data = serde_json::to_value(data)?;
597
598 Ok(InputEvent::Custom(CustomEvent {
599 base: BaseEvent::new(Self::now(), context),
600 key: key.into(),
601 metric_value,
602 data,
603 sampling_ratio: None,
604 }))
605 }
606}
607
608#[derive(Clone, Debug, Serialize)]
609#[serde(into = "EventSummaryOutput")]
610pub struct EventSummary {
611 pub(crate) start_date: u64,
612 pub(crate) end_date: u64,
613 pub(crate) features: HashMap<String, FlagSummary>,
614}
615
616impl Default for EventSummary {
617 fn default() -> Self {
618 EventSummary::new()
619 }
620}
621
622impl EventSummary {
623 pub fn new() -> Self {
624 EventSummary {
625 start_date: u64::MAX,
626 end_date: 0,
627 features: HashMap::new(),
628 }
629 }
630
631 pub fn is_empty(&self) -> bool {
632 self.features.is_empty()
633 }
634
635 pub fn add(&mut self, event: &FeatureRequestEvent) {
636 let FeatureRequestEvent {
637 base:
638 BaseEvent {
639 creation_date,
640 context,
641 ..
642 },
643 key,
644 value,
645 version,
646 variation,
647 default,
648 ..
649 } = event;
650
651 self.start_date = min(self.start_date, *creation_date);
652 self.end_date = max(self.end_date, *creation_date);
653
654 let variation_key = VariationKey {
655 version: *version,
656 variation: *variation,
657 };
658
659 let feature = self
660 .features
661 .entry(key.clone())
662 .or_insert_with(|| FlagSummary::new(default.clone()));
663
664 feature.track(variation_key, value, context);
665 }
666
667 pub fn reset(&mut self) {
668 self.features.clear();
669 self.start_date = u64::MAX;
670 self.end_date = 0;
671 }
672}
673
674#[derive(Clone, Debug)]
675pub struct FlagSummary {
676 pub(crate) counters: HashMap<VariationKey, VariationSummary>,
677 pub(crate) default: FlagValue,
678 pub(crate) context_kinds: HashSet<Kind>,
679}
680
681impl FlagSummary {
682 pub fn new(default: FlagValue) -> Self {
683 Self {
684 counters: HashMap::new(),
685 default,
686 context_kinds: HashSet::new(),
687 }
688 }
689
690 pub fn track(
691 &mut self,
692 variation_key: VariationKey,
693 value: &FlagValue,
694 context: &Context,
695 ) -> &mut Self {
696 if let Some(summary) = self.counters.get_mut(&variation_key) {
697 summary.count_request();
698 } else {
699 self.counters
700 .insert(variation_key, VariationSummary::new(value.clone()));
701 }
702
703 for kind in context.kinds() {
704 self.context_kinds.insert(kind.clone());
705 }
706
707 self
708 }
709}
710
711#[derive(Clone, Debug, Eq, Hash, PartialEq)]
712pub struct VariationKey {
713 pub version: Option<u64>,
714 pub variation: Option<VariationIndex>,
715}
716
717#[derive(Clone, Debug, PartialEq)]
718pub struct VariationSummary {
719 pub count: u64,
720 pub value: FlagValue,
721}
722
723impl VariationSummary {
724 fn new(value: FlagValue) -> Self {
725 VariationSummary { count: 1, value }
726 }
727
728 fn count_request(&mut self) {
729 self.count += 1;
730 }
731}
732
733#[derive(Serialize)]
739#[serde(rename_all = "camelCase")]
740struct EventSummaryOutput {
741 start_date: u64,
742 end_date: u64,
743 features: HashMap<String, FeatureSummaryOutput>,
744}
745
746impl From<EventSummary> for EventSummaryOutput {
747 fn from(summary: EventSummary) -> Self {
748 let features = summary
749 .features
750 .into_iter()
751 .map(|(key, value)| (key, value.into()))
752 .collect();
753
754 EventSummaryOutput {
755 start_date: summary.start_date,
756 end_date: summary.end_date,
757 features,
758 }
759 }
760}
761
762#[derive(Serialize)]
763#[serde(rename_all = "camelCase")]
764struct FeatureSummaryOutput {
765 default: FlagValue,
766 context_kinds: HashSet<Kind>,
767 counters: Vec<VariationCounterOutput>,
768}
769
770impl From<FlagSummary> for FeatureSummaryOutput {
771 fn from(flag_summary: FlagSummary) -> Self {
772 let counters = flag_summary
773 .counters
774 .into_iter()
775 .map(|(variation_key, variation_summary)| (variation_key, variation_summary).into())
776 .collect::<Vec<VariationCounterOutput>>();
777
778 Self {
779 default: flag_summary.default,
780 context_kinds: flag_summary.context_kinds,
781 counters,
782 }
783 }
784}
785
786#[derive(Serialize)]
787struct VariationCounterOutput {
788 pub value: FlagValue,
789 #[serde(skip_serializing_if = "Option::is_none")]
790 pub unknown: Option<bool>,
791 #[serde(skip_serializing_if = "Option::is_none")]
792 pub version: Option<u64>,
793 pub count: u64,
794 #[serde(skip_serializing_if = "Option::is_none")]
795 pub variation: Option<VariationIndex>,
796}
797
798impl From<(VariationKey, VariationSummary)> for VariationCounterOutput {
799 fn from((variation_key, variation_summary): (VariationKey, VariationSummary)) -> Self {
800 VariationCounterOutput {
801 value: variation_summary.value,
802 unknown: variation_key.version.map_or(Some(true), |_| None),
803 version: variation_key.version,
804 count: variation_summary.count,
805 variation: variation_key.variation,
806 }
807 }
808}
809
810fn is_default_ratio(sampling_ratio: &Option<u32>) -> bool {
812 sampling_ratio.unwrap_or(1) == 1
813}
814
815#[cfg(test)]
816mod tests {
817 use launchdarkly_server_sdk_evaluation::{
818 AttributeValue, ContextBuilder, Kind, MultiContextBuilder,
819 };
820 use maplit::{hashmap, hashset};
821
822 use super::*;
823 use crate::test_common::basic_flag;
824 use assert_json_diff::assert_json_eq;
825 use serde_json::json;
826 use test_case::test_case;
827
828 #[test]
829 fn serializes_feature_request_event() {
830 let flag = basic_flag("flag");
831 let default = FlagValue::from(false);
832 let context = ContextBuilder::new("alice")
833 .anonymous(true)
834 .build()
835 .expect("Failed to create context");
836 let fallthrough = Detail {
837 value: Some(FlagValue::from(false)),
838 variation_index: Some(1),
839 reason: Reason::Fallthrough {
840 in_experiment: false,
841 },
842 };
843
844 let event_factory = EventFactory::new(true);
845 let mut feature_request_event =
846 event_factory.new_eval_event(&flag.key, context, &flag, fallthrough, default, None);
847 feature_request_event.base_mut().unwrap().creation_date = 1234;
849
850 if let InputEvent::FeatureRequest(feature_request_event) = feature_request_event {
851 let output_event = OutputEvent::FeatureRequest(
852 feature_request_event.into_inline(false, HashSet::new()),
853 );
854 let event_json = json!({
855 "kind": "feature",
856 "creationDate": 1234,
857 "context": {
858 "key": "alice",
859 "kind": "user",
860 "anonymous": true
861 },
862 "key": "flag",
863 "value": false,
864 "variation": 1,
865 "default": false,
866 "reason": {
867 "kind": "FALLTHROUGH"
868 },
869 "version": 42
870 });
871
872 assert_json_eq!(output_event, event_json);
873 }
874 }
875
876 #[test]
877 fn serializes_feature_request_event_with_global_private_attribute() {
878 let flag = basic_flag("flag");
879 let default = FlagValue::from(false);
880 let context = ContextBuilder::new("alice")
881 .anonymous(true)
882 .set_value("foo", AttributeValue::Bool(true))
883 .build()
884 .expect("Failed to create context");
885 let fallthrough = Detail {
886 value: Some(FlagValue::from(false)),
887 variation_index: Some(1),
888 reason: Reason::Fallthrough {
889 in_experiment: false,
890 },
891 };
892
893 let event_factory = EventFactory::new(true);
894 let mut feature_request_event =
895 event_factory.new_eval_event(&flag.key, context, &flag, fallthrough, default, None);
896 feature_request_event.base_mut().unwrap().creation_date = 1234;
898
899 if let InputEvent::FeatureRequest(feature_request_event) = feature_request_event {
900 let output_event = OutputEvent::FeatureRequest(
901 feature_request_event.into_inline(false, hashset!["foo".into()]),
902 );
903 let event_json = json!({
904 "kind": "feature",
905 "creationDate": 1234,
906 "context": {
907 "key": "alice",
908 "kind": "user",
909 "anonymous": true,
910 "_meta" : {
911 "redactedAttributes" : ["foo"]
912 }
913 },
914 "key": "flag",
915 "value": false,
916 "variation": 1,
917 "default": false,
918 "reason": {
919 "kind": "FALLTHROUGH"
920 },
921 "version": 42
922 });
923
924 assert_json_eq!(output_event, event_json);
925 }
926 }
927
928 #[test]
929 fn serializes_feature_request_event_with_all_private_attributes() {
930 let flag = basic_flag("flag");
931 let default = FlagValue::from(false);
932 let context = ContextBuilder::new("alice")
933 .anonymous(true)
934 .set_value("foo", AttributeValue::Bool(true))
935 .build()
936 .expect("Failed to create context");
937 let fallthrough = Detail {
938 value: Some(FlagValue::from(false)),
939 variation_index: Some(1),
940 reason: Reason::Fallthrough {
941 in_experiment: false,
942 },
943 };
944
945 let event_factory = EventFactory::new(true);
946 let mut feature_request_event =
947 event_factory.new_eval_event(&flag.key, context, &flag, fallthrough, default, None);
948 feature_request_event.base_mut().unwrap().creation_date = 1234;
950
951 if let InputEvent::FeatureRequest(feature_request_event) = feature_request_event {
952 let output_event = OutputEvent::FeatureRequest(
953 feature_request_event.into_inline(true, HashSet::new()),
954 );
955 let event_json = json!({
956 "kind": "feature",
957 "creationDate": 1234,
958 "context": {
959 "_meta": {
960 "redactedAttributes" : ["foo"]
961 },
962 "key": "alice",
963 "kind": "user",
964 "anonymous": true
965 },
966 "key": "flag",
967 "value": false,
968 "variation": 1,
969 "default": false,
970 "reason": {
971 "kind": "FALLTHROUGH"
972 },
973 "version": 42
974 });
975
976 assert_json_eq!(output_event, event_json);
977 }
978 }
979
980 #[test]
981 fn serializes_feature_request_event_with_anonymous_attribute_redaction() {
982 let flag = basic_flag("flag");
983 let default = FlagValue::from(false);
984 let context = ContextBuilder::new("alice")
985 .anonymous(true)
986 .set_value("foo", AttributeValue::Bool(true))
987 .build()
988 .expect("Failed to create context");
989 let fallthrough = Detail {
990 value: Some(FlagValue::from(false)),
991 variation_index: Some(1),
992 reason: Reason::Fallthrough {
993 in_experiment: false,
994 },
995 };
996
997 let event_factory = EventFactory::new(true);
998 let mut feature_request_event =
999 event_factory.new_eval_event(&flag.key, context, &flag, fallthrough, default, None);
1000 feature_request_event.base_mut().unwrap().creation_date = 1234;
1002
1003 if let InputEvent::FeatureRequest(feature_request_event) = feature_request_event {
1004 let output_event = OutputEvent::FeatureRequest(
1005 feature_request_event.into_inline_with_anonymous_redaction(false, HashSet::new()),
1006 );
1007 let event_json = json!({
1008 "kind": "feature",
1009 "creationDate": 1234,
1010 "context": {
1011 "_meta": {
1012 "redactedAttributes" : ["foo"]
1013 },
1014 "key": "alice",
1015 "kind": "user",
1016 "anonymous": true
1017 },
1018 "key": "flag",
1019 "value": false,
1020 "variation": 1,
1021 "default": false,
1022 "reason": {
1023 "kind": "FALLTHROUGH"
1024 },
1025 "version": 42
1026 });
1027
1028 assert_json_eq!(output_event, event_json);
1029 }
1030 }
1031
1032 #[test]
1033 fn serializes_feature_request_event_with_anonymous_attribute_redaction_in_multikind_context() {
1034 let flag = basic_flag("flag");
1035 let default = FlagValue::from(false);
1036 let user_context = ContextBuilder::new("alice")
1037 .anonymous(true)
1038 .set_value("foo", AttributeValue::Bool(true))
1039 .build()
1040 .expect("Failed to create user context");
1041 let org_context = ContextBuilder::new("LaunchDarkly")
1042 .kind("org")
1043 .set_value("foo", AttributeValue::Bool(true))
1044 .build()
1045 .expect("Failed to create org context");
1046 let multi_context = MultiContextBuilder::new()
1047 .add_context(user_context)
1048 .add_context(org_context)
1049 .build()
1050 .expect("Failed to create multi context");
1051 let fallthrough = Detail {
1052 value: Some(FlagValue::from(false)),
1053 variation_index: Some(1),
1054 reason: Reason::Fallthrough {
1055 in_experiment: false,
1056 },
1057 };
1058
1059 let event_factory = EventFactory::new(true);
1060 let mut feature_request_event = event_factory.new_eval_event(
1061 &flag.key,
1062 multi_context,
1063 &flag,
1064 fallthrough,
1065 default,
1066 None,
1067 );
1068 feature_request_event.base_mut().unwrap().creation_date = 1234;
1070
1071 if let InputEvent::FeatureRequest(feature_request_event) = feature_request_event {
1072 let output_event = OutputEvent::FeatureRequest(
1073 feature_request_event.into_inline_with_anonymous_redaction(false, HashSet::new()),
1074 );
1075 let event_json = json!({
1076 "kind": "feature",
1077 "creationDate": 1234,
1078 "context": {
1079 "kind": "multi",
1080 "user": {
1081 "_meta": {
1082 "redactedAttributes" : ["foo"]
1083 },
1084 "key": "alice",
1085 "anonymous": true
1086 },
1087 "org": {
1088 "foo": true,
1089 "key": "LaunchDarkly"
1090 }
1091 },
1092 "key": "flag",
1093 "value": false,
1094 "variation": 1,
1095 "default": false,
1096 "reason": {
1097 "kind": "FALLTHROUGH"
1098 },
1099 "version": 42
1100 });
1101
1102 assert_json_eq!(output_event, event_json);
1103 }
1104 }
1105
1106 #[test]
1107 fn serializes_feature_request_event_with_local_private_attribute() {
1108 let flag = basic_flag("flag");
1109 let default = FlagValue::from(false);
1110 let context = ContextBuilder::new("alice")
1111 .anonymous(true)
1112 .set_value("foo", AttributeValue::Bool(true))
1113 .add_private_attribute("foo")
1114 .build()
1115 .expect("Failed to create context");
1116 let fallthrough = Detail {
1117 value: Some(FlagValue::from(false)),
1118 variation_index: Some(1),
1119 reason: Reason::Fallthrough {
1120 in_experiment: false,
1121 },
1122 };
1123
1124 let event_factory = EventFactory::new(true);
1125 let mut feature_request_event =
1126 event_factory.new_eval_event(&flag.key, context, &flag, fallthrough, default, None);
1127 feature_request_event.base_mut().unwrap().creation_date = 1234;
1129
1130 if let InputEvent::FeatureRequest(feature_request_event) = feature_request_event {
1131 let output_event = OutputEvent::FeatureRequest(
1132 feature_request_event.into_inline(false, HashSet::new()),
1133 );
1134 let event_json = json!({
1135 "kind": "feature",
1136 "creationDate": 1234,
1137 "context": {
1138 "_meta": {
1139 "redactedAttributes" : ["foo"]
1140 },
1141 "key": "alice",
1142 "kind": "user",
1143 "anonymous": true
1144 },
1145 "key": "flag",
1146 "value": false,
1147 "variation": 1,
1148 "default": false,
1149 "reason": {
1150 "kind": "FALLTHROUGH"
1151 },
1152 "version": 42
1153 });
1154
1155 assert_json_eq!(output_event, event_json);
1156 }
1157 }
1158
1159 #[test]
1160 fn serializes_feature_request_event_without_inlining_user() {
1161 let flag = basic_flag("flag");
1162 let default = FlagValue::from(false);
1163 let context = ContextBuilder::new("alice")
1164 .anonymous(true)
1165 .build()
1166 .expect("Failed to create context");
1167 let fallthrough = Detail {
1168 value: Some(FlagValue::from(false)),
1169 variation_index: Some(1),
1170 reason: Reason::Fallthrough {
1171 in_experiment: false,
1172 },
1173 };
1174
1175 let event_factory = EventFactory::new(true);
1176 let mut feature_request_event =
1177 event_factory.new_eval_event(&flag.key, context, &flag, fallthrough, default, None);
1178 feature_request_event.base_mut().unwrap().creation_date = 1234;
1180
1181 if let InputEvent::FeatureRequest(feature_request_event) = feature_request_event {
1182 let output_event = OutputEvent::FeatureRequest(feature_request_event);
1183 let event_json = json!({
1184 "kind": "feature",
1185 "creationDate": 1234,
1186 "contextKeys": {
1187 "user": "alice"
1188 },
1189 "key": "flag",
1190 "value": false,
1191 "variation": 1,
1192 "default": false,
1193 "reason": {
1194 "kind": "FALLTHROUGH"
1195 },
1196 "version": 42
1197 });
1198 assert_json_eq!(output_event, event_json);
1199 }
1200 }
1201
1202 #[test]
1203 fn serializes_identify_event() {
1204 let context = ContextBuilder::new("alice")
1205 .anonymous(true)
1206 .build()
1207 .expect("Failed to create context");
1208 let event_factory = EventFactory::new(true);
1209 let mut identify = event_factory.new_identify(context);
1210 identify.base_mut().unwrap().creation_date = 1234;
1211
1212 if let InputEvent::Identify(identify) = identify {
1213 let output_event = OutputEvent::Identify(identify.into_inline(false, HashSet::new()));
1214 let event_json = json!({
1215 "kind": "identify",
1216 "creationDate": 1234,
1217 "context": {
1218 "key": "alice",
1219 "kind": "user",
1220 "anonymous": true
1221 },
1222 "key": "alice"
1223 });
1224 assert_json_eq!(output_event, event_json);
1225 }
1226 }
1227
1228 #[test]
1229 fn serializes_custom_event() {
1230 let context = ContextBuilder::new("alice")
1231 .anonymous(true)
1232 .build()
1233 .expect("Failed to create context");
1234
1235 let event_factory = EventFactory::new(true);
1236 let mut custom_event = event_factory
1237 .new_custom(
1238 context,
1239 "custom-key",
1240 Some(12345.0),
1241 serde_json::Value::Null,
1242 )
1243 .unwrap();
1244 custom_event.base_mut().unwrap().creation_date = 1234;
1246
1247 if let InputEvent::Custom(custom_event) = custom_event {
1248 let output_event = OutputEvent::Custom(custom_event);
1249 let event_json = json!({
1250 "kind": "custom",
1251 "creationDate": 1234,
1252 "contextKeys": {
1253 "user": "alice"
1254 },
1255 "key": "custom-key",
1256 "metricValue": 12345.0
1257 });
1258 assert_json_eq!(output_event, event_json);
1259 }
1260 }
1261
1262 #[test]
1263 fn serializes_custom_event_without_inlining_user() {
1264 let context = ContextBuilder::new("alice")
1265 .anonymous(true)
1266 .build()
1267 .expect("Failed to create context");
1268
1269 let event_factory = EventFactory::new(true);
1270 let mut custom_event = event_factory
1271 .new_custom(
1272 context,
1273 "custom-key",
1274 Some(12345.0),
1275 serde_json::Value::Null,
1276 )
1277 .unwrap();
1278 custom_event.base_mut().unwrap().creation_date = 1234;
1280
1281 if let InputEvent::Custom(custom_event) = custom_event {
1282 let output_event = OutputEvent::Custom(custom_event);
1283 let event_json = json!({
1284 "kind": "custom",
1285 "creationDate": 1234,
1286 "contextKeys": {
1287 "user": "alice"
1288 },
1289 "key": "custom-key",
1290 "metricValue": 12345.0
1291 });
1292 assert_json_eq!(output_event, event_json);
1293 }
1294 }
1295
1296 #[test]
1297 fn serializes_summary_event() {
1298 let summary = EventSummary {
1299 start_date: 1234,
1300 end_date: 4567,
1301 features: hashmap! {
1302 "f".into() => FlagSummary {
1303 counters: hashmap! {
1304 VariationKey{version: Some(2), variation: Some(1)} => VariationSummary{count: 1, value: true.into()},
1305 },
1306 default: false.into(),
1307 context_kinds: HashSet::new(),
1308 }
1309 },
1310 };
1311 let summary_event = OutputEvent::Summary(summary);
1312
1313 let event_json = json!({
1314 "kind": "summary",
1315 "startDate": 1234,
1316 "endDate": 4567,
1317 "features": {
1318 "f": {
1319 "default": false,
1320 "contextKinds": [],
1321 "counters": [{
1322 "value": true,
1323 "version": 2,
1324 "count": 1,
1325 "variation": 1
1326 }]
1327 }
1328 }});
1329 assert_json_eq!(summary_event, event_json);
1330 }
1331
1332 #[test]
1333 fn summary_resets_appropriately() {
1334 let mut summary = EventSummary {
1335 start_date: 1234,
1336 end_date: 4567,
1337 features: hashmap! {
1338 "f".into() => FlagSummary {
1339 counters: hashmap!{
1340 VariationKey{version: Some(2), variation: Some(1)} => VariationSummary{count: 1, value: true.into()}
1341 },
1342 default: false.into(),
1343 context_kinds: HashSet::new(),
1344 }
1345 },
1346 };
1347
1348 summary.reset();
1349
1350 assert!(summary.features.is_empty());
1351 assert_eq!(summary.start_date, u64::MAX);
1352 assert_eq!(summary.end_date, 0);
1353 }
1354
1355 #[test]
1356 fn serializes_index_event() {
1357 let context = ContextBuilder::new("alice")
1358 .anonymous(true)
1359 .build()
1360 .expect("Failed to create context");
1361 let base_event = BaseEvent::new(1234, context);
1362 let index_event = OutputEvent::Index(base_event.into());
1363
1364 let event_json = json!({
1365 "kind": "index",
1366 "creationDate": 1234,
1367 "context": {
1368 "key": "alice",
1369 "kind": "user",
1370 "anonymous": true
1371 }
1372 });
1373
1374 assert_json_eq!(index_event, event_json);
1375 }
1376
1377 #[test]
1378 fn summarises_feature_request() {
1379 let mut summary = EventSummary::new();
1380 assert!(summary.is_empty());
1381 assert!(summary.start_date > summary.end_date);
1382
1383 let flag = basic_flag("flag");
1384 let default = FlagValue::from(false);
1385 let context = MultiContextBuilder::new()
1386 .add_context(
1387 ContextBuilder::new("alice")
1388 .build()
1389 .expect("Failed to create context"),
1390 )
1391 .add_context(
1392 ContextBuilder::new("LaunchDarkly")
1393 .kind("org")
1394 .build()
1395 .expect("Failed to create context"),
1396 )
1397 .build()
1398 .expect("Failed to create multi-context");
1399
1400 let value = FlagValue::from(false);
1401 let variation_index = 1;
1402 let reason = Reason::Fallthrough {
1403 in_experiment: false,
1404 };
1405 let eval_at = 1234;
1406
1407 let fallthrough_request = FeatureRequestEvent {
1408 base: BaseEvent::new(eval_at, context),
1409 key: flag.key.clone(),
1410 value: value.clone(),
1411 variation: Some(variation_index),
1412 default: default.clone(),
1413 version: Some(flag.version),
1414 reason: Some(reason),
1415 prereq_of: None,
1416 track_events: false,
1417 debug_events_until_date: None,
1418 sampling_ratio: flag.sampling_ratio,
1419 exclude_from_summaries: flag.exclude_from_summaries,
1420 };
1421
1422 summary.add(&fallthrough_request);
1423 assert!(!summary.is_empty());
1424 assert_eq!(summary.start_date, eval_at);
1425 assert_eq!(summary.end_date, eval_at);
1426
1427 let fallthrough_key = VariationKey {
1428 version: Some(flag.version),
1429 variation: Some(variation_index),
1430 };
1431
1432 let feature = summary.features.get(&flag.key);
1433 assert!(feature.is_some());
1434 let feature = feature.unwrap();
1435 assert_eq!(feature.default, default);
1436 assert_eq!(2, feature.context_kinds.len());
1437 assert!(feature.context_kinds.contains(&Kind::user()));
1438 assert!(feature
1439 .context_kinds
1440 .contains(&Kind::try_from("org").unwrap()));
1441
1442 let fallthrough_summary = feature.counters.get(&fallthrough_key);
1443 if let Some(VariationSummary { count: c, value: v }) = fallthrough_summary {
1444 assert_eq!(*c, 1);
1445 assert_eq!(*v, value);
1446 } else {
1447 panic!("Fallthrough summary is wrong type");
1448 }
1449
1450 summary.add(&fallthrough_request);
1451 let feature = summary
1452 .features
1453 .get(&flag.key)
1454 .expect("Failed to get expected feature.");
1455 let fallthrough_summary = feature
1456 .counters
1457 .get(&fallthrough_key)
1458 .expect("Failed to get counters");
1459 assert_eq!(fallthrough_summary.count, 2);
1460 assert_eq!(2, feature.context_kinds.len());
1461 }
1462
1463 #[test]
1464 fn event_factory_unknown_flags_do_not_track_events() {
1465 let event_factory = EventFactory::new(true);
1466 let context = ContextBuilder::new("bob")
1467 .build()
1468 .expect("Failed to create context");
1469 let detail = Detail {
1470 value: Some(FlagValue::from(false)),
1471 variation_index: Some(1),
1472 reason: Reason::Off,
1473 };
1474 let event =
1475 event_factory.new_unknown_flag_event("myFlag", context, detail, FlagValue::Bool(true));
1476
1477 if let InputEvent::FeatureRequest(event) = event {
1478 assert!(!event.track_events);
1479 } else {
1480 panic!("Event should be a feature request type");
1481 }
1482 }
1483
1484 #[test_case(true, true, false, Reason::Off, true, true)]
1486 #[test_case(true, false, false, Reason::Off, false, true)]
1487 #[test_case(false, true, false, Reason::Off, true, false)]
1488 #[test_case(false, false, false, Reason::Off, false, false)]
1489 #[test_case(true, false, true, Reason::Off, false, true)]
1491 #[test_case(true, false, true, Reason::Fallthrough { in_experiment: false }, true, true)]
1492 #[test_case(true, false, false, Reason::Fallthrough { in_experiment: false }, false, true)]
1493 #[test_case(false, false, true, Reason::Off, false, false)]
1494 #[test_case(false, false, true, Reason::Fallthrough { in_experiment: false }, true, true)]
1495 #[test_case(false, false, false, Reason::Fallthrough { in_experiment: false }, false, false)]
1496 #[test_case(true, false, false, Reason::Fallthrough { in_experiment: true }, true, true)]
1498 #[test_case(false, false, false, Reason::Fallthrough { in_experiment: true }, true, true)]
1499 fn event_factory_eval_tracks_events(
1500 event_factory_send_events: bool,
1501 flag_track_events: bool,
1502 flag_track_events_fallthrough: bool,
1503 reason: Reason,
1504 should_events_be_tracked: bool,
1505 should_include_reason: bool,
1506 ) {
1507 let event_factory = EventFactory::new(event_factory_send_events);
1508 let mut flag = basic_flag("myFlag");
1509 flag.track_events = flag_track_events;
1510 flag.track_events_fallthrough = flag_track_events_fallthrough;
1511
1512 let context = ContextBuilder::new("bob")
1513 .build()
1514 .expect("Failed to create context");
1515 let detail = Detail {
1516 value: Some(FlagValue::from(false)),
1517 variation_index: Some(1),
1518 reason,
1519 };
1520 let event = event_factory.new_eval_event(
1521 "myFlag",
1522 context,
1523 &flag,
1524 detail,
1525 FlagValue::Bool(true),
1526 None,
1527 );
1528
1529 if let InputEvent::FeatureRequest(event) = event {
1530 assert_eq!(event.track_events, should_events_be_tracked);
1531 assert_eq!(event.reason.is_some(), should_include_reason);
1532 } else {
1533 panic!("Event should be a feature request type");
1534 }
1535 }
1536
1537 #[test_case(true, 0, false, true, true)]
1538 #[test_case(true, 0, true, true, true)]
1539 #[test_case(true, 1, false, false, true)]
1540 #[test_case(true, 1, true, true, true)]
1541 #[test_case(false, 0, false, true, true)]
1542 #[test_case(false, 0, true, true, true)]
1543 #[test_case(false, 1, false, false, false)]
1544 #[test_case(false, 1, true, true, true)]
1545 fn event_factory_eval_tracks_events_for_rule_matches(
1546 event_factory_send_events: bool,
1547 rule_index: usize,
1548 rule_in_experiment: bool,
1549 should_events_be_tracked: bool,
1550 should_include_reason: bool,
1551 ) {
1552 let event_factory = EventFactory::new(event_factory_send_events);
1553 let flag: Flag = serde_json::from_value(json!({
1554 "key": "with_rule",
1555 "on": true,
1556 "targets": [],
1557 "prerequisites": [],
1558 "rules": [
1559 {
1560 "id": "rule-0",
1561 "clauses": [{
1562 "attribute": "key",
1563 "negate": false,
1564 "op": "matches",
1565 "values": ["do-track"]
1566 }],
1567 "trackEvents": true,
1568 "variation": 1
1569 },
1570 {
1571 "id": "rule-1",
1572 "clauses": [{
1573 "attribute": "key",
1574 "negate": false,
1575 "op": "matches",
1576 "values": ["no-track"]
1577 }],
1578 "trackEvents": false,
1579 "variation": 1
1580 }
1581 ],
1582 "fallthrough": {"variation": 0},
1583 "trackEventsFallthrough": false,
1584 "offVariation": 0,
1585 "clientSideAvailability": {
1586 "usingMobileKey": false,
1587 "usingEnvironmentId": false
1588 },
1589 "salt": "kosher",
1590 "version": 2,
1591 "variations": [false, true]
1592 }))
1593 .unwrap();
1594
1595 let context = ContextBuilder::new("do-track")
1596 .build()
1597 .expect("Failed to create context");
1598 let detail = Detail {
1599 value: Some(FlagValue::from(false)),
1600 variation_index: Some(1),
1601 reason: Reason::RuleMatch {
1602 rule_index,
1603 rule_id: format!("rule-{}", rule_index),
1604 in_experiment: rule_in_experiment,
1605 },
1606 };
1607 let event = event_factory.new_eval_event(
1608 "myFlag",
1609 context,
1610 &flag,
1611 detail,
1612 FlagValue::Bool(true),
1613 None,
1614 );
1615
1616 if let InputEvent::FeatureRequest(event) = event {
1617 assert_eq!(event.track_events, should_events_be_tracked);
1618 assert_eq!(event.reason.is_some(), should_include_reason);
1619 } else {
1620 panic!("Event should be a feature request type");
1621 }
1622 }
1623}