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