fix(ga4): honor full date ranges instead of clamping to 90 days (#563)
This commit is contained in:
parent
bb099ad65a
commit
ad2d28ea6f
@ -127,12 +127,14 @@ common input:
|
||||
| `offset` | Non-negative integer; default 0 |
|
||||
|
||||
With no explicit dates, the range is the last 28 complete days in the GA4
|
||||
property time zone. Explicit ranges are inclusive. The report builder caps the
|
||||
end at the last complete property day and moves the start forward when the
|
||||
range exceeds 90 days. The response returns requested and resolved dates plus
|
||||
`end_date_clamped` or `start_date_clamped` warnings. Invalid date formats,
|
||||
reversed dates, and a single date without its pair return `validation_error`
|
||||
before an API call.
|
||||
property time zone. Explicit ranges are inclusive and honored in full; there is
|
||||
no maximum range. The report builder caps the end at the last complete property
|
||||
day. The response returns requested and resolved dates plus an
|
||||
`end_date_clamped` warning, which the text output also states. The organic
|
||||
overview trend is capped at 1,000 rows and reports `trend_truncated` (also
|
||||
stated in the text) when a range exceeds that. Invalid date
|
||||
formats, reversed dates, and a single date without its pair return
|
||||
`validation_error` before an API call.
|
||||
|
||||
Only these tool-specific inputs are accepted:
|
||||
|
||||
|
||||
@ -36,7 +36,7 @@ describe("Ga4OrganicOverviewService", () => {
|
||||
mocks.getByProjectId.mockResolvedValue(connection);
|
||||
});
|
||||
|
||||
it("returns an equal-length comparison and weekly trend", async () => {
|
||||
it("returns an equal-length comparison and flags a truncated trend", async () => {
|
||||
mocks.runReport
|
||||
.mockResolvedValueOnce({
|
||||
dimensionHeaders: [],
|
||||
@ -93,7 +93,7 @@ describe("Ga4OrganicOverviewService", () => {
|
||||
]),
|
||||
},
|
||||
],
|
||||
rowCount: 1,
|
||||
rowCount: 1200,
|
||||
});
|
||||
const result = await Ga4OrganicOverviewService.getOrganicOverview(
|
||||
{
|
||||
@ -120,6 +120,7 @@ describe("Ga4OrganicOverviewService", () => {
|
||||
sessions: 100,
|
||||
});
|
||||
expect(result.diagnostics).toEqual([]);
|
||||
expect(result.warnings).toEqual(["trend_truncated"]);
|
||||
expect(mocks.runReport).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
|
||||
|
||||
@ -142,7 +142,12 @@ async function getOrganicOverview(
|
||||
reports: reports.map((report) => report.reportMetadata),
|
||||
},
|
||||
quota: trendReport.quota ?? current.quota,
|
||||
warnings: dateRange.warnings,
|
||||
warnings: [
|
||||
...dateRange.warnings,
|
||||
...(trendReport.totalRowCount > trendReport.rows.length
|
||||
? ["trend_truncated"]
|
||||
: []),
|
||||
],
|
||||
};
|
||||
} catch (error) {
|
||||
mapGa4ReportError(error);
|
||||
|
||||
@ -134,7 +134,7 @@ describe("Ga4ReportingService", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("clamps explicit dates and nulls restricted metrics", async () => {
|
||||
it("clamps a future endDate, keeps a long startDate, and nulls restricted metrics", async () => {
|
||||
mocks.runReport.mockResolvedValue({
|
||||
...landingHeaders,
|
||||
rows: [
|
||||
@ -168,10 +168,10 @@ describe("Ga4ReportingService", () => {
|
||||
);
|
||||
|
||||
expect(result.request.resolvedDateRange).toEqual({
|
||||
startDate: "2026-05-08",
|
||||
startDate: "2025-01-01",
|
||||
endDate: "2026-08-05",
|
||||
});
|
||||
expect(result.warnings).toEqual(["end_date_clamped", "start_date_clamped"]);
|
||||
expect(result.warnings).toEqual(["end_date_clamped"]);
|
||||
expect(result.rows[0]?.purchaseRevenue).toBeNull();
|
||||
});
|
||||
|
||||
|
||||
@ -87,17 +87,12 @@ export function resolveGa4DateRange(
|
||||
-1,
|
||||
);
|
||||
let endDate = requestedDateRange?.endDate ?? lastCompleteDay;
|
||||
let startDate = requestedDateRange?.startDate ?? shiftGa4Date(endDate, -27);
|
||||
const startDate = requestedDateRange?.startDate ?? shiftGa4Date(endDate, -27);
|
||||
const warnings: string[] = [];
|
||||
if (endDate > lastCompleteDay) {
|
||||
endDate = lastCompleteDay;
|
||||
warnings.push("end_date_clamped");
|
||||
}
|
||||
const ninetyDayFloor = shiftGa4Date(endDate, -89);
|
||||
if (startDate < ninetyDayFloor) {
|
||||
startDate = ninetyDayFloor;
|
||||
warnings.push("start_date_clamped");
|
||||
}
|
||||
if (startDate > endDate) {
|
||||
throw new Ga4ReportError(
|
||||
"validation_error",
|
||||
|
||||
@ -253,6 +253,7 @@ describe("Google Analytics MCP tools", () => {
|
||||
},
|
||||
comparison: {},
|
||||
trend: [],
|
||||
warnings: [],
|
||||
});
|
||||
mocks.getMeasurementHealth.mockResolvedValue({
|
||||
status: "ok",
|
||||
|
||||
@ -257,6 +257,15 @@ function emptyOrganicHint(result: Ga4ReportResult): string {
|
||||
: " This report is limited to Organic Search.";
|
||||
}
|
||||
|
||||
function endDateClampNote(result: {
|
||||
warnings: string[];
|
||||
request: { resolvedDateRange: { endDate: string } };
|
||||
}) {
|
||||
return result.warnings.includes("end_date_clamped")
|
||||
? ` The requested endDate was moved back to ${result.request.resolvedDateRange.endDate}, the last complete Analytics day.`
|
||||
: "";
|
||||
}
|
||||
|
||||
function reportText(label: string, result: Ga4ReportResult) {
|
||||
const range = result.request.resolvedDateRange;
|
||||
const comparison = result.comparison
|
||||
@ -272,7 +281,7 @@ function reportText(label: string, result: Ga4ReportResult) {
|
||||
const paginate = result.pageInfo.hasMore
|
||||
? " More rows are available; call again with offset to page through them."
|
||||
: "";
|
||||
const summary = `${label}: ${result.rowCount} of ${result.totalRowCount} rows for ${range.startDate} through ${range.endDate}.${comparison}${diagnostics}${limited}${emptyOrganicHint(result)}${paginate}`;
|
||||
const summary = `${label}: ${result.rowCount} of ${result.totalRowCount} rows for ${range.startDate} through ${range.endDate}.${endDateClampNote(result)}${comparison}${diagnostics}${limited}${emptyOrganicHint(result)}${paginate}`;
|
||||
if (result.rows.length === 0) return summary;
|
||||
return `${summary}\n${formatMcpTable(result.rows, reportTableColumns(result))}`;
|
||||
}
|
||||
@ -280,7 +289,10 @@ function reportText(label: string, result: Ga4ReportResult) {
|
||||
function overviewText(result: Ga4OverviewResult) {
|
||||
const range = result.request.resolvedDateRange;
|
||||
const previousRange = result.request.previousDateRange;
|
||||
const summary = `Organic overview for ${range.startDate} through ${range.endDate}, compared with ${previousRange.startDate} through ${previousRange.endDate}.`;
|
||||
const trendTruncated = result.warnings.includes("trend_truncated")
|
||||
? ` The trend was cut at ${result.trend.length} rows; use trend=weekly or a shorter date range for the full series.`
|
||||
: "";
|
||||
const summary = `Organic overview for ${range.startDate} through ${range.endDate}, compared with ${previousRange.startDate} through ${previousRange.endDate}.${endDateClampNote(result)}${trendTruncated}`;
|
||||
if (!result.current) {
|
||||
return `${summary} No Organic Search rows for this date range.`;
|
||||
}
|
||||
|
||||
@ -552,6 +552,21 @@ describe("MCP tool text output (service-backed tools)", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("get_google_analytics_organic_landing_pages states an end-date clamp", async () => {
|
||||
mocks.runGa4Report.mockResolvedValue(
|
||||
makeGa4ReportResult({ warnings: ["end_date_clamped"] }),
|
||||
);
|
||||
|
||||
const result = await getGoogleAnalyticsOrganicLandingPagesTool.handler(
|
||||
{ projectId: "project_1", limit: 100, offset: 0 },
|
||||
toolContext,
|
||||
);
|
||||
|
||||
expect(textContent(result)).toEqual(
|
||||
"Organic landing pages: 0 of 0 rows for 2026-07-09 through 2026-08-05. The requested endDate was moved back to 2026-08-05, the last complete Analytics day. This report is limited to Organic Search.",
|
||||
);
|
||||
});
|
||||
|
||||
it("get_google_analytics_traffic_acquisition does not mention Organic Search when empty", async () => {
|
||||
mocks.runGa4Report.mockResolvedValue(
|
||||
makeGa4ReportResult({
|
||||
@ -587,6 +602,7 @@ describe("MCP tool text output (service-backed tools)", () => {
|
||||
resolvedDateRange: { startDate: "2026-07-09", endDate: "2026-08-05" },
|
||||
previousDateRange: { startDate: "2026-06-11", endDate: "2026-07-08" },
|
||||
},
|
||||
warnings: [],
|
||||
current: {
|
||||
sessions: 120,
|
||||
activeUsers: 80,
|
||||
@ -629,13 +645,14 @@ describe("MCP tool text output (service-backed tools)", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("get_google_analytics_organic_overview names Organic Search when there is no current row", async () => {
|
||||
it("get_google_analytics_organic_overview states a truncated trend and names Organic Search when there is no current row", async () => {
|
||||
mocks.getOrganicOverview.mockResolvedValue({
|
||||
status: "ok",
|
||||
request: {
|
||||
resolvedDateRange: { startDate: "2026-07-09", endDate: "2026-08-05" },
|
||||
previousDateRange: { startDate: "2026-06-11", endDate: "2026-07-08" },
|
||||
},
|
||||
warnings: ["trend_truncated"],
|
||||
current: null,
|
||||
previous: null,
|
||||
comparison: {},
|
||||
@ -648,7 +665,7 @@ describe("MCP tool text output (service-backed tools)", () => {
|
||||
);
|
||||
|
||||
expect(textContent(result)).toEqual(
|
||||
"Organic overview for 2026-07-09 through 2026-08-05, compared with 2026-06-11 through 2026-07-08. No Organic Search rows for this date range.",
|
||||
"Organic overview for 2026-07-09 through 2026-08-05, compared with 2026-06-11 through 2026-07-08. The trend was cut at 0 rows; use trend=weekly or a shorter date range for the full series. No Organic Search rows for this date range.",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user