feat: support rule-based user segments - #2723
Conversation
Extend segments so membership can be defined by attribute-based rules in addition to the included-user list, reusing the existing flag rule/clause model. A user belongs to a segment when it is in the included-user list OR matches any segment rule. Part of #2722 - proto: add rules to CreateSegmentRequest/UpdateSegmentRequest (via a new RuleListValue wrapper so absent = unchanged, present = full replacement), to SegmentUsers for server SDK delivery, and to SegmentUpdatedEvent for audit logs - api: wire rules into CreateSegment/UpdateSegment with server-side uuid generation and validation (no strategy, no SEGMENT/FEATURE_FLAG operators, clause attribute/values required, unique ids, rule/clause limits), and refresh the segment users cache on rule updates - evaluation (go/typescript): evaluate segment rules with the flag-side rule evaluator after the explicit include-list check; change multi-segment SEGMENT clause semantics from AND to OR; SEGMENT/FEATURE_FLAG clauses inside segment rules fail closed - delivery: include rules in the cached SegmentUsers payload (batch cacher and gateway fallback paths) and pass the segments map through the GetEvaluations/EvaluateFeatures paths - tests: shared JSON conformance fixtures consumed by both evaluation engines, plus API validation tests Co-authored-by: Cursor <cursoragent@cursor.com>
There was a problem hiding this comment.
Pull request overview
Adds first-class support for rule-based segment membership (attribute-driven) in addition to the existing explicit included-user list, and wires segment rules through API/storage/caching into both Go and TypeScript evaluators so flags can target segments whose membership is computed locally.
Changes:
- Extend segment APIs/protos/events to accept/store/emit segment
rules, including an update-time wrapper (RuleListValue) to distinguish “absent” vs “replace (possibly empty)”. - Deliver segment rules through the segment-users caching/sync pipeline and thread segment definitions into evaluators (Go + TypeScript) so
SEGMENTclauses can consult segment rules. - Add shared conformance fixtures and tests to keep Go/TS evaluation behavior aligned (including the
SEGMENTmulti-value AND→OR semantic change).
Reviewed changes
Copilot reviewed 50 out of 53 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| proto/proto.lock | Locks updated proto graph to include new segment rules fields and RuleListValue. |
| proto/feature/service.proto | Exposes segment rules on Create/Update APIs; Update uses RuleListValue. |
| proto/feature/segment.proto | Adds rules to SegmentUsers payload for SDK sync/local eval. |
| proto/feature/segment.pb.go | Generated Go changes for SegmentUsers.rules. |
| proto/feature/rule.proto | Adds RuleListValue wrapper message. |
| proto/feature/rule.pb.go | Generated Go changes for RuleListValue. |
| proto/event/domain/event.proto | Adds rules to SegmentUpdatedEvent for audit logging. |
| pkg/feature/storage/v2/segment.go | Extends InUseSegment to include rules for cacher payloads. |
| pkg/feature/storage/v2/postgres/sql/segment/select_all_in_use_segments.sql | Selects seg.rules for in-use segment listing (Postgres). |
| pkg/feature/storage/v2/postgres/segment.go | Scans rules JSON into InUseSegment.Rules (Postgres). |
| pkg/feature/storage/v2/mysql/sql/segment/select_all_in_use_segments.sql | Selects seg.rules for in-use segment listing (MySQL). |
| pkg/feature/storage/v2/mysql/segment.go | Scans rules JSON into InUseSegment.Rules (MySQL). |
| pkg/feature/domain/segment.go | Adds domain helper to replace segment rules and bump UpdatedAt. |
| pkg/feature/cacher/segment_user_cacher.go | Includes rules in cached SegmentUsers entries. |
| pkg/feature/api/validation.go | Adds validateSegmentRules with limits and operator/strategy restrictions. |
| pkg/feature/api/validation_test.go | Unit tests for segment rules validation. |
| pkg/feature/api/segment.go | Handles rule ID generation, update semantics, audit event type change, and cache refresh. |
| pkg/feature/api/segment_test.go | API tests for rule validation, ID generation, and update replacement/clear semantics. |
| pkg/feature/api/feature.go | Fetches/returns SegmentUsers including rules and builds segments map for evaluators. |
| pkg/feature/api/feature_test.go | Updates evaluation tests to include new segment fetch expectations. |
| pkg/feature/api/error.go | Adds gRPC statuses for segment rules validation failures. |
| pkg/api/api/api.go | REST gateway threads segment definitions through evaluation and primes cache with rules. |
| pkg/api/api/api_test.go | Updates gateway tests for new GetSegment fetch on cache-miss paths. |
| pkg/api/api/api_grpc.go | gRPC gateway threads segment definitions through evaluation and primes cache with rules. |
| evaluation/typescript/src/segmentEvaluator.ts | Implements segment membership as include-list OR rule-match; OR across segment IDs. |
| evaluation/typescript/src/ruleEvaluator.ts | Threads user + segments map + nullable flag variations into clause evaluation. |
| evaluation/typescript/src/evaluation.ts | Threads segments map into evaluation/assignment paths. |
| evaluation/typescript/src/clauseEvaluator.ts | Uses SegmentEvaluator with (user, segments); fail-closed for FEATURE_FLAG in segment rules. |
| evaluation/typescript/src/tests/segment_evaluator_test.ts | Updates tests for OR semantics and adds rule-based segment cases. |
| evaluation/typescript/src/tests/segment_conformance_test.ts | New: runs shared segment conformance fixtures in TS. |
| evaluation/typescript/src/tests/rule_evaluator_test.ts | Updates SEGMENT multi-value semantics expectations and new evaluator signature. |
| evaluation/typescript/src/tests/evaluator/yaml_conversion_test.ts | Updates evaluator calls for new signature (mapSegments). |
| evaluation/typescript/src/tests/evaluator/evaluate_feature_test.ts | Updates evaluator calls for new signature (mapSegments). |
| evaluation/typescript/src/tests/evaluator/by_evaluated_at_test.ts | Updates evaluator calls for new signature (mapSegments). |
| evaluation/typescript/src/tests/evaluator/assign_user_target_test.ts | Updates assignUser signature to accept segments map. |
| evaluation/typescript/src/tests/evaluator/assign_user_sampling_seed.ts | Updates assignUser signature to accept segments map. |
| evaluation/typescript/src/tests/evaluator/assign_user_rule_test.ts | Updates assignUser signature to accept segments map. |
| evaluation/typescript/src/tests/evaluator/assign_user_off_variationt_test.ts | Updates assignUser signature to accept segments map. |
| evaluation/typescript/src/tests/evaluator/assign_user_default_strategy_test.ts | Updates assignUser signature to accept segments map. |
| evaluation/typescript/src/tests/clause_evaluator_test.ts | Updates clause evaluator calls to pass User and segments map. |
| evaluation/testdata/segment_rules_conformance.json | New: shared Go/TS conformance fixtures for segment rule evaluation. |
| evaluation/go/segment_evaluator.go | Implements include-list OR rule-match; OR across segment IDs; reuses rule evaluator. |
| evaluation/go/segment_conformance_test.go | New: runs shared segment conformance fixtures in Go. |
| evaluation/go/rule_evaluator.go | Threads segments map into rule/clause evaluation. |
| evaluation/go/rule_evaluator_test.go | Updates SEGMENT multi-value semantics expectations and new evaluator signature. |
| evaluation/go/evaluation.go | Threads segments map into evaluation/assignment paths. |
| evaluation/go/evaluation_test.go | Updates evaluator calls for new signature (mapSegments). |
| evaluation/go/clause_evaluator.go | Uses SegmentEvaluator with (user, segments); fail-closed for FEATURE_FLAG in segment rules. |
| evaluation/go/clause_evaluator_test.go | Updates clause evaluator calls to pass User and segments map. |
| api-description/web-api.swagger.yaml | Documents segment rules fields and RuleListValue wrapper in web API swagger. |
| api-description/apidocs.swagger.yaml | Documents SegmentUsers.rules in API docs swagger. |
Comments suppressed due to low confidence (1)
pkg/feature/api/feature.go:1203
- If
segmentStorage.GetSegmentreturnsv2fs.ErrSegmentNotFound(e.g., segment deleted but still referenced), evaluation will currently fail the whole request. It would be safer to treat a missing segment definition as "no rules" and continue, so a stale reference doesn't become an outage.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- Treat GetSegment NotFound as a stale segment reference in the gateway L3 fallbacks (gRPC and REST) and the feature service getSegmentUsers: evaluate with the user list only instead of failing the whole evaluation request, matching the behavior before rules were delivered. - Return the storage error instead of the earlier cache-miss error when ListSegmentUsers fails in getSegmentUsers. - Make api.NewGRPCStatus pass through errors that already carry a gRPC status instead of double-wrapping them with codes.Unknown. Co-authored-by: Cursor <cursoragent@cursor.com>
- refreshSegmentUsersCache reuses the cached user list (kept warm by the batch cacher) and only falls back to storage on a cache miss, so a rules-only update doesn't load the full user list of large segments from the DB. - Rename the singleflight result in listSegmentUsers so it no longer shadows the FeatureService receiver. Co-authored-by: Cursor <cursoragent@cursor.com>
Reusing the cached user list could write back a stale list with a fresh updated_at when a concurrent user upload or the batch cacher updates the entry. UpdateSegment is a rare admin operation, so keep it simple and read the authoritative list from storage. Co-authored-by: Cursor <cursoragent@cursor.com>
The segment conformance tests cover the segment evaluator directly, but the top-level EvaluateFeatures path with a segments map was untested. Add tests in both Go and TypeScript covering the full chain: flag rule with a SEGMENT clause -> rule-based segment -> variation assignment, including mixed list+rule membership and nil/null segments map backward compatibility. Co-authored-by: Cursor <cursoragent@cursor.com>
Feature API e2e: create a segment with rules (server-side ID generation, persistence), reject invalid rules (SEGMENT/FEATURE_FLAG operators, missing values), and verify update semantics (absent = unchanged, present = full replacement, empty list = clear). Gateway e2e: evaluate a flag with a SEGMENT clause referencing a rule-based segment through GetEvaluations, verifying that a user matching the segment rule by attribute gets the rule variation and a non-matching user falls back to the default strategy. Co-authored-by: Cursor <cursoragent@cursor.com>
Extend the rule-based segment gateway e2e test to upload a user to the segment's include list, verifying all three membership paths through GetEvaluations: matched by rule, matched by list, and neither. Co-authored-by: Cursor <cursoragent@cursor.com>
|
In Should we add that field there too, so it stays consistent with the other cache-write paths ( |
The event-driven cache refresher rebuilds the SegmentUsers cache entry on every segment domain event but omitted the rules, so it would overwrite the correct entry (written by UpdateSegment or the batch cacher) with a rules-less blob — silently disabling rule-based membership until the next batch cacher run. Co-authored-by: Cursor <cursoragent@cursor.com>
@Ubisoft-potato, thanks! |
Signed-off-by: Alessandro Yuichi Okimoto <yuichijpn@gmail.com>
93bca88 to
fc62630
Compare
Signed-off-by: Alessandro Yuichi Okimoto <yuichijpn@gmail.com>
Signed-off-by: Alessandro Yuichi Okimoto <yuichijpn@gmail.com>
Signed-off-by: Alessandro Yuichi Okimoto <yuichijpn@gmail.com>
Ubisoft-potato
left a comment
There was a problem hiding this comment.
Great work! 🚀
Thanks!
Extend segments so membership can be defined by attribute-based rules in addition to the included-user list, reusing the existing flag rule/clause model. A user belongs to a segment when it is in the included-user list OR matches any segment rule.
Part of #2722
Summary
Segments currently support only explicit membership (an uploaded list of user IDs). This PR extends segments so membership can also be defined by attribute-based rules, reusing the existing flag rule/clause model and operators:
The
Segmentproto already had arulesfield, the DB column existed, and the domain layer had rule helpers; none of it was exposed through the API or consulted during evaluation. This PR completes and exposes that model.Changes by layer
Proto (
proto/feature,proto/event/domain)CreateSegmentRequest.rules: rules at creation time.UpdateSegmentRequest.rulesuses a newRuleListValuewrapper so the API can distinguish absent = unchanged from present = full replacement (including replacement with an empty list to clear rules).SegmentUsers.rules: delivers rules to server SDKs through the existing segment-user sync pipeline (optional field; old SDKs ignore it).SegmentUpdatedEvent.rules: rule changes recorded in the audit log along with the existing before/after entity snapshots.SegmentRulemessage — segment rules reusefeature.Rule/feature.Clause.API (
pkg/feature/api)CreateSegment/UpdateSegmentaccept rules. Rule/clause IDs are generated server-side (uuid v4), like flag rules.validateSegmentRulesenforces:strategyon segment rules,SEGMENT/FEATURE_FLAGoperators inside segment rules (no nesting, no flag cycles),UpdateSegmentbumpsupdated_atand refreshes the segment-users cache (best effort) so rule changes propagate immediately instead of waiting for the batch cacher.Evaluation (
evaluation/goandevaluation/typescript, ported identically)segment_evaluator: explicit include-list check first (unchanged behavior), then segment rules evaluated with the existing flag-side rule evaluator (OR across rules, AND across clauses, all operators, attribute resolution).SEGMENTclause with multiple segment IDs now matches when the user is in any of them (OR) instead of all (AND), making it consistent with every other operator. AND across segments remains expressible with multipleSEGMENTclauses in one rule. See the pre-ship check below.userand asegmentsmap are threaded throughclause_evaluator→rule_evaluator→evaluationin both engines.SEGMENT/FEATURE_FLAGclauses inside a segment rule never match (validation rejects them at write time; evaluation guards at read time).Data delivery (
pkg/feature/cacher,pkg/api/api, storage)SegmentUserspayload; the in-use-segments query selectsrules(MySQL + Postgres).GetEvaluations,EvaluateFeatures, and the stream evaluation paths.Backward compatibility
SegmentUsersgains only an optional field; older server SDKs ignore it and keep working for list-only segments. For segments using rules, older server SDKs evaluate only the include list (console warning + min-version docs are part of the follow-up scope in feat: support for rule-based user segments #2722).SEGMENTclause with more than one value (the console is single-select, so only API-created flags could). Scanfeature.rulesfor clauses with operatorSEGMENTand more than one value before release.Testing
evaluation/testdata/segment_rules_conformance.json): 46 JSON cases consumed by both the Go and TypeScript evaluation tests so the two engines stay in lockstep. Covers explicit-only, rule-only, mixed, multi-clause AND, multi-rule OR, multi-segment OR in one clause, missing attributes, every operator, empty rules (backward compat), and fail-closed forSEGMENT/FEATURE_FLAGinside segment rules.GetSegmentmock expectations for the new rules fetch in storage-fallback paths; two rule-evaluator expectations updated to reflect the intended AND→OR semantics change.make lint: 0 issues (Go), 0 errors (eslint).go test ./evaluation/go/... ./pkg/feature/... ./pkg/api/...and the TypeScript suite (181 tests) all pass.Out of scope (per #2722)
Server SDK consumption, console UI, nested segments, exclusion rules, and percentage rollout inside segments.