Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add version_of and matches to rules and conditions #4988

Open
wants to merge 18 commits into
base: main
Choose a base branch
from

Conversation

zachaysan
Copy link
Contributor

Changes

When creating a new change request for segments we need a way of tracing which provided rules and conditions match the candidate change request so that the frontend can more visually associate which rules and conditions match between the change request and the targeted segment (the one that will be updated).

To do so I've reused the concept of version_of which was originally used for segments and I associate a rule to another rule (or sub-rule to sub-rule) and in that process also match up conditions at the same layer. If at least one condition matches, then the sub-rule and rule match and future matching for the subsequent sub-rule is guided by holding onto the match between the parent rule and its sister rule. This way matches are only ever tree like in nature.

Remaining work

Since the matching code is ready in this PR another PR will need to be opened to tie in these changes into the relevant code. Probably both the Workflows and Commons will need to be updated.

How did you test this code?

Lots of new tests. Some are simple, like checking that version_of gets correctly set, which is necessary due to an audit log bug we had when version_of was setup for the segments work.

@zachaysan zachaysan requested a review from a team as a code owner January 10, 2025 14:57
@zachaysan zachaysan requested review from matthewelwell and removed request for a team January 10, 2025 14:57
Copy link

vercel bot commented Jan 10, 2025

The latest updates on your projects. Learn more about Vercel for Git ↗︎

3 Skipped Deployments
Name Status Preview Comments Updated (UTC)
docs ⬜️ Ignored (Inspect) Visit Preview Jan 24, 2025 4:49pm
flagsmith-frontend-preview ⬜️ Ignored (Inspect) Visit Preview Jan 24, 2025 4:49pm
flagsmith-frontend-staging ⬜️ Ignored (Inspect) Visit Preview Jan 24, 2025 4:49pm

@github-actions github-actions bot added api Issue related to the REST API feature New feature or request labels Jan 10, 2025
Copy link
Contributor

github-actions bot commented Jan 10, 2025

Docker builds report

Image Build Status Security report
ghcr.io/flagsmith/flagsmith-e2e:pr-4988 Finished ✅ Skipped
ghcr.io/flagsmith/flagsmith-frontend:pr-4988 Finished ✅ Results
ghcr.io/flagsmith/flagsmith-api:pr-4988 Finished ✅ Results
ghcr.io/flagsmith/flagsmith:pr-4988 Finished ✅ Results
ghcr.io/flagsmith/flagsmith-private-cloud:pr-4988 Finished ✅ Results

Copy link
Contributor

github-actions bot commented Jan 10, 2025

Uffizzi Ephemeral Environment deployment-59778

☁️ https://app.uffizzi.com/github.com/Flagsmith/flagsmith/pull/4988

📄 View Application Logs etc.

What is Uffizzi? Learn more!

Copy link

codecov bot commented Jan 10, 2025

Codecov Report

All modified and coverable lines are covered by tests ✅

Project coverage is 97.42%. Comparing base (2faca89) to head (17defee).
Report is 1 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #4988      +/-   ##
==========================================
+ Coverage   97.40%   97.42%   +0.01%     
==========================================
  Files        1193     1194       +1     
  Lines       41675    41958     +283     
==========================================
+ Hits        40593    40876     +283     
  Misses       1082     1082              

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@github-actions github-actions bot added feature New feature or request and removed feature New feature or request labels Jan 10, 2025
@@ -170,7 +170,7 @@ hubspot-api-client = "^8.2.1"
djangorestframework-dataclasses = "^1.3.1"
pyotp = "^2.9.0"
flagsmith-task-processor = { git = "https://github.com/Flagsmith/flagsmith-task-processor", tag = "v1.1.1" }
flagsmith-common = { git = "https://github.com/Flagsmith/flagsmith-common", tag = "v1.4.2" }
flagsmith-common = { git = "https://github.com/Flagsmith/flagsmith-common", branch = "feat/add_version_of_to_rules_and_conditions" }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's remember to update these once we've got builds in those packages.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea.

api/segments/models.py Show resolved Hide resolved
api/segments/models.py Outdated Show resolved Hide resolved
api/segments/models.py Outdated Show resolved Hide resolved
api/segments/models.py Outdated Show resolved Hide resolved
api/segments/models.py Outdated Show resolved Hide resolved
api/segments/models.py Outdated Show resolved Hide resolved
api/segments/models.py Outdated Show resolved Hide resolved
api/segments/models.py Outdated Show resolved Hide resolved
api/segments/models.py Outdated Show resolved Hide resolved
Comment on lines 247 to 265
for self_rule in self_rules:
if sub_rule_matched:
break

if self_rule in matched_rules and self_rule.version_of != rule:
continue

if rule.type != self_rule.type:
continue
for self_sub_rule in self_rule.rules.all():
if self_sub_rule in matched_sub_rules:
continue
if self_sub_rule.assign_conditions_if_matching_rule(sub_rule):
self_sub_rule.version_of = sub_rule
sub_rule_matched = True
matched_sub_rules.add(self_sub_rule)
self_rule.version_of = rule
matched_rules.add(self_rule)
break
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry @zachaysan , this code is still unreadable to me. I was hoping that the docstring and renaming of the variables would help, but it hasn't really. Can we add some inline comments on each of the conditional statements perhaps?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It feels to me like this is perhaps evidence that we're thinking about this wrong? Could we instead just store a 'diff' object (probably in JSON), or set of objects, which look(s) something like:

class SegmentConditionChanges:
    condition: Condition

    new_operator: str
    new_property: str
    new_value: str

class SegmentRuleChanges:
    rule: SegmentRule

    new_type: str

    new_rules: list[SegmentRule]
    deleted_rule_ids: list[int]
    updated_rules: list[SegmentRuleChanges]

    new_conditions: list[Condition]
    delete_condition_ids: list[int]
    updated_conditions: list[SegmentConditionChanges]

class SegmentChangeSet:
    segment: Segment

    new_name: str
    new_description: str

    new_rules: list[SegmentRule]
    deleted_rule_ids: list[int]
    updated_rules: list[SegmentRuleChanges]

... maybe it's a bit late in the day for this, but I feel like this might be more flexible?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok I've added some inline comments as we discussed.

assert condition2.version_of == condition1


def test_assign_matching_rules_to_segment_simple(project: Project) -> None:
Copy link
Contributor

@matthewelwell matthewelwell Jan 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we be more explicit with the test names? What does _simple mean? Alternatively (or maybe as well as), let's add more comments / docstrings to these tests.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok I've updated the test name and have added comments locally.

Comment on lines 388 to 391
if (
condition.operator == self_condition.operator
and condition.property == self_condition.property
):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't we only care about the property here, rather than the operator as well? What if they want to change it from e.g. organisation_id EQUALS 123 to organisation_id IN 123,456 - that seems like a legitimate use case that we want to cater for, right?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, but there's perhaps an issue with operators like percentage split in that case? But I think the logic should be something like:

if not condition.property: 
    if condition.operator == self_condition.operator:
        # do valid match logic
elif condition.property == self_condition.property:
    # do valid match logic

My concern here as well is that I don't think there is anyone stopping someone from adding e.g. 2 percentage split conditions to a segment. It would be a stupid idea, but there's nothing stopping it from happening as far as I know, and there are likely other scenarios where this is the case.

For example, there are a lot of segments that look something like:

organisation_id IN 123,456,...
OR 
organisation_id IN abc,def,...

This is to get around our limitation on the size of the value in a segment condition. I don't think your logic here (or mine suggested above for that matter) will handle this scenario very well, unless I'm missing something?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've updated the condition matching logic to now include operator differences as well as adding a section to the code to look for exact condition matches first.

@github-actions github-actions bot added feature New feature or request and removed feature New feature or request labels Jan 24, 2025
api/segments/models.py Outdated Show resolved Hide resolved
Comment on lines 410 to 411
if self_condition in matched_conditions:
continue
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I really can't see a scenario where this could be the case?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the condition had previously matched another condition, then when the outer loop starts again we don't want to match it again. This is useful for conditions that have the same property and could match different conditions.

Comment on lines 247 to 248
# Set a bool flag to be used to break out of the
# below self_rules iteration if a sub_rule matches.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why? Please always try to phrase comments about the why, not the what. I can infer exactly what this comment says from a quick glance at the variable name, and its use below, but I'm still none the wiser really as to why.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok I've tried to lean in with a Because to guide my writing, but it's hard not to explain code when writing about why a variable exists.

Comment on lines 254 to 257
# If a self_rule has already been matched then
# the only time we care about it is when we
# further iterate on the the same sub_rules
# that are present in the matching parent rule.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again, why? This logic is super hard to follow and all these comments are doing is just rephrasing the code that I can read already. It's super hard to follow what self_rule, sub_rule and 'parent rule' here are. Can we reword this to talk about the business logic rather than the code?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok I've tried to do this one as well.

Comment on lines +261 to +262
# To eliminate false matches we force the types
# to be the same for the rules.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is a 'false match' ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When the match shouldn't go through.

versioning changes.

Side Effects:
- Updates the `version_of` field for matched rules and sub-rules.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Which segment does it modify though?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok I've updated this to explicitly say that it calls the segment self.

Comment on lines 201 to 212
def assign_matching_rules_to_segment(self, segment: "Segment") -> bool:
"""
Assign matching rules of the current object to the rules of the given segment.

This method iterates through the rules of the provided `Segment` and
attempts to match them to the current object's rules and sub-rules,
updating versioning where matches are found.

This is done in order to set the `version_of` field for matched rules
and conditions so that the frontend can more reliably diff rules and
conditions between a change request for a segment and the target segment
itself.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This still doesn't really talk about when it should be used? One of the biggest things I'm finding is that I'm consistently struggling to follow which segment is which. Like should we be more explicit here (and in all of the rule / condition variables) to say something like current_segment, modified_segment or something?

Maybe the method could then be more explicit like: update_segment_with_matches_from_current_segment(self, current_segment: "Segment")?

This feels like it's pigeon-hoing us a little bit into only using this functionality for change requests, but since we're modifying the segments, it feels like this functionality can only ever be used for that purpose anyway?

Copy link
Contributor Author

@zachaysan zachaysan Jan 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This still doesn't really talk about when it should be used?

Ok I've updated the introductory paragraph to include that self is the target of the changes and that this is called in the context of a change request.

One of the biggest things I'm finding is that I'm consistently struggling to follow which segment is which. Like should we be more explicit here (and in all of the rule / condition variables) to say something like current_segment, modified_segment or something?

I've updated the code to reference current_[segment,rule,sub_rule,condition] and I've added references to which object is being updated by referencing them as modified_[rule,sub_rule,condition] to make it clear which objects are optionally being modified.

Maybe the method could then be more explicit like: update_segment_with_matches_from_current_segment(self, current_segment: "Segment")?

Ok I've updated the method name and args.

This feels like it's pigeon-hoing us a little bit into only using this functionality for change requests, but since we're modifying the segments, it feels like this functionality can only ever be used for that purpose anyway?

Yes this functionality is only going to be used for change requests.

@github-actions github-actions bot added feature New feature or request and removed feature New feature or request labels Jan 24, 2025
@github-actions github-actions bot added feature New feature or request and removed feature New feature or request labels Jan 24, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
api Issue related to the REST API feature New feature or request
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants