-
-
Notifications
You must be signed in to change notification settings - Fork 11.3k
🐛 Fixed Analytics > Growth MRR chart starting at zero at beginning of time range #25694
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
🐛 Fixed Analytics > Growth MRR chart starting at zero at beginning of time range #25694
Conversation
WalkthroughThe hook useGrowthStats was updated to handle ranges that start before any available MRR data by synthesizing a data point at the range start using the earliest MRR value found within the range (copying that point but setting its date to the range start). A unit test was added to verify that when API data begins after the requested start date, the first MRR entry is created at the range start with the earliest in-range value. Estimated code review effort🎯 2 (Simple) | ⏱️ ~10 minutes
Pre-merge checks and finishing touches✅ Passed checks (3 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
🧹 Nitpick comments (2)
apps/stats/test/unit/hooks/use-growth-stats.test.tsx (1)
282-322: New MRR edge‑case test accurately captures the regression scenarioThis test does a good job of reproducing the “no data before dateFrom, sparse data inside range” case and verifying the synthetic start point behavior. Overriding
mockedGetRangeDatesto return the samestartDateused fordateFromkeeps the assertion stable and decoupled fromgetRangeDates’ internal implementation, and the expectations onmrrData[0].dateandmrrData[0].mrrdirectly guard against the original$0 at range startbug.If you want to tighten readability slightly, you could extract
17286into a named constant (e.g.,const INITIAL_MRR = 17286;) and reuse it in the data + assertion, but that’s purely cosmetic.apps/stats/src/hooks/use-growth-stats.ts (1)
299-306: Earliest-in-range fallback correctly fixes missing start-point case; verify impact on MRR % change and consider minor refactorThe new
else if (result.length > 0)branch is the right fix for the sparse-history scenario the PR describes: when there’s no data beforedateFrombut there is data inside the range, you now prepend a synthetic point atdateFromusing the earliest in-range MRR value. That ensures the chart no longer starts at$0and aligns with the new test.Two follow‑ups to consider:
Behavioral interaction with MRR percent-change logic
calculateTotalsderivespercentChanges.mrrfrommrrDataand has special handling for “from beginning” ranges (isFromBeginningRange) where it previously assumed a starting value of 0 when there was no data exactly atdateFrom. With this change, any range that has at least one in-range MRR point will now have a syntheticdateFromentry, so that “assume started from 0” branch may never run in practice. That effectively changes MRR percent‑change semantics for from‑beginning ranges to use the earliest available MRR instead of 0.Please double‑check this against product expectations; if YTD/beginning‑of‑time ranges are still meant to show “0 → current” style growth when historical data is incomplete, you might want to explicitly distinguish synthetic vs. real points or adjust the
calculateTotalslogic accordingly.Minor efficiency/readability tweak (optional)
To obtain
earliestInRange, you currently sort a copy ofresult:const earliestInRange = [...result].sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime())[0];Given
resultis typically small, this is fine; but you could avoid the extra sort by computing the earliest in a single pass:let earliestInRange = result[0]; for (const item of result) { if (new Date(item.date).getTime() < new Date(earliestInRange.date).getTime()) { earliestInRange = item; } }Functionally identical, slightly clearer that we’re just looking for the min date.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
apps/stats/src/hooks/use-growth-stats.ts(1 hunks)apps/stats/test/unit/hooks/use-growth-stats.test.tsx(1 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-11-24T17:29:43.865Z
Learnt from: CR
Repo: TryGhost/Ghost PR: 0
File: e2e/AGENTS.md:0-0
Timestamp: 2025-11-24T17:29:43.865Z
Learning: Applies to e2e/**/*.test.ts : Use factory pattern for all test data creation instead of hard-coded data or direct database manipulation
Applied to files:
apps/stats/test/unit/hooks/use-growth-stats.test.tsx
47c99a4 to
780385c
Compare
ref https://linear.app/ghost/issue/NY-328/investigate-mrr-chart-showing-zero-for-blueshirt-banter
Problem
In some cases, the MRR chart in Analytics > Growth will incorrectly start at $0 for the very first data point.

Cause
The
/stats/mrrendpoint returns a sparse dataset, only including dates in the response that had changes in MRR, so not every data point in the chart is returned by the API. The frontend logic for calculating the first data point was not accounting for this, and defaulting to $0.Fix
The fix is to check for this condition where there are missing data points between the
dateFromparameter passed to the API (the first day of the selected date range) and the first data point returned by the API, and to fill in the earliest MRR value for this range.