Repository navigation
Conversation
…APIs (#3326) Single and batch deletion of experiments, feature flags and segments now share one backend policy instead of relying on the UI: - Role: PermissionService checks the shared ROLE_PERMISSIONS table (moved from the frontend into upgrade_types; the frontend now reads it too). Disallowed roles get 403 before any target is locked. - State/usage: DeletionPolicyService evaluates fields read under the target's deletion lock, in the deletion transaction. Experiments must be Draft, Inactive, Completed/Cancelled or Archived; Enabled flags, Used segments and global-exclude segments are refused. - Single routes return 409 (DELETION_NOT_ALLOWED) on refusal. Batch items are reported with a new 'refused' outcome and policy reason codes (state_not_deletable, segment_in_use, global_exclude_protected), and the batch continues. Refused items write no audit log and trigger no cache invalidation or recompute. - DELETE /segments/:segmentId now receives @currentuser(). - DeletionRepository.isSegmentInUse() evaluates the Used status for one segment; an integration test pins it to getSegmentStatus(). - Frontend accepts 'refused' and reports skipped items. - Batch deletion integration tests inverted to assert role and state refusals, including targets that become ineligible before the lock. Load tests (upgrade-automated-tests) still need an authorized account and deletable test entities before cleanup. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
🟡 Changes recommended
Concurrency gaps can allow deletion of a segment that becomes used before commit, while owner discovery outside the transaction can leave archived experiment membership stale.
2 open issues
What changed in this PR
Moves deletion permissions and eligibility checks from UI-only restrictions into shared backend enforcement for experiments, feature flags, and segments, addressing #3326.
Changes:
- Shares role permissions between frontend and backend, returning 403 for unauthorized deletion requests.
- Adds transactional state and usage checks, with 409 responses for single refusals and per-item
refusedresults for batches. - Updates frontend refusal handling, API documentation, and regression coverage.
| File | Description |
|---|---|
packages/types/src/Permissions/index.ts |
Defines shared role permissions and permission checks. |
packages/types/src/index.ts |
Exports permission and deletion-policy definitions. |
packages/types/src/Experiment/enums.ts |
Adds the deletion-refusal error type. |
packages/types/src/BatchActions/index.ts |
Adds refusal outcomes, reasons, and deletable states. |
packages/frontend/projects/upgrade/src/assets/i18n/en.json |
Adds skipped-item notification messages. |
packages/frontend/projects/upgrade/src/app/core/batch-actions/batch-actions.integration.spec.ts |
Tests refused selection retention and warnings. |
packages/frontend/projects/upgrade/src/app/core/batch-actions/batch-actions.helpers.ts |
Handles refused results and shared state eligibility. |
packages/frontend/projects/upgrade/src/app/core/auth/store/auth.models.ts |
Reuses the shared permission type. |
packages/frontend/projects/upgrade/src/app/core/auth/auth.service.ts |
Reads permissions from the shared table. |
packages/backend/test/unit/services/DeletionPolicyService.test.ts |
Tests role, state, usage, and transaction rules. |
packages/backend/test/unit/services/BatchDeleteService.test.ts |
Tests batch refusals and request-level authorization. |
packages/backend/test/unit/controllers/SegmentController.test.ts |
Adds the deletion-policy mock. |
packages/backend/test/unit/controllers/FeatureFlagController.test.ts |
Adds the deletion-policy mock. |
packages/backend/test/unit/controllers/ExperimentControllerAdaptiveConfig.test.ts |
Updates controller construction for the new dependency. |
packages/backend/test/unit/controllers/ExperimentController.test.ts |
Adds the deletion-policy mock. |
packages/backend/test/integration/BatchDelete/index.ts |
Covers enforcement, mixed batches, usage, and cleanup. |
packages/backend/src/api/services/SegmentService.ts |
Makes post-commit waiting independently configurable. |
packages/backend/src/api/services/PermissionService.ts |
Enforces shared permissions with 403 errors. |
packages/backend/src/api/services/DeletionPolicyService.ts |
Centralizes deletion eligibility and single-route transactions. |
packages/backend/src/api/services/BatchDeleteService.ts |
Checks permissions and continues past refused items. |
packages/backend/src/api/repositories/DeletionRepository.ts |
Reads locked policy fields and queries segment usage. |
packages/backend/src/api/middlewares/ErrorHandlerMiddleware.ts |
Preserves deletion-refusal error responses. |
packages/backend/src/api/controllers/SegmentController.ts |
Adds caller-role checks and policy-controlled deletion. |
packages/backend/src/api/controllers/FeatureFlagController.ts |
Applies the deletion policy and documents refusals. |
packages/backend/src/api/controllers/ExperimentController.ts |
Applies the deletion policy and documents refusals. |
packages/backend/CLAUDE.md |
Documents enforcement rules and internal exemptions. |
Review effort: Balanced
| * post: | ||
| * summary: Delete the selected experiments | ||
| * description: Deletes selected experiments sequentially regardless of state, skipping missing items. Execution failures stop the remaining items. Returns one result per ID. | ||
| * description: Deletes selected experiments sequentially. Requires the Admin or Creator role. Missing items are skipped; experiments that are not Draft, Inactive, Completed or Archived are refused and skipped. Execution failures stop the remaining items. Returns one result per ID. |
There was a problem hiding this comment.
"Execution failures stop the remaining items."
I guess this is pre-existing and i didn't catch it before, but I don't think one error should stop the execution, other items could delete successfully and there's no reason i can think of to stop that from happening, is there? It can return the non-delete reason and the UI can show the reason for items that didn't delete.
There was a problem hiding this comment.
It's unlikely that this will ever happen from a UI interaction, since batch deletes are disabled as long as at least one item isn't deletable. But, yeah, I figured for an API call (like one made by load tests) it makes sense to delete what's deletable and report what isn't.
The single DELETE /segments/:segmentId route passed awaitPostCommit=false, which sent it down the path that resolves affected flags and experiments before the transaction takes the target lock, so an owner attached in that gap was never recomputed. Drop awaitPostCommit (SegmentService.deleteSegment is back to its dev form): the single route now supplies its locking transaction like the batch route, so owners are collected after the lock and recomputation is awaited after commit. Adds an integration test that attaches an archived experiment just before the lock and checks its precomputed membership is updated. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>


Resolves #3326
Changes
Single and batch deletion of experiments, feature flags and segments now go through one backend policy. Before, these rules were only enforced in the UI.
ROLE_PERMISSIONStable, moved from the frontend'sauth.service.tsintoupgrade_types; the frontend now reads it too, so the two tables can't drift.DeletionPolicyServiceon fields read under the target's deletion row lock, inside the deletion transaction. There is no earlier unlocked read.type: DELETION_NOT_ALLOWED. Missing targets keep their existing responses.DELETE /segments/:segmentIdnow receives@CurrentUser().refusedoutcome and a reason code (state_not_deletable,segment_in_useorglobal_exclude_protected), and the batch continues, as it does fornot_found. A refused item writes no audit log and triggers no cache reset or recompute.DeletionRepository.isSegmentInUse()checks one segment with a single query and mirrors the Used status fromSegmentService.getSegmentStatus(). An integration test pins the two together across every kind of usage. (getSegmentStatus()reads every list, so it isn't reused here; the batch path deliberately avoids those reads.)refusedoutcome; refused items stay selected, and the snackbar says they were skipped. The UI already blocks ineligible selections, sorefusedonly appears when the list is out of date (for example, an experiment started after the page loaded) or for callers outside the UI.Behavior changes for existing callers
upgrade-automated-tests): they need an account with an allowed role, and test items that can be deleted (for example, stop experiments and disable flags) before cleanup. That repo isn't changed here.Tests
DeletionPolicyServicetests (role × entity, every experiment state, flag status, segment rules, single-delete transaction), plus refused/role cases inBatchDeleteService.[deletable, running, missing, deletable]batch, nested and archived-owner segment usage, and theisSegmentInUse↔getSegmentStatusagreement.🤖 Generated with Claude Code