feat: Add feature flag checking for rust and python#92
feat: Add feature flag checking for rust and python#92markstory wants to merge 17 commits intofeat-add-feature-schemafrom
Conversation
d66eb74 to
10e1183
Compare
| fn test_feature_context_id_value_align_with_python() { | ||
| // Context.id() determines rollout rates with modulo | ||
| // This implementation should generate the same rollout slots | ||
| // as the previous implementation did. | ||
| let ctx = FeatureContext::new(); | ||
| assert_eq!(ctx.id() % 100, 5, "should match with python implementation"); | ||
|
|
||
| let mut ctx = FeatureContext::new(); | ||
| ctx.insert("foo", "bar".into()); | ||
| ctx.insert("baz", "barfoo".into()); | ||
| ctx.identity_fields(vec!["foo"]); | ||
| assert_eq!(ctx.id() % 100, 62); | ||
|
|
||
| // Undefined fields should not contribute to the id. | ||
| let mut ctx = FeatureContext::new(); | ||
| ctx.insert("foo", "bar".into()); | ||
| ctx.insert("baz", "barfoo".into()); | ||
| ctx.identity_fields(vec!["foo", "whoops"]); | ||
| assert_eq!(ctx.id() % 100, 62); | ||
|
|
||
| let mut ctx = FeatureContext::new(); | ||
| ctx.insert("foo", "bar".into()); | ||
| ctx.insert("baz", "barfoo".into()); | ||
| ctx.identity_fields(vec!["foo", "baz"]); | ||
| assert_eq!(ctx.id() % 100, 1); | ||
|
|
||
| let mut ctx = FeatureContext::new(); | ||
| ctx.insert("foo", "bar".into()); | ||
| ctx.insert("baz", "barfoo".into()); | ||
| // When there is no overlap with identity fields and data, | ||
| // all fields should be used | ||
| ctx.identity_fields(vec!["whoops", "nope"]); | ||
| assert_eq!(ctx.id() % 100, 1); | ||
| } |
There was a problem hiding this comment.
This test matches the attributes and rollout bucket assignments in getsentry/sentry#109822
| pub struct FeatureContext { | ||
| data: HashMap<String, ContextValue>, | ||
| identity_fields: Vec<String>, | ||
| cached_id: Cell<Option<u64>>, |
There was a problem hiding this comment.
i assume we use Cell so we don't need to pass around &mut self? (asking for my own learning)
There was a problem hiding this comment.
Yes it lets the outer struct be immutable, but the cached_id can be mutable. This also goes by interior mutability
clients/rust/src/features.rs
Outdated
|
|
||
| /// A typed value that can be stored in a [`FeatureContext`]. | ||
| #[derive(Debug, Clone)] | ||
| pub enum ContextValue { |
There was a problem hiding this comment.
this enum seems like a lot of code to hold a generic list of objects and perform an operation on them (to_id_string()), could we just reuse serde_json::Value?
the one downside is it'll make it a bit less object oriented:
- (
to_id_string()) will live on its own - we reuse
serde_json::Valuewhen it doesn't have anything to do withjson
i got an LLM to preview what that would look like: #94
what do you think?
There was a problem hiding this comment.
... could we just reuse serde_json::Value?
We could. That would be simpler 👍
| if self.rollout >= 100 { | ||
| return true; | ||
| } | ||
| context.id() % 100 < self.rollout |
There was a problem hiding this comment.
this seems to be <= in sentry src/flagpole/conditions.py https://github.com/getsentry/sentry/blob/master/src/flagpole/conditions.py#L251
There was a problem hiding this comment.
| context.id() % 100 < self.rollout | |
| context.id() % 100 <= self.rollout |
There was a problem hiding this comment.
ah wait, i see you made this change in e5b80b3. I guess flagpole has always been wrong then? < would be mathematically correct as % 100 returns [0, 99] and 0 <= 0 would match the flag..
i guess this is an improvement then!
There was a problem hiding this comment.
Yes, the python implementation has an off-by one error. I thought this was a good time to fix that mistake. It will change rollout groups though which could pose a problem during the transition from the current implementation to this one.
|
linking this PR to https://linear.app/getsentry/issue/DI-1686/flagpole-support |
9f7ae45 to
69c8bbc
Compare
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Add a first pass of features() and FeatureChecker.
1% should only include the 0th bucket, not 0 & 1.
Need this for bigint support
a0f1a30 to
90c76dc
Compare
|
I was curious about the memory impacts and runtime behavior of this implementation. I had claude convert all of our existing feature flags (290 of them) to sentry-options values format, and generate a schema namespace schema for them. I wrote up a small jank script that looks like Detailsimport contextlib
import resource
import sentry_options
import time
@contextlib.contextmanager
def timer(name: str):
start_mem = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss
start = time.time()
yield
end = time.time()
end_mem = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss
duration = end - start
print(f"> {name} : {duration:.8f}")
mem_diff = end_mem - start_mem
if mem_diff > 1_000_000:
print(f"! memory increase: {name} : {mem_diff:.2f}")
def main():
print("with sentry-options")
with timer("init()"):
sentry_options.init()
ctx = sentry_options.FeatureContext(
{
"organization_id": 1,
"organization_slug": "sentry",
"user_email": "mark@sentry.io",
},
identity_fields=["organization_id"]
)
with timer("check single feature once"):
assert sentry_options.features("flagpole").has("organizations:anomaly-detection-eap", ctx)
with timer("check single feature 10000 times"):
for i in range(10000):
sentry_options.features("flagpole").has("organizations:anomaly-detection-eap", ctx)
if __name__ == "__main__":
main()Running this locally yielded: The init() function will add ~2s to application startup time, but after that feature flag checks are quite efficient compared to the overhead of checking a feature today. I built a similar test for the existing flagpole implementation: Detailsimport contextlib
import resource
import time
from sentry.runner import configure
configure()
from sentry import options
from sentry import features
from flagpole import Feature, EvaluationContext
@contextlib.contextmanager
def timer(name: str) -> None:
start_mem = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss
start = time.time()
yield
end = time.time()
end_mem = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss
duration = end - start
print(f"> {name} : {duration:.8f}")
mem_diff = end_mem - start_mem
if mem_diff > 100_000:
print(f"! memory increase: {name} : {mem_diff:.2f}")
def main() -> None:
print("sentry flagpole timing")
ctx = EvaluationContext(
{
"organization_id": 1,
"organization_slug": "sentry",
"user_email": "mark@sentry.io",
},
identity_fields=set(["organization_id"])
)
with timer("check single feature once"):
name = "organizations:anomaly-detection-eap"
config = options.get(f"feature.{name}")
feature = Feature.from_feature_dictionary(name, config)
assert feature.match(ctx)
with timer("check single feature 10000 times"):
name = "organizations:anomaly-detection-eap"
for i in range(10000):
config = options.get(f"feature.{name}")
feature = Feature.from_feature_dictionary(name, config)
feature.match(ctx)
with timer("check every feature 100 times"):
for key in features.all().keys():
if key.startswith("feature"):
name = key.split(".")[1]
for i in range(100):
config = options.get(f"feature.{name}")
feature = Feature.from_feature_config_json(name, config)
feature.match(ctx)
if __name__ == "__main__":
main()and got higher durations for both the single and many checks. |
* Add sha1 package * Implement features() and FeatureChecker Add a first pass of features() and FeatureChecker. * Implement features() and FeatureChecker in python * Fix clippy warnings * Fix tests * Fix tests * Tweak rollout rate comparison 1% should only include the 0th bucket, not 0 & 1. * Features should default to enabled=false * Add num dep Need this for bigint support * cargo fmt * Fix key names in conftest. * Remove dbg! and fix potential panic * as serde value * bump this version as well --------- Co-authored-by: Mark Story <mark@mark-story.com>
Instead of monkey patching the test suite, use the existing fixture files.
Continue work started in #91 to add methods for checking feature flags from both rust + python. I'd like to spend some more time expanding python tests to include more of the existing flagpole tests to ensure that flag checking behavior is consistent. I can do that separately as these changes are large already.