Skip to content

feat: support rule-based user segments - #2723

Merged
cre8ivejp merged 12 commits into
mainfrom
feat-rule-based-segments
Jul 27, 2026
Merged

feat: support rule-based user segments#2723
cre8ivejp merged 12 commits into
mainfrom
feat-rule-based-segments

Conversation

@cre8ivejp

Copy link
Copy Markdown
Member

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:

  • A segment can have an included-user list, rules, or both.
  • A user belongs to a segment if they are in the included-user list OR match any segment rule.
  • Clauses within one rule are AND; multiple rules are OR (same as flag targeting rules).
  • Segment rules determine membership only — no strategy/rollout/variation assignment.

The Segment proto already had a rules field, 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.rules uses a new RuleListValue wrapper 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.
  • No new SegmentRule message — segment rules reuse feature.Rule/feature.Clause.

API (pkg/feature/api)

  • CreateSegment/UpdateSegment accept rules. Rule/clause IDs are generated server-side (uuid v4), like flag rules.
  • validateSegmentRules enforces:
    • no strategy on segment rules,
    • no SEGMENT / FEATURE_FLAG operators inside segment rules (no nesting, no flag cycles),
    • at least one clause per rule, attribute and values required,
    • unique uuid rule/clause IDs,
    • limits: max 20 rules per segment, max 10 clauses per rule.
  • UpdateSegment bumps updated_at and refreshes the segment-users cache (best effort) so rule changes propagate immediately instead of waiting for the batch cacher.

Evaluation (evaluation/go and evaluation/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).
  • Semantics change: a SEGMENT clause 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 multiple SEGMENT clauses in one rule. See the pre-ship check below.
  • user and a segments map are threaded through clause_evaluatorrule_evaluatorevaluation in both engines.
  • Fail-closed: SEGMENT/FEATURE_FLAG clauses 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)

  • Batch cacher includes rules in the cached SegmentUsers payload; the in-use-segments query selects rules (MySQL + Postgres).
  • Gateway (gRPC + REST) fallback paths fetch rules on cache miss and include them when priming the cache.
  • The segments map is built and passed through GetEvaluations, EvaluateFeatures, and the stream evaluation paths.

Backward compatibility

  • Existing segments have empty rules → the rules loop is a no-op, and the include-list check runs first, unchanged. Verified by conformance tests (explicit-only / empty-rules cases).
  • SegmentUsers gains 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).
  • Client SDKs are server-evaluated and require no changes.
  • Pre-ship check for the AND→OR change: verify no stored flag has a SEGMENT clause with more than one value (the console is single-select, so only API-created flags could). Scan feature.rules for clauses with operator SEGMENT and more than one value before release.

Testing

  • Shared conformance fixtures (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 for SEGMENT/FEATURE_FLAG inside segment rules.
  • API tests: every validation rejection (strategy, operators, limits, duplicate/invalid IDs, missing attribute/values), server-side ID generation, and update semantics (absent = unchanged, present = replace, empty list = clear + cache refresh).
  • Existing test updates: gateway/feature tests gained GetSegment mock 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.

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>
Copilot AI review requested due to automatic review settings July 27, 2026 05:55

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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 SEGMENT clauses can consult segment rules.
  • Add shared conformance fixtures and tests to keep Go/TS evaluation behavior aligned (including the SEGMENT multi-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.GetSegment returns v2fs.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.

Comment thread pkg/feature/api/feature.go Outdated
Comment thread pkg/api/api/api.go
Comment thread pkg/api/api/api_grpc.go
- 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>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 52 out of 55 changed files in this pull request and generated 2 comments.

Comment thread pkg/feature/api/segment.go
Comment thread pkg/feature/api/feature.go Outdated
cre8ivejp and others added 2 commits July 27, 2026 15:29
- 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>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 52 out of 55 changed files in this pull request and generated no new comments.

cre8ivejp and others added 3 commits July 27, 2026 15:47
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>
@Ubisoft-potato

Copy link
Copy Markdown
Collaborator

In pkg/subscriber/processor/cache_refresher.go, refreshSegmentUsers (line ~405) builds SegmentUsers from listResp/getResp but doesn't copy getResp.Segment.Rules.

Should we add that field there too, so it stays consistent with the other cache-write paths (segment.go, segment_user_cacher.go)?

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>
@cre8ivejp

Copy link
Copy Markdown
Member Author

In pkg/subscriber/processor/cache_refresher.go, refreshSegmentUsers (line ~405) builds SegmentUsers from listResp/getResp but doesn't copy getResp.Segment.Rules.

Should we add that field there too, so it stays consistent with the other cache-write paths (segment.go, segment_user_cacher.go)?

@Ubisoft-potato, thanks!
I have fixed it 7891474.

@cre8ivejp
cre8ivejp marked this pull request as ready for review July 27, 2026 10:31
@cre8ivejp
cre8ivejp requested review from hvn2k1 and t-kikuc as code owners July 27, 2026 10:31
Signed-off-by: Alessandro Yuichi Okimoto <yuichijpn@gmail.com>
@cre8ivejp
cre8ivejp force-pushed the feat-rule-based-segments branch from 93bca88 to fc62630 Compare July 27, 2026 11:22
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 Ubisoft-potato left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Great work! 🚀
Thanks!

@cre8ivejp
cre8ivejp merged commit 28f1eb0 into main Jul 27, 2026
21 checks passed
@cre8ivejp
cre8ivejp deleted the feat-rule-based-segments branch July 27, 2026 14:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants