Skip to content

Commit 66c4972

Browse files
authored
🐛 Fixed Analytics > Growth MRR chart starting at zero at beginning of time range (#25694)
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. <img width="1428" height="496" alt="Screenshot 2025-12-10 at 21 44 47@2x" src="https://github.com/user-attachments/assets/f53a6abc-6f51-4abe-872f-694ca1efa7c7" /> ## 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.
1 parent 8e89eb7 commit 66c4972

File tree

2 files changed

+50
-0
lines changed

2 files changed

+50
-0
lines changed

apps/stats/src/hooks/use-growth-stats.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,14 @@ export const useGrowthStats = (range: number) => {
296296
...mostRecentBeforeRange,
297297
date: dateFromMoment.format('YYYY-MM-DD')
298298
});
299+
} else if (result.length > 0) {
300+
// No data before range, use the earliest data point in the range
301+
// to fill in the start date (representing MRR at range start)
302+
const earliestInRange = [...result].sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime())[0];
303+
result.unshift({
304+
...earliestInRange,
305+
date: dateFromMoment.format('YYYY-MM-DD')
306+
});
299307
}
300308
}
301309

apps/stats/test/unit/hooks/use-growth-stats.test.tsx

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,48 @@ describe('useGrowthStats', () => {
279279
expect(result.current.mrrData.length).toBeGreaterThan(1);
280280
});
281281

282+
it('uses earliest data point when no MRR data exists before dateFrom', async () => {
283+
// Simulate the bug: API returns MRR data starting AFTER dateFrom
284+
// For a 90-day range, dateFrom would be ~90 days ago
285+
// But the MRR data only starts 15 days into that range
286+
const startDate = moment().subtract(89, 'days');
287+
const dateFrom = startDate.format('YYYY-MM-DD');
288+
289+
// MRR data starts 15 days AFTER dateFrom (no data before range)
290+
const firstMrrDate = moment(startDate).add(15, 'days').format('YYYY-MM-DD');
291+
const secondMrrDate = moment(startDate).add(16, 'days').format('YYYY-MM-DD');
292+
293+
const lateMrrData = [
294+
{date: firstMrrDate, mrr: 17286, currency: 'usd'},
295+
{date: secondMrrDate, mrr: 17286, currency: 'usd'}
296+
];
297+
298+
// Update getRangeDates mock to return predictable dates
299+
mockedGetRangeDates.mockImplementation(() => ({
300+
startDate,
301+
endDate: moment()
302+
}));
303+
304+
mockSuccess(mockedUseMrrHistory, {
305+
stats: lateMrrData,
306+
meta: {
307+
totals: [{mrr: 17286, currency: 'usd'}]
308+
}
309+
});
310+
311+
const {result} = renderHook(() => useGrowthStats(90));
312+
313+
await waitFor(() => {
314+
expect(result.current.isLoading).toBe(false);
315+
});
316+
317+
// The first data point should be at dateFrom (synthetic start point)
318+
expect(result.current.mrrData[0].date).toBe(dateFrom);
319+
320+
// The MRR value should be the earliest available value (17286), NOT 0
321+
expect(result.current.mrrData[0].mrr).toBe(17286);
322+
});
323+
282324
it('handles range=1 correctly', async () => {
283325
const {result} = renderHook(() => useGrowthStats(1));
284326

0 commit comments

Comments
 (0)