Description
The new Experiments portlet (#36987) turns /experiments/new into a routed, autosaving Configure screen and adds list actions the current backend cannot support. Four API gaps stand in the way — all other mutations already exist (create, PATCH, start, end, cancel, archive, delete, variants, promote, results).
1. Archive from DRAFT + restore/unarchive
Today archive is only allowed from ENDED, and no unarchive/restore method exists at all. The approved design archives from DRAFT and ENDED, and restores an ARCHIVED experiment back to DRAFT. Add both transitions to ExperimentsAPIImpl + ExperimentsResource.
Frontend gating note: the matrix AllowedActionsByExperimentStatus (core-web/libs/dotcms-models/src/lib/dot-experiments-constants.ts:256-287) is read by the existing UVE list menu. Do not widen the existing archive: [ENDED] key — the new transitions get new keys (archiveDraft, restore) so the old UI's menu cannot change.
2. PATCH side effects (autosave blocker)
Every PATCH /api/v1/experiments/{id} currently overwrites targetingConditions with []: ExperimentForm defaults it to a non-null empty list, so the resource's != null guard always fires, creating an empty Rule + ConditionGroup on every call. The new screen autosaves on edit, so this would create one Rule per keystroke pause. Fix so an absent targetingConditions leaves the stored value untouched.
Related, document-only: trafficAllocation can never be PATCHed to 0 (if (getTrafficAllocation() > 0)) — harmless, the design's minimum is 1.
3. Goal operators: validate, then extend
The Operator enum has only EQUALS / CONTAINS / EXISTS, and operators are never validated: an unsupported operator is silently accepted and then silently ignored, producing a goal that never converts with no error anywhere. Validate on write (400 on unsupported). Then extend with the operators the design offers — STARTS_WITH, ENDS_WITH, REGEX — wired through the metric/query layer so they actually evaluate. If extending is too large for this issue, validation+reject still lands here (it is the bug) and the new operators split out; the UI then trims to the supported set.
4. Variant cap enforcement
VARIANTS_NUMBER_MAX = 3 (ExperimentsAPIImpl.java:105) is only enforced in calcBayesian (:1344) — not in addVariant (:964). A user can build 5 variants, run for two weeks, and discover the failure when opening Results. Enforce the cap in addVariant with a clear error. Open question: the design says "Up to 5 Variants" — raising the cap requires validating the Bayesian implementation for 5 arms; decide before implementing (the UI ships with whatever cap this issue lands).
Acceptance Criteria
Priority
High — the PATCH fix blocks the Create/Update screen of #36987 (autosave is the creation mechanism).
Additional Context
Description
The new Experiments portlet (#36987) turns
/experiments/newinto a routed, autosaving Configure screen and adds list actions the current backend cannot support. Four API gaps stand in the way — all other mutations already exist (create, PATCH, start, end, cancel, archive, delete, variants, promote, results).1. Archive from DRAFT + restore/unarchive
Today
archiveis only allowed fromENDED, and no unarchive/restore method exists at all. The approved design archives fromDRAFTandENDED, and restores anARCHIVEDexperiment back toDRAFT. Add both transitions toExperimentsAPIImpl+ExperimentsResource.Frontend gating note: the matrix
AllowedActionsByExperimentStatus(core-web/libs/dotcms-models/src/lib/dot-experiments-constants.ts:256-287) is read by the existing UVE list menu. Do not widen the existingarchive: [ENDED]key — the new transitions get new keys (archiveDraft,restore) so the old UI's menu cannot change.2. PATCH side effects (autosave blocker)
Every
PATCH /api/v1/experiments/{id}currently overwritestargetingConditionswith[]:ExperimentFormdefaults it to a non-null empty list, so the resource's!= nullguard always fires, creating an empty Rule + ConditionGroup on every call. The new screen autosaves on edit, so this would create one Rule per keystroke pause. Fix so an absenttargetingConditionsleaves the stored value untouched.Related, document-only:
trafficAllocationcan never be PATCHed to0(if (getTrafficAllocation() > 0)) — harmless, the design's minimum is 1.3. Goal operators: validate, then extend
The
Operatorenum has onlyEQUALS/CONTAINS/EXISTS, and operators are never validated: an unsupported operator is silently accepted and then silently ignored, producing a goal that never converts with no error anywhere. Validate on write (400 on unsupported). Then extend with the operators the design offers —STARTS_WITH,ENDS_WITH,REGEX— wired through the metric/query layer so they actually evaluate. If extending is too large for this issue, validation+reject still lands here (it is the bug) and the new operators split out; the UI then trims to the supported set.4. Variant cap enforcement
VARIANTS_NUMBER_MAX = 3(ExperimentsAPIImpl.java:105) is only enforced incalcBayesian(:1344) — not inaddVariant(:964). A user can build 5 variants, run for two weeks, and discover the failure when opening Results. Enforce the cap inaddVariantwith a clear error. Open question: the design says "Up to 5 Variants" — raising the cap requires validating the Bayesian implementation for 5 arms; decide before implementing (the UI ships with whatever cap this issue lands).Acceptance Criteria
DRAFTandENDED.ARCHIVED→DRAFT.AllowedActionsByExperimentStatusgainsarchiveDraftandrestorekeys; existing keys byte-identical.PATCHwithouttargetingConditionsdoes not create a Rule/ConditionGroup and leaves stored targeting untouched (integration test proving no new Rule rows).addVariantrejects the variant that would exceedVARIANTS_NUMBER_MAX.openapi.yamlregenerated (./mvnw compile -pl :dotcms-core -DskipTests) and committed with the annotation changes.Priority
High — the PATCH fix blocks the Create/Update screen of #36987 (autosave is the creation mechanism).
Additional Context
./mvnw test -pl :dotcms-core -Dtest=ExperimentsAPIImplTest·./mvnw verify -pl :dotcms-integration -Dcoreit.test.skip=false -Dit.test=ExperimentAPIImpIntegrationTest