Skip to content

Wip/precomputed segments for flags#3198

Open
danoswaltCL wants to merge 11 commits into
release/6.5from
wip/precomputed-segments-for-flags
Open

Wip/precomputed segments for flags#3198
danoswaltCL wants to merge 11 commits into
release/6.5from
wip/precomputed-segments-for-flags

Conversation

@danoswaltCL

@danoswaltCL danoswaltCL commented Jun 26, 2026

Copy link
Copy Markdown
Collaborator

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.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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_segment storage (migration + entity + repository) and a PrecomputedSegmentService to compute/cache flattened segment member IDs.
  • Wired recompute/backfill triggers into segment + feature flag write paths, and added startup backfill.
  • Updated FeatureFlagService.getKeys to use a fast-path that pulls precomputed rows and filters via Set.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.

Comment thread packages/backend/src/api/services/FeatureFlagService.ts
Comment thread packages/backend/src/api/services/FeatureFlagService.ts
Comment thread packages/backend/src/api/services/FeatureFlagService.ts
Comment thread packages/backend/src/api/services/FeatureFlagService.ts Outdated
Comment thread packages/backend/src/api/services/FeatureFlagService.ts Outdated
Comment thread packages/backend/src/api/services/FeatureFlagService.ts Outdated
Comment thread packages/backend/src/api/services/SegmentService.ts
Comment thread packages/backend/src/api/services/SegmentService.ts
Comment thread packages/backend/src/api/repositories/PrecomputedSegmentRepository.ts Outdated
Comment thread packages/types/src/Experiment/enums.ts Outdated
Comment thread packages/backend/src/api/models/FeatureFlagPrecomputedSegment.ts
Comment thread packages/backend/src/api/models/FeatureFlagPrecomputedSegment.ts

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

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 21 out of 22 changed files in this pull request and generated 7 comments.

Comment on lines +150 to +153
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) => {
Comment on lines +923 to +925
// 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() : [];

Comment on lines +698 to +702
// 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))
);
Comment on lines +856 to +857
await this.featureFlagPrecomputedSegmentService.recomputeForFlag(listInput.id, logger);

Comment on lines +536 to +538
if (flagId) {
await this.featureFlagPrecomputedSegmentService.recomputeForFlag(flagId, logger);
}
Comment on lines +78 to +93
## 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`.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Fixed in the latest commit — updated all references in CLAUDE.md from precomputed_segment/PrecomputedSegmentService/PrecomputedSegment to feature_flag_precomputed_segment/FeatureFlagPrecomputedSegmentService/FeatureFlagPrecomputedSegment.

Comment thread packages/backend/src/api/services/SegmentService.ts
danoswaltCL and others added 2 commits July 2, 2026 15:25
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>
@danoswaltCL

Copy link
Copy Markdown
Collaborator Author

@copilot please review 8193311, which addresses a couple comments about grouptype information

Copilot AI commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

@copilot please review 8193311, which addresses a couple comments about grouptype informati...

Reviewed 8193311. The implementation correctly addresses both issues:

  • Type-aware matching: precomputedGroupKey(type, groupId) namespaces group IDs as type:groupId on both the write path (FeatureFlagPrecomputedSegmentService) and read path (FeatureFlagService), matching the GroupForSegment composite key (groupId, type) semantics.
  • Group/user ID collision prevention: Groups stored as schoolId:user123 vs individuals stored bare as user123 — the individual Set.has(experimentUser.id) check can never accidentally match a group entry.
  • Migration: Clears stale bare-format rows so the startup backfill rebuilds them correctly; missing rows safely fall back to on-the-fly resolution.

All 49 unit tests pass, including the two new type-aware and collision tests. No issues found.

@danoswaltCL danoswaltCL force-pushed the wip/precomputed-segments-for-flags branch from fcfe52f to aded2f0 Compare July 2, 2026 22:15
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.

4 participants