From 72eaa09c3283f063c8cb10b69c95e8dfccb5a580 Mon Sep 17 00:00:00 2001
From: Ben Senescu <44480372+bensenescu@users.noreply.github.com>
Date: Wed, 12 Aug 2026 19:57:29 -0400
Subject: [PATCH] Hide Analytics section when GA4 OAuth app is pending (#475)
---
.../ga4/GoogleAnalyticsConnectionCard.tsx | 179 +++++++++---------
.../features/projects/ProjectSettings.tsx | 10 +-
src/server/mcp/server.test.ts | 80 ++++++++
src/server/mcp/server.ts | 23 ++-
4 files changed, 193 insertions(+), 99 deletions(-)
create mode 100644 src/server/mcp/server.test.ts
diff --git a/src/client/features/ga4/GoogleAnalyticsConnectionCard.tsx b/src/client/features/ga4/GoogleAnalyticsConnectionCard.tsx
index 8ba977f..27520f4 100644
--- a/src/client/features/ga4/GoogleAnalyticsConnectionCard.tsx
+++ b/src/client/features/ga4/GoogleAnalyticsConnectionCard.tsx
@@ -28,10 +28,12 @@ export function GoogleAnalyticsConnectionCard({
projectId,
onDismiss,
dismissing = false,
+ heading,
}: {
projectId: string;
onDismiss?: () => void;
dismissing?: boolean;
+ heading?: React.ReactNode;
}) {
const hosted = isHostedClientAuthMode();
const queryClient = useQueryClient();
@@ -115,97 +117,100 @@ export function GoogleAnalyticsConnectionCard({
if (hiddenPendingApproval) return null;
return (
- }
- status={
- connectionQuery.isLoading
- ? undefined
- : selfHostedNeedsSetup
- ? "setup_required"
- : connected
- ? "connected"
- : "disconnected"
- }
- >
- {connectionQuery.isLoading ? (
-
-
- Checking…
-
- ) : selfHostedNeedsSetup ? (
-
-
- {onDismiss ? (
-
- ) : null}
-
- ) : connected && !picking ? (
- {
- setSelection(null);
- setPicking(true);
- }}
- onDisconnect={() => disconnectMutation.mutate()}
- disconnecting={disconnectMutation.isPending}
- />
- ) : showPicker ? (
- selection && setPropertyMutation.mutate(selection)}
- saving={setPropertyMutation.isPending}
- onRetry={() => void propertiesQuery.refetch()}
- secondaryAction={
- connected
- ? { label: "Cancel", onClick: () => setPicking(false) }
- : onDismiss
- ? {
- label: "Dismiss",
- disabled: dismissing,
- onClick: onDismiss,
- }
- : {
- label: "Disconnect",
- destructive: true,
- disabled: disconnectMutation.isPending,
- onClick: () => disconnectMutation.mutate(),
- }
- }
- />
- ) : (
-
-
- Connect GA4 to understand what organic visitors do after they land
- on your site.
-
-
-
+ <>
+ {heading}
+
}
+ status={
+ connectionQuery.isLoading
+ ? undefined
+ : selfHostedNeedsSetup
+ ? "setup_required"
+ : connected
+ ? "connected"
+ : "disconnected"
+ }
+ >
+ {connectionQuery.isLoading ? (
+
+
+ Checking…
+
+ ) : selfHostedNeedsSetup ? (
+
+
{onDismiss ? (
) : null}
-
- )}
-
+ ) : connected && !picking ? (
+
{
+ setSelection(null);
+ setPicking(true);
+ }}
+ onDisconnect={() => disconnectMutation.mutate()}
+ disconnecting={disconnectMutation.isPending}
+ />
+ ) : showPicker ? (
+ selection && setPropertyMutation.mutate(selection)}
+ saving={setPropertyMutation.isPending}
+ onRetry={() => void propertiesQuery.refetch()}
+ secondaryAction={
+ connected
+ ? { label: "Cancel", onClick: () => setPicking(false) }
+ : onDismiss
+ ? {
+ label: "Dismiss",
+ disabled: dismissing,
+ onClick: onDismiss,
+ }
+ : {
+ label: "Disconnect",
+ destructive: true,
+ disabled: disconnectMutation.isPending,
+ onClick: () => disconnectMutation.mutate(),
+ }
+ }
+ />
+ ) : (
+
+
+ Connect GA4 to understand what organic visitors do after they land
+ on your site.
+
+
+
+ {onDismiss ? (
+
+ ) : null}
+
+
+ )}
+
+ >
);
}
diff --git a/src/client/features/projects/ProjectSettings.tsx b/src/client/features/projects/ProjectSettings.tsx
index 4993142..43d80e4 100644
--- a/src/client/features/projects/ProjectSettings.tsx
+++ b/src/client/features/projects/ProjectSettings.tsx
@@ -63,8 +63,14 @@ export function ProjectSettings({ projectId }: { projectId: string }) {
- Analytics
-
+
+ Analytics
+
+ }
+ />
1} />
diff --git a/src/server/mcp/server.test.ts b/src/server/mcp/server.test.ts
new file mode 100644
index 0000000..7071add
--- /dev/null
+++ b/src/server/mcp/server.test.ts
@@ -0,0 +1,80 @@
+import { beforeEach, describe, expect, it, vi } from "vitest";
+import { createOpenSeoMcpServer } from "./server";
+
+const mcpServerMocks = vi.hoisted(() => ({
+ registerTool: vi.fn<(name: string, ...args: unknown[]) => void>(),
+}));
+
+vi.mock("@modelcontextprotocol/server", () => ({
+ McpServer: class {
+ registerTool(name: string, ...args: unknown[]) {
+ mcpServerMocks.registerTool(name, ...args);
+ }
+ },
+}));
+
+vi.mock("cloudflare:workers", () => ({
+ DurableObject: vi.fn(),
+ env: {},
+ waitUntil: vi.fn(),
+}));
+
+vi.mock("@/server/mcp/instrumentation", () => ({
+ instrumentMcpToolHandler: (
+ _name: string,
+ _outputSchema: unknown,
+ handler: unknown,
+ ) => handler,
+}));
+
+describe("createOpenSeoMcpServer", () => {
+ beforeEach(() => {
+ mcpServerMocks.registerTool.mockClear();
+ });
+
+ it("does not expose GA4-backed tools while OAuth approval is pending", () => {
+ createOpenSeoMcpServer({
+ openSeoAuth: {
+ userId: "user-1",
+ userEmail: "user@example.com",
+ organizationId: "org-1",
+ baseUrl: "https://example.com",
+ },
+ });
+
+ const registeredToolNames = mcpServerMocks.registerTool.mock.calls.map(
+ ([name]) => name,
+ );
+
+ expect(registeredToolNames).toContain("whoami");
+ expect(registeredToolNames).toContain("get_search_console_performance");
+ expect(registeredToolNames).not.toContain(
+ "get_google_analytics_organic_landing_pages",
+ );
+ expect(registeredToolNames).not.toContain(
+ "get_google_analytics_page_performance",
+ );
+ expect(registeredToolNames).not.toContain(
+ "get_google_analytics_key_events",
+ );
+ expect(registeredToolNames).not.toContain("get_search_opportunities");
+ expect(registeredToolNames).not.toContain(
+ "get_google_analytics_organic_overview",
+ );
+ expect(registeredToolNames).not.toContain(
+ "get_google_analytics_traffic_acquisition",
+ );
+ expect(registeredToolNames).not.toContain(
+ "get_google_analytics_measurement_health",
+ );
+ expect(registeredToolNames).not.toContain(
+ "get_google_analytics_ecommerce_performance",
+ );
+ expect(registeredToolNames).not.toContain(
+ "get_google_analytics_site_search",
+ );
+ expect(registeredToolNames).not.toContain(
+ "get_google_analytics_audience_breakdown",
+ );
+ });
+});
diff --git a/src/server/mcp/server.ts b/src/server/mcp/server.ts
index 88da8ea..956ede0 100644
--- a/src/server/mcp/server.ts
+++ b/src/server/mcp/server.ts
@@ -51,6 +51,7 @@ import {
getSearchConsolePerformanceTool,
inspectUrlsTool,
} from "@/server/mcp/tools/search-console-tools";
+import { GA4_OAUTH_APP_PENDING } from "@/shared/ga4";
import {
getAuditIssuesTool,
getAuditPagesTool,
@@ -166,16 +167,18 @@ export function createOpenSeoMcpServer(authProps: McpProps) {
register(getKeywordMetricsTool);
register(getSearchConsolePerformanceTool);
register(inspectUrlsTool);
- register(getGoogleAnalyticsOrganicLandingPagesTool);
- register(getGoogleAnalyticsPagePerformanceTool);
- register(getGoogleAnalyticsKeyEventsTool);
- register(getSearchOpportunitiesTool);
- register(getGoogleAnalyticsOrganicOverviewTool);
- register(getGoogleAnalyticsTrafficAcquisitionTool);
- register(getGoogleAnalyticsMeasurementHealthTool);
- register(getGoogleAnalyticsEcommercePerformanceTool);
- register(getGoogleAnalyticsSiteSearchTool);
- register(getGoogleAnalyticsAudienceBreakdownTool);
+ if (!GA4_OAUTH_APP_PENDING) {
+ register(getGoogleAnalyticsOrganicLandingPagesTool);
+ register(getGoogleAnalyticsPagePerformanceTool);
+ register(getGoogleAnalyticsKeyEventsTool);
+ register(getSearchOpportunitiesTool);
+ register(getGoogleAnalyticsOrganicOverviewTool);
+ register(getGoogleAnalyticsTrafficAcquisitionTool);
+ register(getGoogleAnalyticsMeasurementHealthTool);
+ register(getGoogleAnalyticsEcommercePerformanceTool);
+ register(getGoogleAnalyticsSiteSearchTool);
+ register(getGoogleAnalyticsAudienceBreakdownTool);
+ }
register(runSiteAuditTool);
register(getAuditStatusTool);
register(getAuditIssuesTool);