Skip to content

feat(experiments): view results screen (/experiments/:id/results) - #37135

Draft
oidacra wants to merge 5 commits into
oidacra/experiments-portlet-screen-2-create-update-experfrom
issue-37004-experiments-results-screen
Draft

feat(experiments): view results screen (/experiments/:id/results)#37135
oidacra wants to merge 5 commits into
oidacra/experiments-portlet-screen-2-create-update-experfrom
issue-37004-experiments-results-screen

Conversation

@oidacra

@oidacra oidacra commented Aug 20, 2026

Copy link
Copy Markdown
Member

What

Screen 3 of the Experiments portlet: the Results screen on /experiments/:experimentId/results — header, stat strip, Daily/Bayesian tabs and summary table, with promote and stop.

Closes #37004

Stacked on #37064. Base is oidacra/experiments-portlet-screen-2-create-update-exper, so this diff shows only screen 3. Retarget to main after #37064 merges.

Reuse over reimplementation

The charts, the legend plugin, the headless table and the winner/promoted helpers already existed under old/, which is frozen and slated for deletion. Rather than duplicating them, they are relocated to shared/ and old/ is repointed at the new paths — one source of truth, and old/ stays deletable. Only import lines change under old/; git records the moves as R100/R099/R097, and old/'s specs pass unmodified.

The approved prototype drives visual and behavioural detail only.

Notable decisions

  • The load is sequential, not forkJoin. DRAFT and SCHEDULED settle on getById alone and never reach getResults, which is uncached and costs two analytics round-trips plus a Monte Carlo run. The branch reads experiment.status, not whether results is null.
  • A lost report does not cost the screen. When getById succeeds and getResults fails, the header, goal and schedule still render and the failure is reported inline. The full error card is reserved for a missing experiment. Blanking here regressed against the screen this replaces — reproduced against a CubeJS schema without the Events cube, where /results 400s while getById returns 200.
  • The winner comes from the backend. The prototype picks the leader by highest conversion rate with no significance gate, so it always names one. Using bayesianResult.suggestedWinner is what lets the "no winner yet" state exist at all.
  • Exactly one chart is mounted at a time. The legend plugin resolves its container by walking up from the canvas and calling querySelector, which from a shared ancestor returns the first match in document order — two mounted charts would silently claim each other's legend, with no thrown error. @if/@else makes that impossible.
  • The session gate is experiment-wide. Below ten total sessions the whole summary is one empty state; above it every row shows full data regardless of its own count.
  • Both Promote entry points confirm. Promoting a RUNNING experiment ends it server-side in the same call (ExperimentsAPIImpl.java:1409), so the strip's inline shortcut cannot be the one path that skips the dialog.
  • Lift is keyed, not positional. The baseline is resolved by key and isControl by name, so row order cannot change the arithmetic.

Verification

Check Result
nx test portlets-dot-experiments-portlet 52/52 suites, 914/914 tests
tsc --noEmit 0 errors in the portlet
nx affected -t lint pass
nx format:check clean

Four projects (dotcms-js, block-editor, dotcms-block-editor, dotcms-webcomponents) fail nx affected -t test for reasons unrelated to this branch: the only dotcms-models change here is 8 purely additive lines, block-editor has no dependency on experiments models, and dotcms-js fails on Cannot find module 'karma'.

Exercised in the browser on the routed screen: header, stat strip, tabs and summary render; tabs switch with a single legend container; the negative winner state renders; goal and period read off the experiment when the report is missing.

Acceptance criteria

  • Route renders stat strip, tabs and summary for RUNNING and ENDED
  • Daily and Bayesian charts use the existing Chart.js config; legend toggles series
  • Lift vs Original in points vs the control, green/red, em dash on control and on a zero-rate control
  • Experiment-wide 10-session gate replaces the whole summary
  • Refresh re-fetches without a full navigation
  • No suggested winner renders the negative state, not a false Leading Variant
  • Promote confirms, states the auto-end while RUNNING, then hides every Promote and chips the promoted row
  • Stop Experiment confirms and re-renders the header as ENDED in place
  • A misconfigured analytics app blocks only this route
  • The old UVE reports screen is untouched
  • Jest specs for the results store and the summary table

Not in this PR

  • The htmlLegendPlugin could not be exercised end to end: without results data there is no canvas. Needs an environment with analytics data, or Experiments: A/B Testing v2 #36763's migration.
  • New Language.properties keys need a dotCMS restart to load; the raw key shows until then.
  • E2E (empty state, session gate, promote confirm) lives in the dedicated E2E issue.

@claude

claude Bot commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

Claude finished @oidacra's task in 36s —— View job


I'll analyze this and get back to you.

The Results screen needs the Chart.js options, the interactive legend plugin, the
p-chart wrapper, the headless details table and the winner/promoted/chart-data
helpers. All of them live under old/, which is frozen and slated for deletion, so
they are relocated to shared/ and old/ is repointed at the new paths rather than
duplicating them: one source of truth, and old/ stays deletable.

Only import lines change under old/, so its behaviour is untouched and its specs
still pass unmodified. Three importers the plan had not accounted for turned up
in the Configure legacy screen and needed the same repoint.

getConversionRate, getConversionRateRage, getBayesianVariantResult and
getProbabilityToBeBest stay where they are: they are not needed outside old/, and
the dependency direction has to remain old/ -> shared/, never the reverse.
State is {experiment, results, status, refreshing, lastRefreshFailed}, keyed by
experimentId alone, so the screen is page-independent the way the old reports
store already was.

The load runs sequentially rather than as the old screen's forkJoin, because the
second call depends on the first: DRAFT and SCHEDULED settle on getById alone and
never reach getResults, which is uncached and costs two analytics round-trips plus
a Monte Carlo run. The branch reads the experiment's status, not whether results
happen to be null.

A failed refresh leaves results and status untouched, so the last good report
stays on screen and only a flag says the refresh failed. A first load that loses
only its report keeps the experiment and reports inline; blanking there would
regress against the screen this one replaces.

Lift vs Original is computed here since no field carries it. The baseline is
resolved by key and isControl by name, never by row position, so the order rows
arrive in cannot change the arithmetic.

The refresh flag is raised only for the statuses the handler will actually serve;
raising it for the others would spin forever with no request in flight.
Wires :experimentId/results, reachable on every status including DRAFT, and hangs
dotAnalyticsHealthCheckResolver off this route only so a broken analytics app takes
out the report and leaves the list reachable.

That resolver is a plain ResolveFn: it reports rather than redirects, so the screen
reads the resolved status and renders the misconfiguration state in place of the
report. The edit-page-coupled AnalyticsAppGuard is deliberately not reused.

DotExperimentsService is provided on the route rather than the component. The
resolver injects it and runs in the route injector, before the screen exists, so
providing it on the screen alone would still throw NG0201 on activation.

configureCommandsOf moves to the list util: the list's row action and the Results
header's Configuration button are two ways to the same URL, and a URL spelled out
twice is a URL that can drift.
Shell, header, stat strip, Daily/Bayesian tabs and summary table, per the approved
prototype.

Both charts reuse the relocated Chart.js options and legend plugin; the prototype's
inline SVG is not reimplemented and the Bayesian posterior comes entirely from the
backend. The two charts render in @if/@else so exactly one canvas is ever mounted:
the legend plugin resolves its container by walking up from the canvas and calling
querySelector, which from a shared ancestor returns the first match in document
order, so two mounted charts would silently claim each other's legend.

The stat strip is presentational and takes the winner legend from the backend's
suggested winner, never from a locally computed highest rate, so "no winner yet"
renders instead of a false Leading Variant claim. Goal and period come off the
experiment, so they still read when the report does not.

The session gate is experiment-wide: below ten total sessions the whole summary is
one empty state, with no per-row filtering above it.

Promote asks first from both entry points -- the strip's inline shortcut and the
table's per-row button -- since promoting a RUNNING experiment ends it server-side
in the same call. The table hosts its own dialog so the shell and the table never
share a key, which would open both at once.
Leads the row's action cell, with Configure staying the first kebab entry, as the
design lays it out.

Ungated on purpose, unlike every kebab entry: AllowedActionsByExperimentStatus
clears RUNNING and ENDED only, but the report renders a waiting state of its own
for an experiment with nothing counted yet, so the row offers it on every status.

Also drops the module-local configureCommandsOf for the shared helper.
@oidacra
oidacra force-pushed the issue-37004-experiments-results-screen branch from e2f3e2a to 52d89e5 Compare August 20, 2026 19:27
@github-actions github-actions Bot added Area : Frontend PR changes Angular/TypeScript frontend code and removed AI: Safe To Rollback labels Aug 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Area : Frontend PR changes Angular/TypeScript frontend code

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

Experiments Portlet — Screen 3: View Results

1 participant