Skip to content

Commit

Permalink
feat: Add option to omit anonymous users from index and identify even…
Browse files Browse the repository at this point in the history
…ts (#89)
  • Loading branch information
keelerm84 authored Jul 10, 2024
1 parent 5a52e41 commit 78c9668
Show file tree
Hide file tree
Showing 6 changed files with 213 additions and 17 deletions.
1 change: 1 addition & 0 deletions contract-tests/src/client_entity.rs
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ impl ClientEntity {
processor_builder.private_attributes(attributes);
}
processor_builder.https_connector(connector.clone());
processor_builder.omit_anonymous_contexts(events.omit_anonymous_contexts);

config_builder.event_processor(&processor_builder)
} else {
Expand Down
3 changes: 3 additions & 0 deletions contract-tests/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ pub struct EventParameters {
pub all_attributes_private: bool,
pub global_private_attributes: Option<HashSet<Reference>>,
pub flush_interval_ms: Option<u64>,
#[serde(default = "bool::default")]
pub omit_anonymous_contexts: bool,
}

#[derive(Deserialize, Debug)]
Expand Down Expand Up @@ -103,6 +105,7 @@ async fn status() -> impl Responder {
"secure-mode-hash".to_string(),
"inline-context".to_string(),
"anonymous-redaction".to_string(),
"omit-anonymous-contexts".to_string(),
],
})
}
Expand Down
2 changes: 1 addition & 1 deletion launchdarkly-server-sdk/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ lazy_static = "1.4.0"
log = "0.4.14"
lru = { version = "0.12.0", default-features = false }
ring = "0.17.5"
launchdarkly-server-sdk-evaluation = "1.2.0"
launchdarkly-server-sdk-evaluation = "2.0.0"
serde = { version = "1.0.132", features = ["derive"] }
serde_json = { version = "1.0.73", features = ["float_roundtrip"] }
thiserror = "1.0"
Expand Down
210 changes: 194 additions & 16 deletions launchdarkly-server-sdk/src/events/dispatcher.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
use crossbeam_channel::{bounded, select, tick, Receiver, Sender};
use std::collections::HashSet;
use std::iter::FromIterator;
use std::time::SystemTime;

use launchdarkly_server_sdk_evaluation::Context;
use lru::LruCache;

use super::event::FeatureRequestEvent;
use super::event::{BaseEvent, FeatureRequestEvent, IndexEvent};
use super::sender::EventSenderResult;
use super::{
event::{EventSummary, InputEvent, OutputEvent},
Expand Down Expand Up @@ -192,11 +190,13 @@ impl EventDispatcher {
self.events_configuration.private_attributes.clone(),
);

if self.notice_context(&fre.base.context) {
self.outbox.add_event(OutputEvent::Index(fre.to_index_event(
if let Some(context) = self.get_indexable_context(&fre.base) {
let base = BaseEvent::new(fre.base.creation_date, context).into_inline(
self.events_configuration.all_attributes_private,
self.events_configuration.private_attributes.clone(),
)));
);
self.outbox
.add_event(OutputEvent::Index(IndexEvent::from(base)));
}

let now = match SystemTime::now().duration_since(std::time::UNIX_EPOCH) {
Expand All @@ -219,30 +219,53 @@ impl EventDispatcher {
self.outbox.add_event(OutputEvent::FeatureRequest(inlined));
}
}
InputEvent::Identify(identify) => {
InputEvent::Identify(mut identify) => {
if self.events_configuration.omit_anonymous_contexts {
match identify.base.context.without_anonymous_contexts() {
Ok(context) => identify.base.context = context,
Err(_) => return,
}
}

self.notice_context(&identify.base.context);
self.outbox
.add_event(OutputEvent::Identify(identify.into_inline(
self.events_configuration.all_attributes_private,
HashSet::from_iter(
self.events_configuration.private_attributes.iter().cloned(),
),
self.events_configuration.private_attributes.clone(),
)));
}
InputEvent::Custom(custom) => {
if self.notice_context(&custom.base.context) {
if let Some(context) = self.get_indexable_context(&custom.base) {
let base = BaseEvent::new(custom.base.creation_date, context).into_inline(
self.events_configuration.all_attributes_private,
self.events_configuration.private_attributes.clone(),
);
self.outbox
.add_event(OutputEvent::Index(custom.to_index_event(
self.events_configuration.all_attributes_private,
self.events_configuration.private_attributes.clone(),
)));
.add_event(OutputEvent::Index(IndexEvent::from(base)));
}

self.outbox.add_event(OutputEvent::Custom(custom));
}
}
}

fn get_indexable_context(&mut self, event: &BaseEvent) -> Option<Context> {
let context = match self.events_configuration.omit_anonymous_contexts {
true => event.context.without_anonymous_contexts(),
false => Ok(event.context.clone()),
};

if let Ok(ctx) = context {
if self.notice_context(&ctx) {
return Some(ctx);
}

return None;
}

None
}

fn notice_context(&mut self, context: &Context) -> bool {
let key = context.canonical_key();

Expand Down Expand Up @@ -273,7 +296,9 @@ mod tests {
use crate::events::event::{EventFactory, OutputEvent};
use crate::events::{create_event_sender, create_events_configuration};
use crate::test_common::basic_flag;
use launchdarkly_server_sdk_evaluation::{ContextBuilder, Detail, FlagValue, Reason};
use launchdarkly_server_sdk_evaluation::{
ContextBuilder, Detail, FlagValue, MultiContextBuilder, Reason,
};
use test_case::test_case;

#[test]
Expand Down Expand Up @@ -359,6 +384,48 @@ mod tests {
assert_eq!(1, dispatcher.outbox.summary.features.len());
}

#[test]
fn dispatcher_strips_anonymous_contexts_from_index_for_feature_request_events() {
let (event_sender, _) = create_event_sender();
let mut events_configuration =
create_events_configuration(event_sender, Duration::from_secs(100));
events_configuration.omit_anonymous_contexts = true;
let mut dispatcher = create_dispatcher(events_configuration);

let context = ContextBuilder::new("context")
.anonymous(true)
.build()
.expect("Failed to create context");
let mut flag = basic_flag("flag");
flag.debug_events_until_date = Some(64_060_606_800_000);
flag.track_events = true;

let detail = Detail {
value: Some(FlagValue::from(false)),
variation_index: Some(1),
reason: Reason::Fallthrough {
in_experiment: false,
},
};

let event_factory = EventFactory::new(true);
let feature_request_event = event_factory.new_eval_event(
&flag.key,
context,
&flag,
detail,
FlagValue::from(false),
None,
);

dispatcher.process_event(feature_request_event);
assert_eq!(2, dispatcher.outbox.events.len());
assert_eq!("debug", dispatcher.outbox.events[0].kind());
assert_eq!("feature", dispatcher.outbox.events[1].kind());
assert_eq!(0, dispatcher.context_keys.len());
assert_eq!(1, dispatcher.outbox.summary.features.len());
}

#[test_case(0, 64_060_606_800_000, vec!["debug", "index", "summary"])]
#[test_case(64_060_606_800_000, 64_060_606_800_000, vec!["index", "summary"])]
#[test_case(64_060_606_800_001, 64_060_606_800_000, vec!["index", "summary"])]
Expand Down Expand Up @@ -446,6 +513,61 @@ mod tests {
assert_eq!(1, dispatcher.context_keys.len());
}

#[test]
fn dispatcher_can_ignore_identify_if_anonymous() {
let (event_sender, _) = create_event_sender();
let mut events_configuration =
create_events_configuration(event_sender, Duration::from_secs(100));
events_configuration.omit_anonymous_contexts = true;
let mut dispatcher = create_dispatcher(events_configuration);

let context = ContextBuilder::new("context")
.anonymous(true)
.build()
.expect("Failed to create context");
let event_factory = EventFactory::new(true);

dispatcher.process_event(event_factory.new_identify(context));
assert_eq!(0, dispatcher.outbox.events.len());
assert_eq!(0, dispatcher.context_keys.len());
}

#[test]
fn dispatcher_strips_anon_contexts_from_multi_kind_identify() {
let (event_sender, _) = create_event_sender();
let mut events_configuration =
create_events_configuration(event_sender, Duration::from_secs(100));
events_configuration.omit_anonymous_contexts = true;
let mut dispatcher = create_dispatcher(events_configuration);

let user_context = ContextBuilder::new("user")
.anonymous(true)
.build()
.expect("Failed to create context");
let org_context = ContextBuilder::new("org")
.kind("org")
.build()
.expect("Failed to create context");
let context = MultiContextBuilder::new()
.add_context(user_context)
.add_context(org_context)
.build()
.expect("Failed to create context");

let event_factory = EventFactory::new(true);

dispatcher.process_event(event_factory.new_identify(context));
assert_eq!(1, dispatcher.outbox.events.len());
assert_eq!("identify", dispatcher.outbox.events[0].kind());
assert_eq!(1, dispatcher.context_keys.len());

if let OutputEvent::Identify(identify) = &dispatcher.outbox.events[0] {
assert_eq!("org:org", identify.base.context.canonical_key());
} else {
panic!("Expected an identify event");
}
}

#[test]
fn dispatcher_adds_index_on_custom_event() {
let (event_sender, _) = create_event_sender();
Expand All @@ -468,6 +590,62 @@ mod tests {
assert_eq!(1, dispatcher.context_keys.len());
}

#[test]
fn dispatcher_can_strip_anonymous_from_index_events() {
let (event_sender, _) = create_event_sender();
let mut events_configuration =
create_events_configuration(event_sender, Duration::from_secs(100));
events_configuration.omit_anonymous_contexts = true;
let mut dispatcher = create_dispatcher(events_configuration);

let context = ContextBuilder::new("context")
.anonymous(true)
.build()
.expect("Failed to create context");
let event_factory = EventFactory::new(true);
let custom_event = event_factory
.new_custom(context, "context", None, "")
.expect("failed to make new custom event");

dispatcher.process_event(custom_event);
assert_eq!(1, dispatcher.outbox.events.len());
assert_eq!("custom", dispatcher.outbox.events[0].kind());
assert_eq!(0, dispatcher.context_keys.len());
}

#[test]
fn dispatcher_can_strip_anonymous_from_index_events_with_multi_kinds() {
let (event_sender, _) = create_event_sender();
let mut events_configuration =
create_events_configuration(event_sender, Duration::from_secs(100));
events_configuration.omit_anonymous_contexts = true;
let mut dispatcher = create_dispatcher(events_configuration);

let user_context = ContextBuilder::new("user")
.anonymous(true)
.build()
.expect("Failed to create context");
let org_context = ContextBuilder::new("org")
.kind("org")
.build()
.expect("Failed to create context");
let context = MultiContextBuilder::new()
.add_context(user_context)
.add_context(org_context)
.build()
.expect("Failed to create context");
let event_factory = EventFactory::new(true);
let custom_event = event_factory
.new_custom(context, "context", None, "")
.expect("failed to make new custom event");

dispatcher.process_event(custom_event);
assert_eq!(2, dispatcher.outbox.events.len());
assert_eq!("index", dispatcher.outbox.events[0].kind());
assert_eq!("custom", dispatcher.outbox.events[1].kind());
assert_eq!(1, dispatcher.context_keys.len());
}

#[test]
fn can_process_events_successfully() {
let (event_sender, event_rx) = create_event_sender();
Expand Down
2 changes: 2 additions & 0 deletions launchdarkly-server-sdk/src/events/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ pub struct EventsConfiguration {
context_keys_flush_interval: Duration,
all_attributes_private: bool,
private_attributes: HashSet<Reference>,
omit_anonymous_contexts: bool,
}

#[cfg(test)]
Expand All @@ -35,6 +36,7 @@ fn create_events_configuration(
context_keys_flush_interval: Duration::from_secs(100),
all_attributes_private: false,
private_attributes: HashSet::new(),
omit_anonymous_contexts: false,
}
}

Expand Down
12 changes: 12 additions & 0 deletions launchdarkly-server-sdk/src/events/processor_builders.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ pub struct EventProcessorBuilder<C> {
all_attributes_private: bool,
private_attributes: HashSet<Reference>,
connector: Option<C>,
omit_anonymous_contexts: bool,
// diagnostic_recording_interval: Duration
}

Expand Down Expand Up @@ -149,6 +150,7 @@ where
context_keys_flush_interval: self.context_keys_flush_interval,
all_attributes_private: self.all_attributes_private,
private_attributes: self.private_attributes.clone(),
omit_anonymous_contexts: self.omit_anonymous_contexts,
};

let events_processor =
Expand All @@ -174,6 +176,7 @@ impl<C> EventProcessorBuilder<C> {
event_sender: None,
all_attributes_private: false,
private_attributes: HashSet::new(),
omit_anonymous_contexts: false,
connector: None,
}
}
Expand Down Expand Up @@ -246,6 +249,15 @@ impl<C> EventProcessorBuilder<C> {
self
}

/// Sets whether anonymous contexts should be omitted from index and identify events.
///
/// The default is false, meaning that anonymous contexts will be included in index and
/// identify events.
pub fn omit_anonymous_contexts(&mut self, omit: bool) -> &mut Self {
self.omit_anonymous_contexts = omit;
self
}

#[cfg(test)]
pub fn event_sender(&mut self, event_sender: Arc<dyn EventSender>) -> &mut Self {
self.event_sender = Some(event_sender);
Expand Down

0 comments on commit 78c9668

Please sign in to comment.