Skip to content

Conversation

@cmraible
Copy link
Collaborator

@cmraible cmraible commented Dec 11, 2025

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.
Screenshot 2025-12-10 at 21 44 47@2x

Cause

The /stats/mrr endpoint 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 dateFrom parameter 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.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 11, 2025

Walkthrough

The 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

  • Files to review:
    • apps/stats/src/hooks/use-growth-stats.ts — verify the synthetic data-point creation, date assignment, and that no other fields are inadvertently mutated.
    • apps/stats/test/unit/hooks/use-growth-stats.test.tsx — confirm the new test accurately mocks range dates and MRR results and asserts the intended behavior.

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Description check ✅ Passed The description clearly explains the problem (MRR chart starting at $0), root cause (sparse API dataset), and the implemented fix (filling missing range start with earliest MRR value).
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Title check ✅ Passed The title accurately describes the main fix: addressing an MRR chart that incorrectly starts at zero at the beginning of a time range. This directly matches the core problem and solution described in the PR objectives.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch chris-ny-328-investigate-mrr-chart-showing-zero-for-blueshirt-banter

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@cmraible cmraible changed the title Fixed MRR chart starting at zero at beginning of time range 🐛 Fixed MRR chart starting at zero at beginning of time range Dec 11, 2025
@cmraible cmraible requested a review from troyciesco December 11, 2025 05:46
@cmraible cmraible marked this pull request as ready for review December 11, 2025 05:52
Copy link
Contributor

@coderabbitai coderabbitai bot left a 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 scenario

This 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 mockedGetRangeDates to return the same startDate used for dateFrom keeps the assertion stable and decoupled from getRangeDates’ internal implementation, and the expectations on mrrData[0].date and mrrData[0].mrr directly guard against the original $0 at range start bug.

If you want to tighten readability slightly, you could extract 17286 into 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 refactor

The new else if (result.length > 0) branch is the right fix for the sparse-history scenario the PR describes: when there’s no data before dateFrom but there is data inside the range, you now prepend a synthetic point at dateFrom using the earliest in-range MRR value. That ensures the chart no longer starts at $0 and aligns with the new test.

Two follow‑ups to consider:

  1. Behavioral interaction with MRR percent-change logic

    calculateTotals derives percentChanges.mrr from mrrData and has special handling for “from beginning” ranges (isFromBeginningRange) where it previously assumed a starting value of 0 when there was no data exactly at dateFrom. With this change, any range that has at least one in-range MRR point will now have a synthetic dateFrom entry, 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 calculateTotals logic accordingly.

  2. Minor efficiency/readability tweak (optional)

    To obtain earliestInRange, you currently sort a copy of result:

    const earliestInRange = [...result].sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime())[0];

    Given result is 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

📥 Commits

Reviewing files that changed from the base of the PR and between 287d2a3 and 47c99a4.

📒 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

@cmraible cmraible force-pushed the chris-ny-328-investigate-mrr-chart-showing-zero-for-blueshirt-banter branch from 47c99a4 to 780385c Compare December 12, 2025 02:08
@cmraible cmraible changed the title 🐛 Fixed MRR chart starting at zero at beginning of time range 🐛 Fixed Analytics > Growth MRR chart starting at zero at beginning of time range Dec 12, 2025
@cmraible cmraible merged commit 66c4972 into main Dec 12, 2025
35 checks passed
@cmraible cmraible deleted the chris-ny-328-investigate-mrr-chart-showing-zero-for-blueshirt-banter branch December 12, 2025 02:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants