#6900·growthbook

[Bug] Stopped experiments with no phase end date show "ended <today>" and break Date sorting

Author: august-growthbookCreated Sep 9, 2026Updated Sep 10, 2026
Labelsbug

Summary

On /experiments#stopped, a stopped experiment whose last phase has no dateEnded renders as ended <today>, and that fabricated value is also the Date column's sort key — so those rows sort as if they ended today and the date changes every day. The experiment page shows the same gap as Runtime: <start> - now beside a "Stopped" badge.

Reported by an Enterprise Cloud customer with 148 stopped experiments, one of which stopped in March 2026 and was listed as ended Sep 9, 2026.

Reproduce

  1. Get an experiment to status: "stopped" with the last phase's dateEnded unset — e.g. POST /api/v1/experiments/:id with {"status":"stopped"}, a Statsig import of an abandoned experiment, or clearing the end date in the Edit Phase modal on an already-stopped experiment.
  2. Open /experiments#stopped sorted by Date.

Expected: no date, sorted after experiments that have a real end date. Actual: ended <today>, sorted to the top, changing daily.

Cause 1 — the display substitutes the current time

packages/front-end/services/experiments.ts:172-182:

: exp.status === "stopped"
  ? exp.phases?.[exp.phases?.length - 1]?.dateEnded
  : exp.dateCreated) ?? new Date().toISOString() // fallback to now

That value is rendered with the word "ended" (components/Experiment/ExperimentsListTable.tsx:212-227) and is also the sort key (services/experiments.ts:439-441,501; comparator services/search.tsx:397-406), so the display and the sort share one fabricated timestamp. search.tsx:400-403 already has an undefinedLast branch that the ?? makes unreachable.

The experiment page handles the same missing field separately and never checks status: components/Experiment/TabbedPage/ProjectTagBar.tsx:87-89 prints the literal "now". components/Metrics/DateGraph.tsx:324-327 already does this correctly, gating its fallback on status === "running".

Cause 2 — stopped transitions that never write dateEnded

dateEnded is optional with no default (shared/src/validators/experiments.ts:101-105, back-end/src/models/ExperimentModel.ts:261-265). stopExperiment() (services/experimentChanges/changeExperimentStatus.ts:834-846 — Stop modal, /stop, scheduled auto-stop) and postExperimentStatus for running → stopped (controllers/experiments.ts:2439-2453) do close the phase. These don't:

  • REST POST /api/v1/experiments/:id, which writes only the status (services/experiments.ts:4847); the handler special-cases only draft → running.
  • Internal POST /experiment/:id, where "phases" isn't in the writable key allowlist (controllers/experiments.ts:1875-1974).
  • Both create endpoints, which accept status: "stopped" with an open phase.
  • The Statsig importer, where decision_made/abandoned map to stopped with dateEnded: endTime ? … : "".
  • The Edit Phase modal, whose "Leave blank if still running" and Clear Input (EditPhaseModal.tsx:96-108controllers/experiments.ts:2898-2903) wipe a correct date.
  • A GET→PATCH round-trip: the serializer emits "" (services/experiments.ts:3064) and the update maps it back to undefined (:4654).

No migration or backfill exists, and phase dates are excluded from the audit diff (models/ExperimentModel.ts:1320-1327), so these leave no trace.

"Awaiting decision" is a co-symptom

That label derives only from archived/status/type/results (shared/src/enterprise/decision-criteria/statusIndicatorData.ts:105-116), never dateEnded. The paths above skip results too, so the same records lack both.

When it started

#4342 (2025-07-28, v4.1.0) changed the fallback from ?? "" to ?? new Date().toISOString() while fixing this column's sort; before it, the cell was blank but still sorted as today. The experiment page's - now arrived with #4848 (2026-01-06, v4.3.0). "Awaiting decision" is older (#3323, #3709) and unrelated. #6856 and #6827 address the sibling missing-dateStarted problem and don't touch this.

Suggested fix

Display, shippable on its own: return undefined from experimentDate() for a stopped experiment with no dateEnded, render an explicit "no end date", pass undefinedLast so those rows sort last, and gate the runtime line on experiment.status.

Write side: close the last phase on every stopped transition, ideally by routing these paths through stopExperiment(), and stop the round-trip from clearing a set value.

Open question: backfill

The display fix leaves existing records with no date, and there's no true end date to recover — only proxies (the phase's last snapshot, an audit-log status change, dateUpdated). Suggestion: backfill only where an audit event gives a real timestamp and leave the rest blank and labelled.

Verified against main @ 4b52e5f (2026-09-09).