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);