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