Hide Analytics section when GA4 OAuth app is pending (#475)
This commit is contained in:
parent
75aec8424f
commit
72eaa09c32
@ -28,10 +28,12 @@ export function GoogleAnalyticsConnectionCard({
|
|||||||
projectId,
|
projectId,
|
||||||
onDismiss,
|
onDismiss,
|
||||||
dismissing = false,
|
dismissing = false,
|
||||||
|
heading,
|
||||||
}: {
|
}: {
|
||||||
projectId: string;
|
projectId: string;
|
||||||
onDismiss?: () => void;
|
onDismiss?: () => void;
|
||||||
dismissing?: boolean;
|
dismissing?: boolean;
|
||||||
|
heading?: React.ReactNode;
|
||||||
}) {
|
}) {
|
||||||
const hosted = isHostedClientAuthMode();
|
const hosted = isHostedClientAuthMode();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
@ -115,97 +117,100 @@ export function GoogleAnalyticsConnectionCard({
|
|||||||
if (hiddenPendingApproval) return null;
|
if (hiddenPendingApproval) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<IntegrationConnectionCard
|
<>
|
||||||
title="Google Analytics"
|
{heading}
|
||||||
icon={<GoogleAnalyticsLogo className="size-5" />}
|
<IntegrationConnectionCard
|
||||||
status={
|
title="Google Analytics"
|
||||||
connectionQuery.isLoading
|
icon={<GoogleAnalyticsLogo className="size-5" />}
|
||||||
? undefined
|
status={
|
||||||
: selfHostedNeedsSetup
|
connectionQuery.isLoading
|
||||||
? "setup_required"
|
? undefined
|
||||||
: connected
|
: selfHostedNeedsSetup
|
||||||
? "connected"
|
? "setup_required"
|
||||||
: "disconnected"
|
: connected
|
||||||
}
|
? "connected"
|
||||||
>
|
: "disconnected"
|
||||||
{connectionQuery.isLoading ? (
|
}
|
||||||
<div className="flex items-center gap-2 text-sm text-base-content/50">
|
>
|
||||||
<span className="loading loading-spinner loading-sm" />
|
{connectionQuery.isLoading ? (
|
||||||
Checking…
|
<div className="flex items-center gap-2 text-sm text-base-content/50">
|
||||||
</div>
|
<span className="loading loading-spinner loading-sm" />
|
||||||
) : selfHostedNeedsSetup ? (
|
Checking…
|
||||||
<div className="space-y-3">
|
</div>
|
||||||
<GoogleOAuthSetupWarning
|
) : selfHostedNeedsSetup ? (
|
||||||
integrationName="Google Analytics"
|
<div className="space-y-3">
|
||||||
docsUrl={GA4_SELF_HOSTED_SETUP_DOCS_URL}
|
<GoogleOAuthSetupWarning
|
||||||
/>
|
integrationName="Google Analytics"
|
||||||
{onDismiss ? (
|
docsUrl={GA4_SELF_HOSTED_SETUP_DOCS_URL}
|
||||||
<DismissButton onClick={onDismiss} disabled={dismissing} />
|
/>
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
) : connected && !picking ? (
|
|
||||||
<ConnectedState
|
|
||||||
displayName={connection?.propertyDisplayName ?? ""}
|
|
||||||
propertyId={connection?.propertyId ?? ""}
|
|
||||||
timeZone={connection?.propertyTimeZone ?? ""}
|
|
||||||
currencyCode={connection?.propertyCurrencyCode ?? ""}
|
|
||||||
connectedByEmail={connection?.connectedByEmail ?? null}
|
|
||||||
onChange={() => {
|
|
||||||
setSelection(null);
|
|
||||||
setPicking(true);
|
|
||||||
}}
|
|
||||||
onDisconnect={() => disconnectMutation.mutate()}
|
|
||||||
disconnecting={disconnectMutation.isPending}
|
|
||||||
/>
|
|
||||||
) : showPicker ? (
|
|
||||||
<Ga4PropertyPicker
|
|
||||||
loading={propertiesQuery.isLoading}
|
|
||||||
error={propertiesQuery.isError}
|
|
||||||
accounts={accounts}
|
|
||||||
selection={selection}
|
|
||||||
onSelect={setSelection}
|
|
||||||
onSave={() => 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(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<div className="space-y-4">
|
|
||||||
<p className="text-sm text-base-content/70">
|
|
||||||
Connect GA4 to understand what organic visitors do after they land
|
|
||||||
on your site.
|
|
||||||
</p>
|
|
||||||
<div className="flex flex-wrap items-center gap-1">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={handleConnect}
|
|
||||||
className="inline-flex items-center gap-2.5 rounded-lg border border-base-300 bg-base-100 px-4 py-2.5 text-sm font-semibold text-base-content shadow-sm transition hover:bg-base-200 hover:shadow focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary"
|
|
||||||
>
|
|
||||||
<GoogleGlyph className="size-[18px]" />
|
|
||||||
Connect with Google
|
|
||||||
</button>
|
|
||||||
{onDismiss ? (
|
{onDismiss ? (
|
||||||
<DismissButton onClick={onDismiss} disabled={dismissing} />
|
<DismissButton onClick={onDismiss} disabled={dismissing} />
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
) : connected && !picking ? (
|
||||||
)}
|
<ConnectedState
|
||||||
</IntegrationConnectionCard>
|
displayName={connection?.propertyDisplayName ?? ""}
|
||||||
|
propertyId={connection?.propertyId ?? ""}
|
||||||
|
timeZone={connection?.propertyTimeZone ?? ""}
|
||||||
|
currencyCode={connection?.propertyCurrencyCode ?? ""}
|
||||||
|
connectedByEmail={connection?.connectedByEmail ?? null}
|
||||||
|
onChange={() => {
|
||||||
|
setSelection(null);
|
||||||
|
setPicking(true);
|
||||||
|
}}
|
||||||
|
onDisconnect={() => disconnectMutation.mutate()}
|
||||||
|
disconnecting={disconnectMutation.isPending}
|
||||||
|
/>
|
||||||
|
) : showPicker ? (
|
||||||
|
<Ga4PropertyPicker
|
||||||
|
loading={propertiesQuery.isLoading}
|
||||||
|
error={propertiesQuery.isError}
|
||||||
|
accounts={accounts}
|
||||||
|
selection={selection}
|
||||||
|
onSelect={setSelection}
|
||||||
|
onSave={() => 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(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<p className="text-sm text-base-content/70">
|
||||||
|
Connect GA4 to understand what organic visitors do after they land
|
||||||
|
on your site.
|
||||||
|
</p>
|
||||||
|
<div className="flex flex-wrap items-center gap-1">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleConnect}
|
||||||
|
className="inline-flex items-center gap-2.5 rounded-lg border border-base-300 bg-base-100 px-4 py-2.5 text-sm font-semibold text-base-content shadow-sm transition hover:bg-base-200 hover:shadow focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary"
|
||||||
|
>
|
||||||
|
<GoogleGlyph className="size-[18px]" />
|
||||||
|
Connect with Google
|
||||||
|
</button>
|
||||||
|
{onDismiss ? (
|
||||||
|
<DismissButton onClick={onDismiss} disabled={dismissing} />
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</IntegrationConnectionCard>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -63,8 +63,14 @@ export function ProjectSettings({ projectId }: { projectId: string }) {
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section id="google-analytics" className="space-y-3 scroll-mt-6">
|
<section id="google-analytics" className="space-y-3 scroll-mt-6">
|
||||||
<h2 className="text-sm font-medium text-base-content/50">Analytics</h2>
|
<GoogleAnalyticsConnectionCard
|
||||||
<GoogleAnalyticsConnectionCard projectId={projectId} />
|
projectId={projectId}
|
||||||
|
heading={
|
||||||
|
<h2 className="text-sm font-medium text-base-content/50">
|
||||||
|
Analytics
|
||||||
|
</h2>
|
||||||
|
}
|
||||||
|
/>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<DangerSection project={project} canArchive={projects.length > 1} />
|
<DangerSection project={project} canArchive={projects.length > 1} />
|
||||||
|
|||||||
80
src/server/mcp/server.test.ts
Normal file
80
src/server/mcp/server.test.ts
Normal file
@ -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",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -51,6 +51,7 @@ import {
|
|||||||
getSearchConsolePerformanceTool,
|
getSearchConsolePerformanceTool,
|
||||||
inspectUrlsTool,
|
inspectUrlsTool,
|
||||||
} from "@/server/mcp/tools/search-console-tools";
|
} from "@/server/mcp/tools/search-console-tools";
|
||||||
|
import { GA4_OAUTH_APP_PENDING } from "@/shared/ga4";
|
||||||
import {
|
import {
|
||||||
getAuditIssuesTool,
|
getAuditIssuesTool,
|
||||||
getAuditPagesTool,
|
getAuditPagesTool,
|
||||||
@ -166,16 +167,18 @@ export function createOpenSeoMcpServer(authProps: McpProps) {
|
|||||||
register(getKeywordMetricsTool);
|
register(getKeywordMetricsTool);
|
||||||
register(getSearchConsolePerformanceTool);
|
register(getSearchConsolePerformanceTool);
|
||||||
register(inspectUrlsTool);
|
register(inspectUrlsTool);
|
||||||
register(getGoogleAnalyticsOrganicLandingPagesTool);
|
if (!GA4_OAUTH_APP_PENDING) {
|
||||||
register(getGoogleAnalyticsPagePerformanceTool);
|
register(getGoogleAnalyticsOrganicLandingPagesTool);
|
||||||
register(getGoogleAnalyticsKeyEventsTool);
|
register(getGoogleAnalyticsPagePerformanceTool);
|
||||||
register(getSearchOpportunitiesTool);
|
register(getGoogleAnalyticsKeyEventsTool);
|
||||||
register(getGoogleAnalyticsOrganicOverviewTool);
|
register(getSearchOpportunitiesTool);
|
||||||
register(getGoogleAnalyticsTrafficAcquisitionTool);
|
register(getGoogleAnalyticsOrganicOverviewTool);
|
||||||
register(getGoogleAnalyticsMeasurementHealthTool);
|
register(getGoogleAnalyticsTrafficAcquisitionTool);
|
||||||
register(getGoogleAnalyticsEcommercePerformanceTool);
|
register(getGoogleAnalyticsMeasurementHealthTool);
|
||||||
register(getGoogleAnalyticsSiteSearchTool);
|
register(getGoogleAnalyticsEcommercePerformanceTool);
|
||||||
register(getGoogleAnalyticsAudienceBreakdownTool);
|
register(getGoogleAnalyticsSiteSearchTool);
|
||||||
|
register(getGoogleAnalyticsAudienceBreakdownTool);
|
||||||
|
}
|
||||||
register(runSiteAuditTool);
|
register(runSiteAuditTool);
|
||||||
register(getAuditStatusTool);
|
register(getAuditStatusTool);
|
||||||
register(getAuditIssuesTool);
|
register(getAuditIssuesTool);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user