Allow editing pages with an active experiment, with a non-blocking warning banner
@oidacra is already working on this.
Since Sep 10, 2026.
- Dominant language
- Java
- Stars
- 970
- Forks
- 486
- Avg merge
- 3d 33m
- Merged PRs (30d)
- 170
Description
Description
A page with an active experiment is currently locked for editing until the experiment ends or is cancelled. That makes experiments impractical on exactly the pages worth experimenting on: you cannot run an experiment on the home page, because content editors would be unable to touch it for the entire duration of the run.
The trade-off is backwards. Editing a variant mid-run does have a cost — visitors after the edit see different content than visitors before it, so the experiment's data ends up mixing two versions and the comparison gets muddier. But that is a judgement call for the editor to make, not something the product should enforce by freezing the page. Content teams cannot commit to leaving a page untouched for weeks.
Remove the constraint. Editing stays available while an experiment is RUNNING or SCHEDULED; the risk is communicated with a persistent, non-blocking warning instead.
On wording: editing does not invalidate results already collected — those measurements stay valid for the content that was live when they were taken. What it does is pollute the dataset going forward: from the edit onward the experiment is measuring different content, so the run mixes data from two versions. The banner copy must say that, not "invalidate".
Where the constraint lives
It is entirely frontend, in two places, and both block on RUNNING and SCHEDULED:
// core-web/libs/portlets/edit-ema/portlet/src/lib/utils/index.ts:681
const isBlockedByExperiment = [
DotExperimentStatus.RUNNING,
DotExperimentStatus.SCHEDULED
].includes(experiment?.status);
if (!hasEditPermission || isBlockedByExperiment) {
return false;
}
| Location | Symbol | Effect |
|---|---|---|
utils/index.ts:681 |
computeCanEditPage() |
Blocks entering edit mode |
store/features/editor/withEditor.ts:99 |
editorHasAccessToEditMode |
Blocks edit mode in the UVE store |
store/features/editor/withEditor.ts:124 |
hasPermissionToEditLayout |
Blocks template/layout editing |
No backend enforcement exists. ExperimentsAPIImpl guards only overlapping experiment scheduling (getRunningExperimentsOnPage) — it never rejects a contentlet save because an experiment is running. Removing the frontend guard is therefore sufficient; there is no server-side counterpart to change.
Note that SCHEDULED is blocked too, so a page is frozen from the moment an experiment is scheduled — before a single result has been collected. Both statuses are unblocked by this issue.
The warning
The old, pre-UVE behavior warned rather than blocked. The copy still exists and is already translated:
# dotCMS/src/main/webapp/WEB-INF/messages/Language.properties:6034
experiment.running.edit.confirmation=An Experiment is currently running on this Page. \
If you Edit the Page, you may invalidate any results already collected. \
<b>Are you sure you want to continue?</b>
We are not restoring the confirm dialog. Instead: a persistent, non-blocking banner, always visible while editing a page under an active experiment. No dialog, no click-through, no per-session dismissal state to manage — the warning simply stays in view for as long as it is true.
The UVE toolbar already renders a related indicator — dot-ema-running-experiment shows a green "Running until <date>" tag linking to the reports screen — but it is informational, gated on RUNNING only, and says nothing about the risk of editing. The banner is the warning surface; how it relates to that existing tag is a design call.
What the banner looks like
Follow the existing page-lock banner precedent — p-message severity="warn" pinned at the top of the UVE shell (dot-ema-shell.component.html:2), so this is a known pattern rather than a new surface.
RUNNING — data is being collected right now, so an edit splits the run across two versions of the content:
┌────────────────────────────────────────────────────────────────────────────┐
│ ⚠ An experiment is running on this page. Editing a variant now means │
│ its results will mix data from before and after your changes. │
│ View experiment │
└────────────────────────────────────────────────────────────────────────────┘
SCHEDULED — nothing is being collected yet, so there is nothing to pollute; edits simply become the baseline:
┌────────────────────────────────────────────────────────────────────────────┐
│ ⚠ An experiment is scheduled to start on this page. Edits you make │
│ before it starts become part of what it measures. │
│ View experiment │
└────────────────────────────────────────────────────────────────────────────┘
The two statuses get different copy on purpose, because the risk is genuinely different:
| Status | What editing actually does | What to tell the user |
|---|---|---|
RUNNING |
Data collection is live. After the edit, the experiment measures different content, so the run's data spans two versions. | The results will mix data from before and after the change. |
SCHEDULED |
Nothing is being collected. The edit lands before the run begins. | Edits made now simply become part of what the experiment measures. |
Neither string says "invalidate" — results already collected stay valid for the content that was live when they were taken. The problem is pollution of the dataset going forward, not retroactive destruction of what is already there. A SCHEDULED experiment has no pollution risk at all, which is why its copy is informational rather than cautionary.
Proposed message keys — naming follows the existing uve.shell.page.locked.* convention:
uve.shell.experiment.running.edit.warning=An experiment is running on this page. Editing a variant now means its results will mix data from before and after your changes.
uve.shell.experiment.scheduled.edit.warning=An experiment is scheduled to start on this page. Edits you make before it starts become part of what it measures.
uve.shell.experiment.view.experiment=View experiment
The pre-UVE copy at Language.properties:6034 is not reusable, for three reasons: it ends in "Are you sure you want to continue?", which belongs to a dialog; it covers only the running case; and its central claim — that editing "may invalidate any results already collected" — is inaccurate. Do not copy that phrasing forward.
Markup sketch, mirroring the lock banner:
@if ($showExperimentBanner()) {
<p-message severity="warn" data-testid="experiment-edit-warning">
<div class="flex items-center gap-2">
<i class="pi pi-exclamation-triangle"></i>
<span class="inline leading-6" data-testid="experiment-edit-warning-content">
{{ $experimentWarningKey() | dm }}
<a
class="inline-flex items-center text-primary-600 underline hover:text-primary-700"
[routerLink]="['/edit-page/experiments/', $pageId(), $experimentId(), 'reports']"
queryParamsHandling="preserve">
{{ 'uve.shell.experiment.view.experiment' | dm }}
</a>
</span>
</div>
</p-message>
}
One deliberate divergence from the lock banner: that one carries a close (×) button and $showBanner() dismissal state. This banner has no close button — the whole point of choosing a persistent banner over a confirm dialog was that the warning stays in view while it is true. Copy the lock banner's layout, not its dismissal.
Final copy is a proposal — worth a look from product/design before implementation, but the RUNNING/SCHEDULED split should survive whatever wording lands.
Acceptance Criteria
Removing the block
- A user with edit permission can enter edit mode on a page whose experiment is
RUNNING. - A user with edit permission can enter edit mode on a page whose experiment is
SCHEDULED. - Content in the original variant can be edited while an experiment is
RUNNINGorSCHEDULED. - Content in a created variant can be edited while an experiment is
RUNNINGorSCHEDULED. - Template/layout editing is available on a page with a
RUNNINGorSCHEDULEDexperiment —hasPermissionToEditLayoutno longer consults experiment status. -
computeCanEditPage()no longer consults experiment status; its result depends only on edit permission and lock state. - Saving an edit made during an active experiment succeeds and persists, with no error from the page or contentlet APIs.
What must NOT change
- Page lock behavior is untouched: a page locked by another user is still not editable, exactly as on a page with no experiment.
- Edit permission is still enforced — removing the experiment guard does not grant edit access to a user without
canEdit. - The experiment itself keeps running: editing does not end, cancel, pause, or otherwise alter the experiment's status or schedule.
- Existing overlap rules are untouched —
ExperimentsAPIImplstill prevents two experiments running on the same page. - The existing
dot-ema-running-experimenttoolbar tag continues to link to the reports screen.
Data integrity — collected data survives the edit
The whole justification for allowing mid-run edits is that the cost is future data pollution, not destruction of what is already collected. That has to be true in practice, not just in the copy:
- Editing a variant — original or created — does not delete, reset, purge, or archive any experiment data already collected.
- Data collected before an edit remains accessible on the experiment's results screen after the edit.
-
GET /v1/experiments/{id}/resultsreturns the pre-edit measurements unchanged for the period preceding the edit — the numbers do not shift or zero out because content changed. - The experiment keeps its identity across an edit: same
id, same variants, samerunningIds, samelookBackWindow. An edit does not start a new run or rotate a running id. - Editing does not alter the experiment's
scheduling(start or end date). - No new subscriber to contentlet save or publish events is added that touches experiment data. The existing
ContentletDeletedEventsubscription inExperimentsAPIImplstays scoped to page deletion and is not widened to saves. - Regression check: run an experiment, record the results, edit a variant, and confirm the previously recorded results are still returned and still visible.
The warning banner
- A persistent, non-blocking banner is shown while editing a page whose experiment is
RUNNINGorSCHEDULED. - Under
RUNNING, the banner explains that the results will mix data from before and after the change. - Under
SCHEDULED, the banner uses different copy stating that edits made before the start become part of what the experiment measures. - No banner copy claims that editing invalidates or discards results already collected — that is not what happens.
- The banner uses
p-message severity="warn", matching the page-lock banner indot-ema-shell.component.html. - The banner has no close button and no dismissal state — unlike the lock banner it is modelled on.
- The banner links to the experiment's reports screen.
- The banner does not block, gate, or delay any editing action — no confirm step, no disabled controls.
- The banner stays visible for the whole editing session; it is not dismissible-and-forgotten within that session.
- The banner is not shown on pages with no experiment, or whose experiment is
DRAFT,ENDED, orARCHIVED. - The banner disappears once the experiment ends, is cancelled, or is archived.
- Banner copy goes through the
| dmmessage pipe — no hardcoded strings.
Edge cases
- A
SCHEDULEDexperiment that flips toRUNNINGwhile the user is editing keeps the page editable; the banner stays correct. - An experiment that ends while the user is editing leaves the page editable and removes the banner.
- A page with no experiment behaves exactly as before — no banner, no behavior change.
Tests
- Unit tests for
computeCanEditPage()coveringRUNNING,SCHEDULED,DRAFT,ENDED,ARCHIVED, and no-experiment — asserting experiment status no longer affects the result. - Store tests for
editorHasAccessToEditModeandhasPermissionToEditLayoutasserting both are true underRUNNINGandSCHEDULEDgiven edit permission and an unlocked page. - Regression test: a page locked by another user stays non-editable under a
RUNNINGexperiment — the lock rule survives the guard's removal. - Component test (Spectator,
byTestId) for banner visibility across each experiment status.
Priority
Medium
Additional Context
Decisions made during refinement:
| Question | Decision |
|---|---|
| Which statuses unblock | Both RUNNING and SCHEDULED — the guard goes away entirely |
| Warning UX | Persistent non-blocking banner — not the legacy confirm dialog |
| Template/layout editing | Also unblocked — treated the same as content |
| Recording edits made mid-experiment | Out of scope — file separately if results need annotating |
| Banner copy | Distinct strings per status. Framed as data pollution going forward, never as invalidating results already collected |
| Banner dismissal | None — no close button, unlike the lock banner it borrows its layout from |
| Collected data | Must stay intact and accessible across an edit — verified explicitly, not assumed |
Relevant files:
core-web/libs/portlets/edit-ema/portlet/src/lib/utils/index.ts:681—computeCanEditPage()core-web/libs/portlets/edit-ema/portlet/src/lib/store/features/editor/withEditor.ts:99,124—editorHasAccessToEditMode,hasPermissionToEditLayoutcore-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/dot-uve-toolbar/components/dot-ema-running-experiment/— existing running-experiment tagcore-web/libs/portlets/edit-ema/portlet/src/lib/dot-ema-shell/dot-ema-shell.component.html:2— the page-lock banner to model the new banner ondotCMS/src/main/webapp/WEB-INF/messages/Language.properties:6033-6035— existing experiment-edit warning copydocs/backend/EXPERIMENTS_CONSTRAINTS.md— status lifecycle and the rules that stay in force
On the data-integrity ACs: those are a regression guard, not new work. Experiment results are queried live from CubeJS (ExperimentsAPIImpl.getResults() → ExperimentResultsQueryFactory), and the only contentlet event Experiments subscribes to is ContentletDeletedEvent → checkAndDeleteExperiment — triggered by deleting the page, never by saving it. So no code path purges collected data on an edit today, and the ACs exist to keep it that way once the edit guard is gone.
Known consequence, accepted: editing a variant mid-run pollutes the experiment's dataset — from that point on it is measuring different content, so the run mixes data from two versions and the comparison is weaker. It does not invalidate or discard the measurements already taken; those remain accurate for the content that was live when they were collected.
That is the point of the change: the decision moves to the editor, and the banner makes the trade-off explicit. Anyone who wants a clean comparison can still end or cancel the experiment before editing.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Assessment
This issue has not been assessed yet.