Wip/precomputed segments for flags#3198
Conversation
…flag include lists
…d-segments-for-flags
There was a problem hiding this comment.
Pull request overview
Implements “precomputed segment lists” for feature flags to avoid recursive segment resolution on the /featureflag read path by persisting flattened inclusion/exclusion member IDs and doing in-memory lookups.
Changes:
- Added
precomputed_segmentstorage (migration + entity + repository) and aPrecomputedSegmentServiceto compute/cache flattened segment member IDs. - Wired recompute/backfill triggers into segment + feature flag write paths, and added startup backfill.
- Updated
FeatureFlagService.getKeysto use a fast-path that pulls precomputed rows and filters viaSet.has()checks (with fallback to legacy on-the-fly resolution when a row is missing).
Reviewed changes
Copilot reviewed 21 out of 22 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/types/src/Experiment/enums.ts | Adds cache prefix for precomputed segment caching. |
| packages/backend/src/database/migrations/1779500000000-precomputedSegment.ts | Creates precomputed_segment table keyed by feature flag id. |
| packages/backend/src/api/models/PrecomputedSegment.ts | New TypeORM entity for precomputed flag segment members. |
| packages/backend/src/api/repositories/PrecomputedSegmentRepository.ts | Upsert + batch fetch helpers for precomputed rows. |
| packages/backend/src/api/services/PrecomputedSegmentService.ts | Flattens segment graphs into ID arrays; caching; backfill; recompute scheduling. |
| packages/backend/src/api/services/FeatureFlagService.ts | Fast-path filtering using precomputed rows; recompute/seed triggers. |
| packages/backend/src/api/services/SegmentService.ts | Schedules recomputes on segment mutations; recompute-after-delete logic. |
| packages/backend/src/api/repositories/SegmentRepository.ts | Adds parent-segment lookup for ancestor recompute walks. |
| packages/backend/src/api/repositories/FeatureFlagRepository.ts | Adds minimal projection query for /featureflag keys path. |
| packages/backend/src/init/seed/backfillPrecomputedSegments.ts | Startup backfill for missing precomputed flag rows. |
| packages/backend/src/app.ts | Wires startup backfill into boot sequence. |
| packages/backend/test/unit/services/*.test.ts | Adds/updates unit tests for precomputed behavior + triggers. |
| packages/backend/CLAUDE.md | Documents the precomputed flag segment design + triggers. |
| clientlibs/js/src/**/*.spec.ts | Formatting-only test changes. |
| .gitignore | Ignores .claude*. |
| .claude/plans/precomputed-segments-experiments.md | Planning doc for future experiment parity. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| for (const segment of segments) { | ||
| segment.individualForSegment.forEach((ind) => ids.push(ind.userId)); | ||
| segment.groupForSegment.forEach((grp) => ids.push(grp.groupId)); | ||
| segment.subSegments.forEach((sub) => { |
| // Flatten all group IDs from the user's group map (type is ignored per design decision) | ||
| const userGroupIds: string[] = experimentUser.group ? Object.values(experimentUser.group).flat() : []; | ||
|
|
| // Recompute precomputed sets for each affected flag after the transaction commits | ||
| const affectedFlagIds = [...new Set(listsInput.map((l) => l.id))]; | ||
| await Promise.all( | ||
| affectedFlagIds.map((flagId) => this.featureFlagPrecomputedSegmentService.recomputeForFlag(flagId, logger)) | ||
| ); |
| await this.featureFlagPrecomputedSegmentService.recomputeForFlag(listInput.id, logger); | ||
|
|
| if (flagId) { | ||
| await this.featureFlagPrecomputedSegmentService.recomputeForFlag(flagId, logger); | ||
| } |
| ## Precomputed Segment Lists (Feature Flags) | ||
|
|
||
| Segment inclusion/exclusion for **feature flags** is precomputed and stored flat in the `precomputed_segment` table rather than resolved on-the-fly at assignment time. | ||
|
|
||
| ### How it works | ||
|
|
||
| - **`PrecomputedSegment` entity** (`src/api/models/PrecomputedSegment.ts`) — one row per feature flag, columns: `featureFlagId` (PK), `inclusionIds: text[]`, `exclusionIds: text[]`. FK to `feature_flag` with `onDelete: CASCADE`. | ||
| - **`PrecomputedSegmentService`** (`src/api/services/PrecomputedSegmentService.ts`) — owns all computation and cache logic: | ||
| - `recomputeForFlag(flagId)` — flattens all enabled inclusion/exclusion segments (recursive sub-segments) into flat ID arrays and upserts the row. | ||
| - `scheduleRecomputeForSegment(segmentId)` — fire-and-forget; finds all flags referencing a segment (and its parents) and calls `recomputeForFlag` for each. | ||
| - `getAffectedFlagIds(segmentId)` — public helper that returns flag IDs affected by a given segment (used before deletion). | ||
| - `getPrecomputedSets(flagIds[])` — cache-wrapped batch fetch, returns a `Map<flagId, PrecomputedSegment>`. | ||
| - `backfillMissingFlags(logger)` — called at startup; computes rows only for flags that have none yet (no-op once all flags are populated). | ||
| - `recomputeAllFlags(logger)` — full refresh of every flag; not called automatically, available for manual recovery. | ||
| - **Assignment read path** — `FeatureFlagService.featureFlagLevelInclusionExclusion()` calls `getPrecomputedSets()` and does in-memory `Set.has()` checks. No recursive segment queries at assignment time. | ||
| - **Cache** — keyed by `CACHE_PREFIX.PRECOMPUTED_SEGMENT_KEY_PREFIX + flagId`. Invalidated by `recomputeForFlag`. |
There was a problem hiding this comment.
Fixed in the latest commit — updated all references in CLAUDE.md from precomputed_segment/PrecomputedSegmentService/PrecomputedSegment to feature_flag_precomputed_segment/FeatureFlagPrecomputedSegmentService/FeatureFlagPrecomputedSegment.
Group IDs in feature_flag_precomputed_segment were stored bare in the same flat arrays as individual user IDs, so a group ID could collide with a user ID and flip an include/exclude decision. Matching also ignored group type, diverging from the type-aware experiment / on-the-fly resolution path. Namespace group IDs as `type:groupId` (individuals stay bare) via a shared precomputedGroupKey helper used by both the write and read paths. Add a migration that clears the table so the startup backfill rebuilds every row in the new format; missing rows fall back to on-the-fly resolution in the interim. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Reviewed
All 49 unit tests pass, including the two new type-aware and collision tests. No issues found. |
fcfe52f to
aded2f0
Compare
requirements largely found here: #3191
note that this is just for the flags at this moment, as its somewhat more straightforward, the main mechanics will be the same for both.
This creates a new table for precomputed segments, which will get updated on segment list changes.
getKeys()will now do very little to snag flags.I have this going to the old flow still in the event of some error finding the precomputed segment, which hopefully would be really hard to do.