diff --git a/rust/feature-flags/src/v0_endpoint.rs b/rust/feature-flags/src/api/endpoint.rs similarity index 95% rename from rust/feature-flags/src/v0_endpoint.rs rename to rust/feature-flags/src/api/endpoint.rs index 9adfa67e882..e995cfd5dc1 100644 --- a/rust/feature-flags/src/v0_endpoint.rs +++ b/rust/feature-flags/src/api/endpoint.rs @@ -1,8 +1,9 @@ use std::net::IpAddr; use crate::{ - api::{FlagError, FlagsResponse}, - request_handler::{process_request, FlagsQueryParams, RequestContext}, + api::errors::FlagError, + api::handler::{process_request, FlagsQueryParams, RequestContext}, + api::types::FlagsResponse, router, }; // TODO: stream this instead diff --git a/rust/feature-flags/src/api.rs b/rust/feature-flags/src/api/errors.rs similarity index 84% rename from rust/feature-flags/src/api.rs rename to rust/feature-flags/src/api/errors.rs index be21c1c37f5..d2f1de10a3f 100644 --- a/rust/feature-flags/src/api.rs +++ b/rust/feature-flags/src/api/errors.rs @@ -1,60 +1,9 @@ -use std::collections::HashMap; - use axum::http::StatusCode; use axum::response::{IntoResponse, Response}; -use serde::{Deserialize, Serialize}; use thiserror::Error; -use crate::database::CustomDatabaseError; -use crate::redis::CustomRedisError; - -#[derive(Debug, PartialEq, Eq, Deserialize, Serialize)] -pub enum FlagsResponseCode { - Ok = 1, -} - -#[derive(Debug, PartialEq, Eq, Deserialize, Serialize)] -#[serde(untagged)] -pub enum FlagValue { - Boolean(bool), - String(String), -} - -// TODO the following two types are kinda general, maybe we should move them to a shared module -#[derive(Debug, PartialEq, Eq, Deserialize, Serialize)] -#[serde(untagged)] -pub enum BooleanOrStringObject { - Boolean(bool), - Object(HashMap), -} - -#[derive(Debug, PartialEq, Eq, Deserialize, Serialize)] -#[serde(untagged)] -pub enum BooleanOrBooleanObject { - Boolean(bool), - Object(HashMap), -} - -#[derive(Debug, PartialEq, Eq, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct FlagsResponse { - pub error_while_computing_flags: bool, - pub feature_flags: HashMap, - // TODO support the other fields in the payload - // pub config: HashMap, - // pub toolbar_params: HashMap, - // pub is_authenticated: bool, - // pub supported_compression: Vec, - // pub session_recording: bool, - // pub feature_flag_payloads: HashMap, - // pub capture_performance: BooleanOrBooleanObject, - // #[serde(rename = "autocapture_opt_out")] - // pub autocapture_opt_out: bool, - // pub autocapture_exceptions: BooleanOrStringObject, - // pub surveys: bool, - // pub heatmaps: bool, - // pub site_apps: Vec, -} +use crate::client::database::CustomDatabaseError; +use crate::client::redis::CustomRedisError; #[derive(Error, Debug)] pub enum ClientFacingError { diff --git a/rust/feature-flags/src/request_handler.rs b/rust/feature-flags/src/api/handler.rs similarity index 97% rename from rust/feature-flags/src/request_handler.rs rename to rust/feature-flags/src/api/handler.rs index 5ef43896e64..26c75276785 100644 --- a/rust/feature-flags/src/request_handler.rs +++ b/rust/feature-flags/src/api/handler.rs @@ -1,11 +1,12 @@ use crate::{ - api::{FlagError, FlagsResponse}, - cohort_cache::CohortCacheManager, - database::Client, - flag_definitions::FeatureFlagList, - flag_matching::{FeatureFlagMatcher, GroupTypeMappingCache}, - flag_request::FlagRequest, - geoip::GeoIpClient, + api::errors::FlagError, + api::types::FlagsResponse, + client::database::Client, + client::geoip::GeoIpClient, + cohort::cohort_cache_manager::CohortCacheManager, + flags::flag_matching::{FeatureFlagMatcher, GroupTypeMappingCache}, + flags::flag_models::FeatureFlagList, + flags::flag_request::FlagRequest, router, }; use axum::{extract::State, http::HeaderMap}; @@ -254,10 +255,13 @@ fn decompress_gzip(compressed: Bytes) -> Result { #[cfg(test)] mod tests { use crate::{ - api::FlagValue, + api::types::FlagValue, config::Config, - flag_definitions::{FeatureFlag, FlagFilters, FlagGroupType, OperatorType, PropertyFilter}, - test_utils::{insert_new_team_in_pg, setup_pg_reader_client, setup_pg_writer_client}, + flags::flag_models::{FeatureFlag, FlagFilters, FlagGroupType}, + properties::property_models::{OperatorType, PropertyFilter}, + utils::test_utils::{ + insert_new_team_in_pg, setup_pg_reader_client, setup_pg_writer_client, + }, }; use super::*; diff --git a/rust/feature-flags/src/api/mod.rs b/rust/feature-flags/src/api/mod.rs new file mode 100644 index 00000000000..7ccf71dc5fe --- /dev/null +++ b/rust/feature-flags/src/api/mod.rs @@ -0,0 +1,4 @@ +pub mod endpoint; +pub mod errors; +pub mod handler; +pub mod types; diff --git a/rust/feature-flags/src/api/types.rs b/rust/feature-flags/src/api/types.rs new file mode 100644 index 00000000000..3eb81b7d1ad --- /dev/null +++ b/rust/feature-flags/src/api/types.rs @@ -0,0 +1,21 @@ +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +#[derive(Debug, PartialEq, Eq, Deserialize, Serialize)] +pub enum FlagsResponseCode { + Ok = 1, +} + +#[derive(Debug, PartialEq, Eq, Deserialize, Serialize)] +#[serde(untagged)] +pub enum FlagValue { + Boolean(bool), + String(String), +} + +#[derive(Debug, PartialEq, Eq, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct FlagsResponse { + pub error_while_computing_flags: bool, + pub feature_flags: HashMap, +} diff --git a/rust/feature-flags/src/database.rs b/rust/feature-flags/src/client/database.rs similarity index 100% rename from rust/feature-flags/src/database.rs rename to rust/feature-flags/src/client/database.rs diff --git a/rust/feature-flags/src/geoip.rs b/rust/feature-flags/src/client/geoip.rs similarity index 100% rename from rust/feature-flags/src/geoip.rs rename to rust/feature-flags/src/client/geoip.rs diff --git a/rust/feature-flags/src/client/mod.rs b/rust/feature-flags/src/client/mod.rs new file mode 100644 index 00000000000..dc34e1e6c12 --- /dev/null +++ b/rust/feature-flags/src/client/mod.rs @@ -0,0 +1,3 @@ +pub mod database; +pub mod geoip; +pub mod redis; diff --git a/rust/feature-flags/src/redis.rs b/rust/feature-flags/src/client/redis.rs similarity index 100% rename from rust/feature-flags/src/redis.rs rename to rust/feature-flags/src/client/redis.rs diff --git a/rust/feature-flags/src/cohort_cache.rs b/rust/feature-flags/src/cohort/cohort_cache_manager.rs similarity index 94% rename from rust/feature-flags/src/cohort_cache.rs rename to rust/feature-flags/src/cohort/cohort_cache_manager.rs index 68894c19f88..54955356593 100644 --- a/rust/feature-flags/src/cohort_cache.rs +++ b/rust/feature-flags/src/cohort/cohort_cache_manager.rs @@ -1,6 +1,6 @@ -use crate::api::FlagError; -use crate::cohort_models::Cohort; -use crate::flag_matching::{PostgresReader, TeamId}; +use crate::api::errors::FlagError; +use crate::cohort::cohort_models::Cohort; +use crate::flags::flag_matching::{PostgresReader, TeamId}; use moka::future::Cache; use std::time::Duration; @@ -74,8 +74,8 @@ impl CohortCacheManager { #[cfg(test)] mod tests { use super::*; - use crate::cohort_models::Cohort; - use crate::test_utils::{ + use crate::cohort::cohort_models::Cohort; + use crate::utils::test_utils::{ insert_cohort_for_team_in_pg, insert_new_team_in_pg, setup_pg_reader_client, setup_pg_writer_client, }; @@ -84,15 +84,15 @@ mod tests { /// Helper function to setup a new team for testing. async fn setup_test_team( - writer_client: Arc, + writer_client: Arc, ) -> Result { - let team = crate::test_utils::insert_new_team_in_pg(writer_client, None).await?; + let team = insert_new_team_in_pg(writer_client, None).await?; Ok(team.id) } /// Helper function to insert a cohort for a team. async fn setup_test_cohort( - writer_client: Arc, + writer_client: Arc, team_id: TeamId, name: Option, ) -> Result { diff --git a/rust/feature-flags/src/cohort_models.rs b/rust/feature-flags/src/cohort/cohort_models.rs similarity index 95% rename from rust/feature-flags/src/cohort_models.rs rename to rust/feature-flags/src/cohort/cohort_models.rs index d1099839017..947668b6fdb 100644 --- a/rust/feature-flags/src/cohort_models.rs +++ b/rust/feature-flags/src/cohort/cohort_models.rs @@ -1,4 +1,4 @@ -use crate::flag_definitions::PropertyFilter; +use crate::properties::property_models::PropertyFilter; use serde::{Deserialize, Serialize}; use sqlx::FromRow; diff --git a/rust/feature-flags/src/cohort_operations.rs b/rust/feature-flags/src/cohort/cohort_operations.rs similarity index 97% rename from rust/feature-flags/src/cohort_operations.rs rename to rust/feature-flags/src/cohort/cohort_operations.rs index ea4214ccdc0..b987ae3e225 100644 --- a/rust/feature-flags/src/cohort_operations.rs +++ b/rust/feature-flags/src/cohort/cohort_operations.rs @@ -2,8 +2,11 @@ use std::collections::HashSet; use std::sync::Arc; use tracing::instrument; -use crate::cohort_models::{Cohort, CohortId, CohortProperty, InnerCohortProperty}; -use crate::{api::FlagError, database::Client as DatabaseClient, flag_definitions::PropertyFilter}; +use crate::cohort::cohort_models::{Cohort, CohortId, CohortProperty, InnerCohortProperty}; +use crate::{ + api::errors::FlagError, client::database::Client as DatabaseClient, + properties::property_models::PropertyFilter, +}; impl Cohort { /// Returns a cohort from postgres given a cohort_id and team_id @@ -185,8 +188,8 @@ impl InnerCohortProperty { mod tests { use super::*; use crate::{ - cohort_models::{CohortPropertyType, CohortValues}, - test_utils::{ + cohort::cohort_models::{CohortPropertyType, CohortValues}, + utils::test_utils::{ insert_cohort_for_team_in_pg, insert_new_team_in_pg, setup_pg_reader_client, setup_pg_writer_client, }, diff --git a/rust/feature-flags/src/cohort/mod.rs b/rust/feature-flags/src/cohort/mod.rs new file mode 100644 index 00000000000..bf51554a830 --- /dev/null +++ b/rust/feature-flags/src/cohort/mod.rs @@ -0,0 +1,3 @@ +pub mod cohort_cache_manager; +pub mod cohort_models; +pub mod cohort_operations; diff --git a/rust/feature-flags/src/flag_analytics.rs b/rust/feature-flags/src/flags/flag_analytics.rs similarity index 94% rename from rust/feature-flags/src/flag_analytics.rs rename to rust/feature-flags/src/flags/flag_analytics.rs index 6bdfcb4b2e9..fe65d08a602 100644 --- a/rust/feature-flags/src/flag_analytics.rs +++ b/rust/feature-flags/src/flags/flag_analytics.rs @@ -2,8 +2,8 @@ use anyhow::Result; use std::sync::Arc; use std::time::{SystemTime, UNIX_EPOCH}; -use crate::flag_request::FlagRequestType; -use crate::redis::{Client as RedisClient, CustomRedisError}; +use crate::client::redis::{Client as RedisClient, CustomRedisError}; +use crate::flags::flag_request::FlagRequestType; const CACHE_BUCKET_SIZE: u64 = 60 * 2; // duration in seconds @@ -37,7 +37,7 @@ pub async fn increment_request_count( #[cfg(test)] mod tests { use super::*; - use crate::test_utils::setup_redis_client; + use crate::utils::test_utils::setup_redis_client; #[tokio::test] async fn test_get_team_request_key() { diff --git a/rust/feature-flags/src/feature_flag_match_reason.rs b/rust/feature-flags/src/flags/flag_match_reason.rs similarity index 100% rename from rust/feature-flags/src/feature_flag_match_reason.rs rename to rust/feature-flags/src/flags/flag_match_reason.rs diff --git a/rust/feature-flags/src/flag_matching.rs b/rust/feature-flags/src/flags/flag_matching.rs similarity index 99% rename from rust/feature-flags/src/flag_matching.rs rename to rust/feature-flags/src/flags/flag_matching.rs index d9332fce4e4..ea04f6fb00b 100644 --- a/rust/feature-flags/src/flag_matching.rs +++ b/rust/feature-flags/src/flags/flag_matching.rs @@ -1,14 +1,14 @@ -use crate::{ - api::{FlagError, FlagValue, FlagsResponse}, - cohort_cache::CohortCacheManager, - cohort_models::{Cohort, CohortId}, - database::Client as DatabaseClient, - feature_flag_match_reason::FeatureFlagMatchReason, - flag_definitions::{FeatureFlag, FeatureFlagList, FlagGroupType, OperatorType, PropertyFilter}, - metrics_consts::{FLAG_EVALUATION_ERROR_COUNTER, FLAG_HASH_KEY_WRITES_COUNTER}, - metrics_utils::parse_exception_for_prometheus_label, - property_matching::match_property, -}; +use crate::api::errors::FlagError; +use crate::api::types::{FlagValue, FlagsResponse}; +use crate::client::database::Client as DatabaseClient; +use crate::cohort::cohort_cache_manager::CohortCacheManager; +use crate::cohort::cohort_models::{Cohort, CohortId}; +use crate::flags::flag_match_reason::FeatureFlagMatchReason; +use crate::flags::flag_models::{FeatureFlag, FeatureFlagList, FlagGroupType}; +use crate::metrics::metrics_consts::{FLAG_EVALUATION_ERROR_COUNTER, FLAG_HASH_KEY_WRITES_COUNTER}; +use crate::metrics::metrics_utils::parse_exception_for_prometheus_label; +use crate::properties::property_matching::match_property; +use crate::properties::property_models::{OperatorType, PropertyFilter}; use anyhow::Result; use common_metrics::inc; use petgraph::algo::{is_cyclic_directed, toposort}; @@ -1796,11 +1796,11 @@ mod tests { use super::*; use crate::{ - flag_definitions::{ + flags::flag_models::{ FeatureFlagRow, FlagFilters, MultivariateFlagOptions, MultivariateFlagVariant, - OperatorType, }, - test_utils::{ + properties::property_models::OperatorType, + utils::test_utils::{ add_person_to_cohort, get_person_id_by_distinct_id, insert_cohort_for_team_in_pg, insert_flag_for_team_in_pg, insert_new_team_in_pg, insert_person_for_team_in_pg, setup_pg_reader_client, setup_pg_writer_client, diff --git a/rust/feature-flags/src/flags/flag_models.rs b/rust/feature-flags/src/flags/flag_models.rs new file mode 100644 index 00000000000..7c76c2531b7 --- /dev/null +++ b/rust/feature-flags/src/flags/flag_models.rs @@ -0,0 +1,70 @@ +use serde::{Deserialize, Serialize}; + +use crate::properties::property_models::PropertyFilter; + +// TRICKY: This cache data is coming from django-redis. If it ever goes out of sync, we'll bork. +// TODO: Add integration tests across repos to ensure this doesn't happen. +pub const TEAM_FLAGS_CACHE_PREFIX: &str = "posthog:1:team_feature_flags_"; + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct FlagGroupType { + pub properties: Option>, + pub rollout_percentage: Option, + pub variant: Option, +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct MultivariateFlagVariant { + pub key: String, + pub name: Option, + pub rollout_percentage: f64, +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct MultivariateFlagOptions { + pub variants: Vec, +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct FlagFilters { + pub groups: Vec, + pub multivariate: Option, + pub aggregation_group_type_index: Option, + pub payloads: Option, + pub super_groups: Option>, +} + +// TODO: see if you can combine these two structs, like we do with cohort models +// this will require not deserializing on read and instead doing it lazily, on-demand +// (which, tbh, is probably a better idea) +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct FeatureFlag { + pub id: i32, + pub team_id: i32, + pub name: Option, + pub key: String, + pub filters: FlagFilters, + #[serde(default)] + pub deleted: bool, + #[serde(default)] + pub active: bool, + #[serde(default)] + pub ensure_experience_continuity: bool, +} + +#[derive(Debug, Serialize, sqlx::FromRow)] +pub struct FeatureFlagRow { + pub id: i32, + pub team_id: i32, + pub name: Option, + pub key: String, + pub filters: serde_json::Value, + pub deleted: bool, + pub active: bool, + pub ensure_experience_continuity: bool, +} + +#[derive(Clone, Debug, Default, Deserialize, Serialize)] +pub struct FeatureFlagList { + pub flags: Vec, +} diff --git a/rust/feature-flags/src/flag_definitions.rs b/rust/feature-flags/src/flags/flag_operations.rs similarity index 94% rename from rust/feature-flags/src/flag_definitions.rs rename to rust/feature-flags/src/flags/flag_operations.rs index d62ecc9e0e0..0bb357b7eca 100644 --- a/rust/feature-flags/src/flag_definitions.rs +++ b/rust/feature-flags/src/flags/flag_operations.rs @@ -1,52 +1,12 @@ -use crate::{ - api::FlagError, cohort_models::CohortId, database::Client as DatabaseClient, - redis::Client as RedisClient, -}; -use serde::{Deserialize, Serialize}; +use crate::api::errors::FlagError; +use crate::client::database::Client as DatabaseClient; +use crate::client::redis::Client as RedisClient; +use crate::cohort::cohort_models::CohortId; +use crate::flags::flag_models::*; +use crate::properties::property_models::PropertyFilter; use std::sync::Arc; use tracing::instrument; -// TRICKY: This cache data is coming from django-redis. If it ever goes out of sync, we'll bork. -// TODO: Add integration tests across repos to ensure this doesn't happen. -pub const TEAM_FLAGS_CACHE_PREFIX: &str = "posthog:1:team_feature_flags_"; - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)] -#[serde(rename_all = "snake_case")] -pub enum OperatorType { - Exact, - IsNot, - Icontains, - NotIcontains, - Regex, - NotRegex, - Gt, - Lt, - Gte, - Lte, - IsSet, - IsNotSet, - IsDateExact, - IsDateAfter, - IsDateBefore, - In, - NotIn, -} - -#[derive(Debug, Clone, Deserialize, Serialize)] -pub struct PropertyFilter { - pub key: String, - // TODO: Probably need a default for value? - // incase operators like is_set, is_not_set are used - // not guaranteed to have a value, if say created via api - pub value: serde_json::Value, - pub operator: Option, - #[serde(rename = "type")] - // TODO: worth making a enum here to differentiate between cohort and person filters? - pub prop_type: String, - pub negation: Option, - pub group_type_index: Option, -} - impl PropertyFilter { /// Checks if the filter is a cohort filter pub fn is_cohort(&self) -> bool { @@ -63,64 +23,6 @@ impl PropertyFilter { } } -#[derive(Debug, Clone, Deserialize, Serialize)] -pub struct FlagGroupType { - pub properties: Option>, - pub rollout_percentage: Option, - pub variant: Option, -} - -#[derive(Debug, Clone, Deserialize, Serialize)] -pub struct MultivariateFlagVariant { - pub key: String, - pub name: Option, - pub rollout_percentage: f64, -} - -#[derive(Debug, Clone, Deserialize, Serialize)] -pub struct MultivariateFlagOptions { - pub variants: Vec, -} - -#[derive(Debug, Clone, Deserialize, Serialize)] -pub struct FlagFilters { - pub groups: Vec, - pub multivariate: Option, - pub aggregation_group_type_index: Option, - pub payloads: Option, - pub super_groups: Option>, -} - -// TODO: see if you can combine these two structs, like we do with cohort models -// this will require not deserializing on read and instead doing it lazily, on-demand -// (which, tbh, is probably a better idea) -#[derive(Debug, Clone, Deserialize, Serialize)] -pub struct FeatureFlag { - pub id: i32, - pub team_id: i32, - pub name: Option, - pub key: String, - pub filters: FlagFilters, - #[serde(default)] - pub deleted: bool, - #[serde(default)] - pub active: bool, - #[serde(default)] - pub ensure_experience_continuity: bool, -} - -#[derive(Debug, Serialize, sqlx::FromRow)] -pub struct FeatureFlagRow { - pub id: i32, - pub team_id: i32, - pub name: Option, - pub key: String, - pub filters: serde_json::Value, - pub deleted: bool, - pub active: bool, - pub ensure_experience_continuity: bool, -} - impl FeatureFlag { pub fn get_group_type_index(&self) -> Option { self.filters.aggregation_group_type_index @@ -146,11 +48,6 @@ impl FeatureFlag { } } -#[derive(Clone, Debug, Default, Deserialize, Serialize)] -pub struct FeatureFlagList { - pub flags: Vec, -} - impl FeatureFlagList { /// Returns feature flags from redis given a team_id #[instrument(skip_all)] @@ -243,14 +140,14 @@ impl FeatureFlagList { #[cfg(test)] mod tests { - use crate::flag_definitions; + use crate::{flags::flag_models::*, properties::property_models::OperatorType}; use rand::Rng; use serde_json::json; use std::time::Instant; use tokio::task; use super::*; - use crate::test_utils::{ + use crate::utils::test_utils::{ insert_flag_for_team_in_pg, insert_flags_for_team_in_redis, insert_new_team_in_pg, insert_new_team_in_redis, setup_invalid_pg_client, setup_pg_reader_client, setup_redis_client, @@ -803,6 +700,7 @@ mod tests { } } } + #[tokio::test] async fn test_flag_with_super_groups() { let redis_client = setup_redis_client(None); @@ -1114,7 +1012,7 @@ mod tests { redis_client .set( - format!("{}{}", flag_definitions::TEAM_FLAGS_CACHE_PREFIX, team.id), + format!("{}{}", TEAM_FLAGS_CACHE_PREFIX, team.id), "not a json".to_string(), ) .await diff --git a/rust/feature-flags/src/flag_request.rs b/rust/feature-flags/src/flags/flag_request.rs similarity index 96% rename from rust/feature-flags/src/flag_request.rs rename to rust/feature-flags/src/flags/flag_request.rs index 1cf64eb879a..89890505c6c 100644 --- a/rust/feature-flags/src/flag_request.rs +++ b/rust/feature-flags/src/flags/flag_request.rs @@ -7,8 +7,11 @@ use serde_json::Value; use tracing::instrument; use crate::{ - api::FlagError, database::Client as DatabaseClient, flag_definitions::FeatureFlagList, - metrics_consts::FLAG_CACHE_HIT_COUNTER, redis::Client as RedisClient, team::Team, + api::errors::FlagError, + client::{database::Client as DatabaseClient, redis::Client as RedisClient}, + flags::flag_models::FeatureFlagList, + metrics::metrics_consts::FLAG_CACHE_HIT_COUNTER, + team::team_models::Team, }; #[derive(Debug, Clone, Copy)] @@ -204,14 +207,17 @@ impl FlagRequest { mod tests { use std::collections::HashMap; - use crate::api::FlagError; - use crate::flag_definitions::{ - FeatureFlag, FeatureFlagList, FlagFilters, FlagGroupType, OperatorType, PropertyFilter, - TEAM_FLAGS_CACHE_PREFIX, + use crate::api::errors::FlagError; + use crate::flags::flag_models::{ + FeatureFlag, FeatureFlagList, FlagFilters, FlagGroupType, TEAM_FLAGS_CACHE_PREFIX, + }; + + use crate::flags::flag_request::FlagRequest; + use crate::properties::property_models::{OperatorType, PropertyFilter}; + use crate::team::team_models::Team; + use crate::utils::test_utils::{ + insert_new_team_in_redis, setup_pg_reader_client, setup_redis_client, }; - use crate::flag_request::FlagRequest; - use crate::team::Team; - use crate::test_utils::{insert_new_team_in_redis, setup_pg_reader_client, setup_redis_client}; use bytes::Bytes; use serde_json::json; diff --git a/rust/feature-flags/src/flags/mod.rs b/rust/feature-flags/src/flags/mod.rs new file mode 100644 index 00000000000..0555b993828 --- /dev/null +++ b/rust/feature-flags/src/flags/mod.rs @@ -0,0 +1,6 @@ +pub mod flag_analytics; +pub mod flag_match_reason; +pub mod flag_matching; +pub mod flag_models; +pub mod flag_operations; +pub mod flag_request; diff --git a/rust/feature-flags/src/lib.rs b/rust/feature-flags/src/lib.rs index 67659bfcf9d..9f2fa1d5d68 100644 --- a/rust/feature-flags/src/lib.rs +++ b/rust/feature-flags/src/lib.rs @@ -1,24 +1,13 @@ pub mod api; -pub mod cohort_cache; -pub mod cohort_models; -pub mod cohort_operations; +pub mod client; +pub mod cohort; pub mod config; -pub mod database; -pub mod feature_flag_match_reason; -pub mod flag_analytics; -pub mod flag_definitions; -pub mod flag_matching; -pub mod flag_request; -pub mod geoip; -pub mod metrics_consts; -pub mod metrics_utils; -pub mod property_matching; -pub mod redis; -pub mod request_handler; +pub mod flags; +pub mod metrics; +pub mod properties; pub mod router; pub mod server; pub mod team; -pub mod v0_endpoint; // Test modules don't need to be compiled with main binary // #[cfg(test)] @@ -26,4 +15,4 @@ pub mod v0_endpoint; // or make it a separate feature using cfg(feature = "integration-tests") // and then use this feature only in tests. // For now, ok to just include in binary -pub mod test_utils; +pub mod utils; diff --git a/rust/feature-flags/src/metrics_consts.rs b/rust/feature-flags/src/metrics/metrics_consts.rs similarity index 100% rename from rust/feature-flags/src/metrics_consts.rs rename to rust/feature-flags/src/metrics/metrics_consts.rs diff --git a/rust/feature-flags/src/metrics_utils.rs b/rust/feature-flags/src/metrics/metrics_utils.rs similarity index 98% rename from rust/feature-flags/src/metrics_utils.rs rename to rust/feature-flags/src/metrics/metrics_utils.rs index e17b4caf13d..1abd88e9474 100644 --- a/rust/feature-flags/src/metrics_utils.rs +++ b/rust/feature-flags/src/metrics/metrics_utils.rs @@ -1,4 +1,4 @@ -use crate::{api::FlagError, config::TeamIdsToTrack}; +use crate::{api::errors::FlagError, config::TeamIdsToTrack}; pub fn team_id_label_filter( team_ids_to_track: TeamIdsToTrack, diff --git a/rust/feature-flags/src/metrics/mod.rs b/rust/feature-flags/src/metrics/mod.rs new file mode 100644 index 00000000000..3b1364c3aed --- /dev/null +++ b/rust/feature-flags/src/metrics/mod.rs @@ -0,0 +1,2 @@ +pub mod metrics_consts; +pub mod metrics_utils; diff --git a/rust/feature-flags/src/properties/mod.rs b/rust/feature-flags/src/properties/mod.rs new file mode 100644 index 00000000000..2c3ad0067d3 --- /dev/null +++ b/rust/feature-flags/src/properties/mod.rs @@ -0,0 +1,2 @@ +pub mod property_matching; +pub mod property_models; diff --git a/rust/feature-flags/src/property_matching.rs b/rust/feature-flags/src/properties/property_matching.rs similarity index 99% rename from rust/feature-flags/src/property_matching.rs rename to rust/feature-flags/src/properties/property_matching.rs index 84479f13161..3389e82b211 100644 --- a/rust/feature-flags/src/property_matching.rs +++ b/rust/feature-flags/src/properties/property_matching.rs @@ -1,6 +1,6 @@ use std::collections::HashMap; -use crate::flag_definitions::{OperatorType, PropertyFilter}; +use crate::properties::property_models::{OperatorType, PropertyFilter}; use regex::Regex; use serde_json::Value; diff --git a/rust/feature-flags/src/properties/property_models.rs b/rust/feature-flags/src/properties/property_models.rs new file mode 100644 index 00000000000..620c9175348 --- /dev/null +++ b/rust/feature-flags/src/properties/property_models.rs @@ -0,0 +1,38 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)] +#[serde(rename_all = "snake_case")] +pub enum OperatorType { + Exact, + IsNot, + Icontains, + NotIcontains, + Regex, + NotRegex, + Gt, + Lt, + Gte, + Lte, + IsSet, + IsNotSet, + IsDateExact, + IsDateAfter, + IsDateBefore, + In, + NotIn, +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct PropertyFilter { + pub key: String, + // TODO: Probably need a default for value? + // incase operators like is_set, is_not_set are used + // not guaranteed to have a value, if say created via api + pub value: serde_json::Value, + pub operator: Option, + #[serde(rename = "type")] + // TODO: worth making a enum here to differentiate between cohort and person filters? + pub prop_type: String, + pub negation: Option, + pub group_type_index: Option, +} diff --git a/rust/feature-flags/src/router.rs b/rust/feature-flags/src/router.rs index e34ea31a3c6..46706586a07 100644 --- a/rust/feature-flags/src/router.rs +++ b/rust/feature-flags/src/router.rs @@ -9,13 +9,13 @@ use health::HealthRegistry; use tower::limit::ConcurrencyLimitLayer; use crate::{ - cohort_cache::CohortCacheManager, + api::endpoint, + client::{ + database::Client as DatabaseClient, geoip::GeoIpClient, redis::Client as RedisClient, + }, + cohort::cohort_cache_manager::CohortCacheManager, config::{Config, TeamIdsToTrack}, - database::Client as DatabaseClient, - geoip::GeoIpClient, - metrics_utils::team_id_label_filter, - redis::Client as RedisClient, - v0_endpoint, + metrics::metrics_utils::team_id_label_filter, }; #[derive(Clone)] @@ -56,7 +56,7 @@ where .route("/_liveness", get(move || ready(liveness.get_status()))); let flags_router = Router::new() - .route("/flags", post(v0_endpoint::flags).get(v0_endpoint::flags)) + .route("/flags", post(endpoint::flags).get(endpoint::flags)) .layer(ConcurrencyLimitLayer::new(config.max_concurrency)) .with_state(state); diff --git a/rust/feature-flags/src/server.rs b/rust/feature-flags/src/server.rs index 69ff759ddfc..12a79b4f1b4 100644 --- a/rust/feature-flags/src/server.rs +++ b/rust/feature-flags/src/server.rs @@ -6,11 +6,11 @@ use std::time::Duration; use health::{HealthHandle, HealthRegistry}; use tokio::net::TcpListener; -use crate::cohort_cache::CohortCacheManager; +use crate::client::database::get_pool; +use crate::client::geoip::GeoIpClient; +use crate::client::redis::RedisClient; +use crate::cohort::cohort_cache_manager::CohortCacheManager; use crate::config::Config; -use crate::database::get_pool; -use crate::geoip::GeoIpClient; -use crate::redis::RedisClient; use crate::router; pub async fn serve(config: Config, listener: TcpListener, shutdown: F) diff --git a/rust/feature-flags/src/team/mod.rs b/rust/feature-flags/src/team/mod.rs new file mode 100644 index 00000000000..4e6fe1869f0 --- /dev/null +++ b/rust/feature-flags/src/team/mod.rs @@ -0,0 +1,2 @@ +pub mod team_models; +pub mod team_operations; diff --git a/rust/feature-flags/src/team/team_models.rs b/rust/feature-flags/src/team/team_models.rs new file mode 100644 index 00000000000..29223825618 --- /dev/null +++ b/rust/feature-flags/src/team/team_models.rs @@ -0,0 +1,23 @@ +use serde::{Deserialize, Serialize}; + +// TRICKY: This cache data is coming from django-redis. If it ever goes out of sync, we'll bork. +// TODO: Add integration tests across repos to ensure this doesn't happen. +pub const TEAM_TOKEN_CACHE_PREFIX: &str = "posthog:1:team_token:"; + +#[derive(Clone, Debug, Deserialize, Serialize, sqlx::FromRow)] +pub struct Team { + pub id: i32, + pub name: String, + pub api_token: String, + // TODO: the following fields are used for the `/decide` response, + // but they're not used for flags and they don't live in redis. + // At some point I'll need to differentiate between teams in Redis and teams + // with additional fields in Postgres, since the Postgres team is a superset of the fields + // we use for flags, anyway. + // pub surveys_opt_in: bool, + // pub heatmaps_opt_in: bool, + // pub capture_performance_opt_in: bool, + // pub autocapture_web_vitals_opt_in: bool, + // pub autocapture_opt_out: bool, + // pub autocapture_exceptions_opt_in: bool, +} diff --git a/rust/feature-flags/src/team.rs b/rust/feature-flags/src/team/team_operations.rs similarity index 79% rename from rust/feature-flags/src/team.rs rename to rust/feature-flags/src/team/team_operations.rs index f13cf29094b..4f9b706153c 100644 --- a/rust/feature-flags/src/team.rs +++ b/rust/feature-flags/src/team/team_operations.rs @@ -1,34 +1,15 @@ -use serde::{Deserialize, Serialize}; use std::sync::Arc; use tracing::instrument; -use crate::{api::FlagError, database::Client as DatabaseClient, redis::Client as RedisClient}; - -// TRICKY: This cache data is coming from django-redis. If it ever goes out of sync, we'll bork. -// TODO: Add integration tests across repos to ensure this doesn't happen. -pub const TEAM_TOKEN_CACHE_PREFIX: &str = "posthog:1:team_token:"; - -#[derive(Clone, Debug, Deserialize, Serialize, sqlx::FromRow)] -pub struct Team { - pub id: i32, - pub name: String, - pub api_token: String, - // TODO: the following fields are used for the `/decide` response, - // but they're not used for flags and they don't live in redis. - // At some point I'll need to differentiate between teams in Redis and teams - // with additional fields in Postgres, since the Postgres team is a superset of the fields - // we use for flags, anyway. - // pub surveys_opt_in: bool, - // pub heatmaps_opt_in: bool, - // pub capture_performance_opt_in: bool, - // pub autocapture_web_vitals_opt_in: bool, - // pub autocapture_opt_out: bool, - // pub autocapture_exceptions_opt_in: bool, -} +use crate::{ + api::errors::FlagError, + client::database::Client as DatabaseClient, + client::redis::Client as RedisClient, + team::team_models::{Team, TEAM_TOKEN_CACHE_PREFIX}, +}; impl Team { /// Validates a token, and returns a team if it exists. - #[instrument(skip_all)] pub async fn from_redis( client: Arc, @@ -94,12 +75,9 @@ mod tests { use redis::AsyncCommands; use super::*; - use crate::{ - team, - test_utils::{ - insert_new_team_in_pg, insert_new_team_in_redis, random_string, setup_pg_reader_client, - setup_redis_client, - }, + use crate::utils::test_utils::{ + insert_new_team_in_pg, insert_new_team_in_redis, random_string, setup_pg_reader_client, + setup_redis_client, }; #[tokio::test] @@ -159,11 +137,7 @@ mod tests { .await .expect("Failed to get redis connection"); conn.set::( - format!( - "{}{}", - team::TEAM_TOKEN_CACHE_PREFIX, - team.api_token.clone() - ), + format!("{}{}", TEAM_TOKEN_CACHE_PREFIX, team.api_token.clone()), serialized_team, ) .await diff --git a/rust/feature-flags/src/utils/mod.rs b/rust/feature-flags/src/utils/mod.rs new file mode 100644 index 00000000000..681d26e346c --- /dev/null +++ b/rust/feature-flags/src/utils/mod.rs @@ -0,0 +1 @@ +pub mod test_utils; diff --git a/rust/feature-flags/src/test_utils.rs b/rust/feature-flags/src/utils/test_utils.rs similarity index 96% rename from rust/feature-flags/src/test_utils.rs rename to rust/feature-flags/src/utils/test_utils.rs index 346ed106ea6..ad108d08023 100644 --- a/rust/feature-flags/src/test_utils.rs +++ b/rust/feature-flags/src/utils/test_utils.rs @@ -6,12 +6,14 @@ use std::sync::Arc; use uuid::Uuid; use crate::{ - cohort_models::Cohort, + client::{ + database::{get_pool, Client, CustomDatabaseError}, + redis::{Client as RedisClientTrait, RedisClient}, + }, + cohort::cohort_models::Cohort, config::{Config, DEFAULT_TEST_CONFIG}, - database::{get_pool, Client, CustomDatabaseError}, - flag_definitions::{self, FeatureFlag, FeatureFlagRow}, - redis::{Client as RedisClientTrait, RedisClient}, - team::{self, Team}, + flags::flag_models::{FeatureFlag, FeatureFlagRow, TEAM_FLAGS_CACHE_PREFIX}, + team::team_models::{Team, TEAM_TOKEN_CACHE_PREFIX}, }; use rand::{distributions::Alphanumeric, Rng}; @@ -38,11 +40,7 @@ pub async fn insert_new_team_in_redis( let serialized_team = serde_json::to_string(&team)?; client .set( - format!( - "{}{}", - team::TEAM_TOKEN_CACHE_PREFIX, - team.api_token.clone() - ), + format!("{}{}", TEAM_TOKEN_CACHE_PREFIX, team.api_token.clone()), serialized_team, ) .await?; @@ -82,10 +80,7 @@ pub async fn insert_flags_for_team_in_redis( }; client - .set( - format!("{}{}", flag_definitions::TEAM_FLAGS_CACHE_PREFIX, team_id), - payload, - ) + .set(format!("{}{}", TEAM_FLAGS_CACHE_PREFIX, team_id), payload) .await?; Ok(()) diff --git a/rust/feature-flags/tests/test_flag_matching_consistency.rs b/rust/feature-flags/tests/test_flag_matching_consistency.rs index c632d28bc15..c31ac2094ad 100644 --- a/rust/feature-flags/tests/test_flag_matching_consistency.rs +++ b/rust/feature-flags/tests/test_flag_matching_consistency.rs @@ -1,13 +1,14 @@ use std::sync::Arc; -use feature_flags::cohort_cache::CohortCacheManager; -use feature_flags::feature_flag_match_reason::FeatureFlagMatchReason; /// These tests are common between all libraries doing local evaluation of feature flags. /// This ensures there are no mismatches between implementations. -use feature_flags::flag_matching::{FeatureFlagMatch, FeatureFlagMatcher}; - -use feature_flags::test_utils::{ - create_flag_from_json, setup_pg_reader_client, setup_pg_writer_client, +use feature_flags::{ + cohort::cohort_cache_manager::CohortCacheManager, + flags::{ + flag_match_reason::FeatureFlagMatchReason, + flag_matching::{FeatureFlagMatch, FeatureFlagMatcher}, + }, + utils::test_utils::{create_flag_from_json, setup_pg_reader_client, setup_pg_writer_client}, }; use serde_json::json; diff --git a/rust/feature-flags/tests/test_flags.rs b/rust/feature-flags/tests/test_flags.rs index 6b6263b4a77..918a73ede6f 100644 --- a/rust/feature-flags/tests/test_flags.rs +++ b/rust/feature-flags/tests/test_flags.rs @@ -7,7 +7,7 @@ use serde_json::{json, Value}; use crate::common::*; use feature_flags::config::DEFAULT_TEST_CONFIG; -use feature_flags::test_utils::{ +use feature_flags::utils::test_utils::{ insert_flags_for_team_in_redis, insert_new_team_in_pg, insert_new_team_in_redis, setup_pg_reader_client, setup_redis_client, };