From 3edbd0c11e2cb4aedea7d5c83fef8dad087f4839 Mon Sep 17 00:00:00 2001 From: Shuvam Kumar <145355204+shuvamk@users.noreply.github.com> Date: Thu, 23 Jul 2026 19:27:25 +0530 Subject: [PATCH] fix(gsc): clamp month-based date ranges instead of overflowing short months (#133) --- .../features/gsc/searchAnalytics.test.ts | 17 +++++++++++ src/server/features/gsc/searchAnalytics.ts | 29 ++++++++++++------- 2 files changed, 35 insertions(+), 11 deletions(-) diff --git a/src/server/features/gsc/searchAnalytics.test.ts b/src/server/features/gsc/searchAnalytics.test.ts index 3536c6e..e62cae6 100644 --- a/src/server/features/gsc/searchAnalytics.test.ts +++ b/src/server/features/gsc/searchAnalytics.test.ts @@ -46,6 +46,23 @@ describe("resolveDateRange", () => { ); expect(startDate).toBe("2026-01-01"); }); + + it("subtracts calendar months without overflowing short months", () => { + const { startDate, endDate } = resolveDateRange( + { dateRange: "last_3_months" }, + new Date("2026-06-03T00:00:00Z"), + ); + expect(startDate).toBe("2026-02-28"); + expect(endDate).toBe("2026-05-31"); + }); + + it("clamps the 16-month floor to the last valid day of a short month", () => { + const { startDate } = resolveDateRange( + { dateRange: "last_16_months" }, + new Date("2026-06-30T00:00:00Z"), + ); + expect(startDate).toBe("2025-02-28"); + }); }); describe("buildSearchAnalyticsRequest", () => { diff --git a/src/server/features/gsc/searchAnalytics.ts b/src/server/features/gsc/searchAnalytics.ts index 40b0f8b..59cc8c7 100644 --- a/src/server/features/gsc/searchAnalytics.ts +++ b/src/server/features/gsc/searchAnalytics.ts @@ -72,6 +72,19 @@ function formatDate(date: Date): string { return date.toISOString().slice(0, 10); } +// Subtract calendar months in UTC, clamping the day to the target month's length. +function subtractUtcMonths(date: Date, months: number): Date { + const day = date.getUTCDate(); + const d = new Date(date); + d.setUTCDate(1); + d.setUTCMonth(d.getUTCMonth() - months); + const daysInTargetMonth = new Date( + Date.UTC(d.getUTCFullYear(), d.getUTCMonth() + 1, 0), + ).getUTCDate(); + d.setUTCDate(Math.min(day, daysInTargetMonth)); + return d; +} + function subtractRange(end: Date, range: GscDateRange): Date { const d = new Date(end); switch (range) { @@ -82,25 +95,19 @@ function subtractRange(end: Date, range: GscDateRange): Date { d.setUTCDate(d.getUTCDate() - 28); break; case "last_3_months": - d.setUTCMonth(d.getUTCMonth() - 3); - break; + return subtractUtcMonths(d, 3); case "last_6_months": - d.setUTCMonth(d.getUTCMonth() - 6); - break; + return subtractUtcMonths(d, 6); case "last_12_months": - d.setUTCMonth(d.getUTCMonth() - 12); - break; + return subtractUtcMonths(d, 12); case "last_16_months": - d.setUTCMonth(d.getUTCMonth() - 16); - break; + return subtractUtcMonths(d, 16); } return d; } function sixteenMonthFloor(today: Date): string { - const d = new Date(today); - d.setUTCMonth(d.getUTCMonth() - 16); - return formatDate(d); + return formatDate(subtractUtcMonths(today, 16)); } /** Resolve a convenience `dateRange` or explicit start/end into GSC dates.