Remove GA4 launch gating; dashboard shows an Organic traffic card once connected (#505)
This commit is contained in:
parent
9305ea89d6
commit
a6f96c516e
@ -1,5 +1,3 @@
|
||||
import { GA4_OAUTH_APP_PENDING } from "@/shared/ga4";
|
||||
|
||||
type McpTool = {
|
||||
name: string;
|
||||
title: string;
|
||||
@ -254,14 +252,10 @@ const toolCategories: ToolCategory[] = [
|
||||
},
|
||||
];
|
||||
|
||||
const visibleCategories = GA4_OAUTH_APP_PENDING
|
||||
? toolCategories.filter((cat) => cat.label !== "Google Analytics")
|
||||
: toolCategories;
|
||||
|
||||
export function AvailableTools() {
|
||||
return (
|
||||
<div className="grid gap-x-8 gap-y-8 md:grid-cols-2">
|
||||
{visibleCategories.map((cat) => (
|
||||
{toolCategories.map((cat) => (
|
||||
<div key={cat.label}>
|
||||
<h3 className="text-xs font-semibold uppercase tracking-wide text-base-content/50">
|
||||
{cat.label}
|
||||
|
||||
@ -4,7 +4,6 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { toast } from "sonner";
|
||||
import { ChevronLeft, ChevronRight, Check } from "lucide-react";
|
||||
import { captureClientEvent } from "@/client/lib/posthog";
|
||||
import { Ga4ConnectCard } from "@/client/features/dashboard/Ga4ConnectCard";
|
||||
import {
|
||||
computeNextStep,
|
||||
isStepDone,
|
||||
@ -15,6 +14,7 @@ import {
|
||||
BacklinkPulseCard,
|
||||
GscCard,
|
||||
} from "@/client/features/dashboard/DashboardCards";
|
||||
import { Ga4Card } from "@/client/features/dashboard/Ga4Card";
|
||||
import { McpConnectCard } from "@/client/features/dashboard/McpConnectCard";
|
||||
import { WorkspaceMergeBanner } from "@/client/features/dashboard/WorkspaceMergeBanner";
|
||||
import { getStandardErrorMessage } from "@/client/lib/error-messages";
|
||||
@ -26,8 +26,6 @@ import {
|
||||
refreshDashboardBacklinkSnapshot,
|
||||
} from "@/serverFunctions/dashboard";
|
||||
import { setProjectDomain } from "@/serverFunctions/projects";
|
||||
import { useSession } from "@/lib/auth-client";
|
||||
import { isGa4ConnectAvailable } from "@/shared/ga4";
|
||||
import type { DashboardHeroStep } from "@/types/schemas/dashboard";
|
||||
|
||||
const HERO_COPY: Record<
|
||||
@ -236,7 +234,6 @@ function OnboardingChecklist({
|
||||
|
||||
export function DashboardPage({ projectId }: { projectId: string }) {
|
||||
const queryClient = useQueryClient();
|
||||
const { data: session } = useSession();
|
||||
|
||||
const activationQuery = useQuery({
|
||||
queryKey: ["dashboardActivation", projectId],
|
||||
@ -338,17 +335,13 @@ export function DashboardPage({ projectId }: { projectId: string }) {
|
||||
hasData: gscConnected,
|
||||
node: <GscCard projectId={projectId} connected={gscConnected} />,
|
||||
},
|
||||
...(isGa4ConnectAvailable(session?.user?.email) &&
|
||||
(ga4Connected || !activation.ga4.cardDismissedAt)
|
||||
...(ga4Connected || !activation.ga4.cardDismissedAt
|
||||
? [
|
||||
{
|
||||
key: "ga4",
|
||||
hasData: ga4Connected,
|
||||
node: (
|
||||
<Ga4ConnectCard
|
||||
projectId={projectId}
|
||||
connected={ga4Connected}
|
||||
/>
|
||||
<Ga4Card projectId={projectId} connected={ga4Connected} />
|
||||
),
|
||||
},
|
||||
]
|
||||
|
||||
185
src/client/features/dashboard/Ga4Card.tsx
Normal file
185
src/client/features/dashboard/Ga4Card.tsx
Normal file
@ -0,0 +1,185 @@
|
||||
import { Link } from "@tanstack/react-router";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import {
|
||||
Area,
|
||||
AreaChart,
|
||||
ResponsiveContainer,
|
||||
Tooltip,
|
||||
XAxis,
|
||||
YAxis,
|
||||
} from "recharts";
|
||||
import {
|
||||
CardShell,
|
||||
moreDetailsClass,
|
||||
PercentDelta,
|
||||
Stat,
|
||||
} from "@/client/features/dashboard/cardParts";
|
||||
import { Ga4ConnectCard } from "@/client/features/dashboard/Ga4ConnectCard";
|
||||
import {
|
||||
formatCount,
|
||||
formatCtr,
|
||||
} from "@/client/features/search-performance/SearchPerformanceColumns";
|
||||
import { getGa4DashboardReport } from "@/serverFunctions/ga4";
|
||||
|
||||
function formatTrendDay(date: string): string {
|
||||
// Construct in local time: Date.parse("2026-08-01") is UTC midnight, which
|
||||
// toLocaleDateString would render as the previous day west of Greenwich.
|
||||
const [year, month, day] = date.split("-").map(Number);
|
||||
return new Date(year, month - 1, day).toLocaleDateString(undefined, {
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
});
|
||||
}
|
||||
|
||||
function statValue(
|
||||
value: number | null,
|
||||
format: (value: number) => string,
|
||||
): string {
|
||||
return value === null ? "—" : format(value);
|
||||
}
|
||||
|
||||
function statDelta(current: number | null, previous: number | null) {
|
||||
return current !== null && previous !== null ? (
|
||||
<PercentDelta current={current} previous={previous} />
|
||||
) : undefined;
|
||||
}
|
||||
|
||||
function SessionsTooltip({
|
||||
active,
|
||||
payload,
|
||||
label,
|
||||
}: {
|
||||
active?: boolean;
|
||||
payload?: Array<{ value: number }>;
|
||||
label?: string;
|
||||
}) {
|
||||
if (!active || !payload?.length) return null;
|
||||
return (
|
||||
<div className="rounded-md border border-base-300 bg-base-100 px-3 py-2 shadow-sm">
|
||||
<p className="text-xs text-base-content/60">
|
||||
{label ? formatTrendDay(label) : ""}
|
||||
</p>
|
||||
<p className="text-sm font-medium tabular-nums">
|
||||
{formatCount(payload[0].value)} sessions
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function Ga4Card({
|
||||
projectId,
|
||||
connected,
|
||||
}: {
|
||||
projectId: string;
|
||||
connected: boolean;
|
||||
}) {
|
||||
const reportQuery = useQuery({
|
||||
queryKey: ["dashboardGa4Report", projectId],
|
||||
queryFn: () => getGa4DashboardReport({ data: { projectId } }),
|
||||
enabled: connected,
|
||||
});
|
||||
|
||||
// Not connected (or a dead grant discovered by the report call): the
|
||||
// connection card sells and runs the whole flow itself.
|
||||
if (!connected || (reportQuery.data && !reportQuery.data.connected)) {
|
||||
return <Ga4ConnectCard projectId={projectId} connected={connected} />;
|
||||
}
|
||||
|
||||
const report = reportQuery.data;
|
||||
|
||||
return (
|
||||
<CardShell
|
||||
title="Organic traffic"
|
||||
stamp="Google Analytics · last 28 days"
|
||||
action={
|
||||
<Link
|
||||
to="/p/$projectId/settings"
|
||||
params={{ projectId }}
|
||||
hash="google-analytics"
|
||||
className={moreDetailsClass}
|
||||
>
|
||||
Manage
|
||||
</Link>
|
||||
}
|
||||
>
|
||||
{reportQuery.isPending ? (
|
||||
<div className="space-y-3" aria-busy>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
{Array.from({ length: 4 }, (_, i) => (
|
||||
<div key={i} className="skeleton h-16" />
|
||||
))}
|
||||
</div>
|
||||
<div className="skeleton h-24" />
|
||||
</div>
|
||||
) : reportQuery.isError ? (
|
||||
<p className="text-sm text-base-content/60">
|
||||
Couldn’t load Google Analytics data. Try again shortly.
|
||||
</p>
|
||||
) : report?.connected ? (
|
||||
// Covers null (no report row) and 0: a zero-session period would
|
||||
// otherwise render an all-zero flatline chart in an empty box.
|
||||
!report.totals.sessions ? (
|
||||
<p className="text-sm text-base-content/60">
|
||||
No organic search traffic recorded in the last 28 days yet.
|
||||
</p>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<Stat
|
||||
label="Sessions"
|
||||
value={statValue(report.totals.sessions, formatCount)}
|
||||
sub={statDelta(
|
||||
report.totals.sessions,
|
||||
report.prevTotals.sessions,
|
||||
)}
|
||||
/>
|
||||
<Stat
|
||||
label="Active users"
|
||||
value={statValue(report.totals.activeUsers, formatCount)}
|
||||
sub={statDelta(
|
||||
report.totals.activeUsers,
|
||||
report.prevTotals.activeUsers,
|
||||
)}
|
||||
/>
|
||||
<Stat
|
||||
label="Engagement rate"
|
||||
value={statValue(report.totals.engagementRate, formatCtr)}
|
||||
/>
|
||||
<Stat
|
||||
label="Key events"
|
||||
value={statValue(report.totals.keyEvents, formatCount)}
|
||||
sub={statDelta(
|
||||
report.totals.keyEvents,
|
||||
report.prevTotals.keyEvents,
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="h-24">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<AreaChart
|
||||
data={report.trend}
|
||||
margin={{ top: 4, right: 0, bottom: 0, left: 0 }}
|
||||
>
|
||||
<XAxis dataKey="date" hide />
|
||||
<YAxis hide domain={[0, "auto"]} />
|
||||
<Tooltip
|
||||
content={<SessionsTooltip />}
|
||||
cursor={{ stroke: "currentColor", strokeOpacity: 0.2 }}
|
||||
/>
|
||||
<Area
|
||||
type="monotone"
|
||||
dataKey="sessions"
|
||||
stroke="var(--color-primary)"
|
||||
strokeWidth={2}
|
||||
fill="var(--color-primary)"
|
||||
fillOpacity={0.08}
|
||||
/>
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
) : null}
|
||||
</CardShell>
|
||||
);
|
||||
}
|
||||
@ -19,11 +19,7 @@ import {
|
||||
listGa4Properties,
|
||||
setGa4Property,
|
||||
} from "@/serverFunctions/ga4";
|
||||
import { useSession } from "@/lib/auth-client";
|
||||
import {
|
||||
GA4_SELF_HOSTED_SETUP_DOCS_URL,
|
||||
isGa4ConnectAvailable,
|
||||
} from "@/shared/ga4";
|
||||
import { GA4_SELF_HOSTED_SETUP_DOCS_URL } from "@/shared/ga4";
|
||||
|
||||
export function GoogleAnalyticsConnectionCard({
|
||||
projectId,
|
||||
@ -38,7 +34,6 @@ export function GoogleAnalyticsConnectionCard({
|
||||
}) {
|
||||
const hosted = isHostedClientAuthMode();
|
||||
const queryClient = useQueryClient();
|
||||
const { data: session } = useSession();
|
||||
const [picking, setPicking] = React.useState(false);
|
||||
const [selection, setSelection] = React.useState<Ga4PropertySelection | null>(
|
||||
null,
|
||||
@ -50,14 +45,6 @@ export function GoogleAnalyticsConnectionCard({
|
||||
});
|
||||
const connection = connectionQuery.data;
|
||||
const connected = Boolean(connection?.connected);
|
||||
// Hide the hosted connect surface while the OAuth app awaits Google's
|
||||
// approval, but keep the card for users who already hold a grant so they
|
||||
// can finish property selection or disconnect.
|
||||
const hiddenPendingApproval =
|
||||
!isGa4ConnectAvailable(session?.user?.email) &&
|
||||
hosted &&
|
||||
!connected &&
|
||||
!connection?.currentUserHasGrant;
|
||||
const selfHostedNeedsSetup =
|
||||
!hosted && connectionQuery.isSuccess && !connection?.googleOAuthConfigured;
|
||||
const showPicker = picking || (connection?.currentUserHasGrant && !connected);
|
||||
@ -92,6 +79,9 @@ export function GoogleAnalyticsConnectionCard({
|
||||
void queryClient.invalidateQueries({
|
||||
queryKey: ["dashboardActivation", projectId],
|
||||
});
|
||||
void queryClient.invalidateQueries({
|
||||
queryKey: ["dashboardGa4Report", projectId],
|
||||
});
|
||||
};
|
||||
const setPropertyMutation = useMutation({
|
||||
mutationFn: (selected: Ga4PropertySelection) =>
|
||||
@ -116,8 +106,6 @@ export function GoogleAnalyticsConnectionCard({
|
||||
});
|
||||
const handleConnect = () => void startGoogleLink("ga4", window.location.href);
|
||||
|
||||
if (hiddenPendingApproval) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
{heading}
|
||||
|
||||
@ -1,80 +0,0 @@
|
||||
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",
|
||||
);
|
||||
});
|
||||
});
|
||||
@ -6,7 +6,6 @@ import {
|
||||
import type { z } from "zod";
|
||||
import {
|
||||
createMcpToolContext,
|
||||
MCP_AUTH_CONTEXT_PROP,
|
||||
type McpProps,
|
||||
type ToolContext,
|
||||
} from "@/server/mcp/context";
|
||||
@ -63,7 +62,6 @@ import {
|
||||
getSearchConsolePerformanceTool,
|
||||
inspectUrlsTool,
|
||||
} from "@/server/mcp/tools/search-console-tools";
|
||||
import { GA4_OAUTH_APP_PENDING, isGa4ConnectAvailable } from "@/shared/ga4";
|
||||
import {
|
||||
getAuditIssuesTool,
|
||||
getAuditPagesTool,
|
||||
@ -186,21 +184,16 @@ export function createOpenSeoMcpServer(authProps: McpProps) {
|
||||
register(getKeywordMetricsTool);
|
||||
register(getSearchConsolePerformanceTool);
|
||||
register(inspectUrlsTool);
|
||||
if (
|
||||
!GA4_OAUTH_APP_PENDING ||
|
||||
isGa4ConnectAvailable(authProps[MCP_AUTH_CONTEXT_PROP].userEmail)
|
||||
) {
|
||||
register(getGoogleAnalyticsOrganicLandingPagesTool);
|
||||
register(getGoogleAnalyticsPagePerformanceTool);
|
||||
register(getGoogleAnalyticsKeyEventsTool);
|
||||
register(getSearchOpportunitiesTool);
|
||||
register(getGoogleAnalyticsOrganicOverviewTool);
|
||||
register(getGoogleAnalyticsTrafficAcquisitionTool);
|
||||
register(getGoogleAnalyticsMeasurementHealthTool);
|
||||
register(getGoogleAnalyticsEcommercePerformanceTool);
|
||||
register(getGoogleAnalyticsSiteSearchTool);
|
||||
register(getGoogleAnalyticsAudienceBreakdownTool);
|
||||
}
|
||||
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);
|
||||
|
||||
@ -2,7 +2,10 @@ import { createServerFn } from "@tanstack/react-start";
|
||||
import { getRequest } from "@tanstack/react-start/server";
|
||||
import { waitUntil } from "cloudflare:workers";
|
||||
import { z } from "zod";
|
||||
import { shiftGa4Date } from "@/server/features/ga4/services/Ga4Dates";
|
||||
import { Ga4OrganicOverviewService } from "@/server/features/ga4/services/Ga4OrganicOverviewService";
|
||||
import { Ga4Service } from "@/server/features/ga4/services/Ga4Service";
|
||||
import { Ga4ReportError } from "@/server/lib/ga4Errors";
|
||||
import { hasSelfHostedGoogleOAuthConfig } from "@/server/features/google/oauth-config";
|
||||
import {
|
||||
createSelfHostedGoogleAuthorizationUrl,
|
||||
@ -49,6 +52,81 @@ export const getGa4Connection = createServerFn({ method: "POST" })
|
||||
};
|
||||
});
|
||||
|
||||
function overviewMetric(
|
||||
row: Record<string, string | number | null> | null,
|
||||
name: string,
|
||||
): number | null {
|
||||
const value = row?.[name];
|
||||
return typeof value === "number" ? value : null;
|
||||
}
|
||||
|
||||
/** Zero-fill the daily trend across the resolved range: GA4 omits days with
|
||||
* no organic sessions, which would silently shrink the chart's x-axis. */
|
||||
function fillDailySessions(
|
||||
rows: Array<Record<string, string | number | null>>,
|
||||
range: { startDate: string; endDate: string },
|
||||
): Array<{ date: string; sessions: number }> {
|
||||
const sessionsByDate = new Map<string, number>();
|
||||
for (const row of rows) {
|
||||
// GA4's `date` dimension is YYYYMMDD; the range dates are YYYY-MM-DD.
|
||||
if (typeof row.date !== "string" || typeof row.sessions !== "number") {
|
||||
continue;
|
||||
}
|
||||
const iso = `${row.date.slice(0, 4)}-${row.date.slice(4, 6)}-${row.date.slice(6, 8)}`;
|
||||
sessionsByDate.set(iso, row.sessions);
|
||||
}
|
||||
const days: Array<{ date: string; sessions: number }> = [];
|
||||
for (
|
||||
let date = range.startDate;
|
||||
date <= range.endDate;
|
||||
date = shiftGa4Date(date, 1)
|
||||
) {
|
||||
days.push({ date, sessions: sessionsByDate.get(date) ?? 0 });
|
||||
}
|
||||
return days;
|
||||
}
|
||||
|
||||
/** The dashboard's GA4 card: organic totals vs the previous period plus a
|
||||
* daily sessions trend, over the default (last 28 complete days) range. */
|
||||
export const getGa4DashboardReport = createServerFn({ method: "POST" })
|
||||
.middleware(requireProjectContext)
|
||||
.validator(projectScopedSchema)
|
||||
.handler(async ({ context }) => {
|
||||
try {
|
||||
const overview = await Ga4OrganicOverviewService.getOrganicOverview({
|
||||
projectId: context.projectId,
|
||||
});
|
||||
const totals = (row: Record<string, string | number | null> | null) => ({
|
||||
sessions: overviewMetric(row, "sessions"),
|
||||
activeUsers: overviewMetric(row, "activeUsers"),
|
||||
engagementRate: overviewMetric(row, "engagementRate"),
|
||||
keyEvents: overviewMetric(row, "keyEvents"),
|
||||
});
|
||||
return {
|
||||
connected: true as const,
|
||||
totals: totals(overview.current),
|
||||
prevTotals: totals(overview.previous),
|
||||
trend: fillDailySessions(
|
||||
overview.trend,
|
||||
overview.request.resolvedDateRange,
|
||||
),
|
||||
};
|
||||
} catch (error) {
|
||||
// Not connected, a dead grant, or a lost/deleted property: the dashboard
|
||||
// card falls back to the connect card instead of retrying a report that
|
||||
// can never succeed. Other report errors are real faults.
|
||||
if (
|
||||
error instanceof Ga4ReportError &&
|
||||
(error.code === "ga4_not_connected" ||
|
||||
error.code === "ga4_reconnect_required" ||
|
||||
error.code === "ga4_property_inaccessible")
|
||||
) {
|
||||
return { connected: false as const };
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
|
||||
export const listGa4Properties = createServerFn({ method: "POST" })
|
||||
.middleware(requireProjectContext)
|
||||
.validator(projectScopedSchema)
|
||||
|
||||
@ -1,25 +1,6 @@
|
||||
/** Better Auth provider ID for the dedicated Google Analytics grant. */
|
||||
export const GA4_OAUTH_PROVIDER_ID = "google-analytics";
|
||||
|
||||
// Google hasn't approved the GA4 OAuth app yet, so hosted connect attempts
|
||||
// show Google's "unverified app" warning. Gates every GA4 connect surface;
|
||||
// flip to false once the app is approved. Self-hosted deployments use their
|
||||
// own OAuth app, so only hosted mode is gated.
|
||||
export const GA4_OAUTH_APP_PENDING = true;
|
||||
|
||||
// Google's OAuth verification reviewer tests with this account, so the GA4
|
||||
// connect/disconnect surfaces stay visible for it while the app is pending
|
||||
// approval. MCP tool registration stays gated for everyone until approval.
|
||||
const GA4_OAUTH_REVIEWER_EMAILS = new Set(["walkthrough@everyapp.dev"]);
|
||||
|
||||
/** Whether GA4 connect surfaces are visible to this user despite the pending gate. */
|
||||
export function isGa4ConnectAvailable(
|
||||
email: string | null | undefined,
|
||||
): boolean {
|
||||
if (!GA4_OAUTH_APP_PENDING) return true;
|
||||
return email != null && GA4_OAUTH_REVIEWER_EMAILS.has(email);
|
||||
}
|
||||
|
||||
export const GA4_OAUTH_SCOPES = [
|
||||
"openid",
|
||||
"email",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user