diff --git a/release-notes/v0.0.24.md b/release-notes/v0.0.24.md index 1f06301..8378cee 100644 --- a/release-notes/v0.0.24.md +++ b/release-notes/v0.0.24.md @@ -1,10 +1,11 @@ -GSC UI, improved app layout and beta in app agent. +GSC UI, improved app layout and beta in app agent. ## What's new + - Get GSC Insights inside the app — thanks @mattmacrocket - - See queries in "striking distance" of ranking + - See queries in "striking distance" of ranking - (Beta) In app agent - MCP is still recommended, but we'll be working to improve this during the summer. - - Requires `OPENROUTER_API_KEY` + - Requires `OPENROUTER_API_KEY` - Redesigned the app layout ## Fixed diff --git a/src/client/features/audit/launch/LaunchView.tsx b/src/client/features/audit/launch/LaunchView.tsx index e5db4a1..b3d5a99 100644 --- a/src/client/features/audit/launch/LaunchView.tsx +++ b/src/client/features/audit/launch/LaunchView.tsx @@ -1,4 +1,4 @@ -import { AutumnProvider, useCustomer } from "autumn-js/react"; +import { useCustomer } from "autumn-js/react"; import { AuditHistorySection } from "@/client/features/audit/launch/AuditHistorySection"; import { LaunchFormCard } from "@/client/features/audit/launch/LaunchFormCard"; import { useLaunchController } from "@/client/features/audit/launch/useLaunchController"; @@ -18,11 +18,7 @@ export function LaunchView(props: LaunchViewProps) { return ; } - return ( - - - - ); + return ; } function HostedLaunchView(props: LaunchViewProps) { diff --git a/src/client/features/billing/FreePlanBanner.tsx b/src/client/features/billing/FreePlanBanner.tsx index 8b483ca..11b5351 100644 --- a/src/client/features/billing/FreePlanBanner.tsx +++ b/src/client/features/billing/FreePlanBanner.tsx @@ -1,5 +1,5 @@ import { Link } from "@tanstack/react-router"; -import { AutumnProvider, useCustomer } from "autumn-js/react"; +import { useCustomer } from "autumn-js/react"; import { useSession } from "@/lib/auth-client"; import { getCustomerPlanStatus } from "@/client/features/billing/plan-detection"; import { @@ -12,14 +12,6 @@ import { } from "@/shared/billing"; export function FreePlanBanner() { - return ( - - - - ); -} - -function FreePlanBannerContent() { const { data: session } = useSession(); const customerQuery = useCustomer({ queryOptions: { diff --git a/src/client/features/billing/HostedPlanGate.tsx b/src/client/features/billing/HostedPlanGate.tsx index 0508a47..ec020a6 100644 --- a/src/client/features/billing/HostedPlanGate.tsx +++ b/src/client/features/billing/HostedPlanGate.tsx @@ -1,5 +1,5 @@ import type { ReactNode } from "react"; -import { AutumnProvider, useCustomer } from "autumn-js/react"; +import { useCustomer } from "autumn-js/react"; import { useSession } from "@/lib/auth-client"; import { isHostedClientAuthMode } from "@/lib/auth-mode"; import { getCustomerPlanStatus } from "@/client/features/billing/plan-detection"; @@ -23,11 +23,7 @@ export function HostedPlanGate({ return children(SELF_HOSTED_PLAN_GATE); } - return ( - - {children} - - ); + return {children}; } function HostedPlanGateContent({ diff --git a/src/client/features/onboarding/OnboardingChat.tsx b/src/client/features/onboarding/OnboardingChat.tsx index b02191f..7075ec9 100644 --- a/src/client/features/onboarding/OnboardingChat.tsx +++ b/src/client/features/onboarding/OnboardingChat.tsx @@ -1,5 +1,4 @@ import { useMutation, useQuery } from "@tanstack/react-query"; -import { AutumnProvider } from "autumn-js/react"; import { useState } from "react"; import { Loader2 } from "lucide-react"; import { DEFAULT_LOCATION_CODE } from "@/shared/keyword-locations"; @@ -57,9 +56,7 @@ export function OnboardingChat() { {!domain ? ( ) : ( - - - + )} ); diff --git a/src/client/features/rank-tracking/RankTrackingDomainDetail.tsx b/src/client/features/rank-tracking/RankTrackingDomainDetail.tsx index b59fbcd..c777631 100644 --- a/src/client/features/rank-tracking/RankTrackingDomainDetail.tsx +++ b/src/client/features/rank-tracking/RankTrackingDomainDetail.tsx @@ -1,7 +1,7 @@ import { useMemo, useState } from "react"; import { toast } from "sonner"; import { useQuery, useQueryClient } from "@tanstack/react-query"; -import { AutumnProvider, useCustomer } from "autumn-js/react"; +import { useCustomer } from "autumn-js/react"; import { getLatestRankResults, getRankPositionMatrix, @@ -57,20 +57,7 @@ function deviceVisibility( }; } -export function RankTrackingDomainDetail(props: { - config: RankTrackingConfig; - projectId: string; - onBack: () => void; - onEdit: () => void; -}) { - return ( - - - - ); -} - -function RankTrackingDomainDetailInner({ +export function RankTrackingDomainDetail({ config, projectId, onBack, diff --git a/src/lib/auth-options.ts b/src/lib/auth-options.ts index 9debbd4..377164c 100644 --- a/src/lib/auth-options.ts +++ b/src/lib/auth-options.ts @@ -20,4 +20,17 @@ export const baseAuthOptions = { user: { additionalFields: userAdditionalFields, }, + session: { + // Serve getSession from a signed cookie instead of a DB round trip. The + // session lookup runs on every authenticated request, and the DB lives in + // us-east — from far colos that single query was ~1s of wall time. The + // trade-off is revocation lag: a session revoked elsewhere (sign-out on + // another device, password reset) stays valid on an already-issued cookie + // for up to maxAge. Authorization still hits the DB via the canonical + // project-access checks, so the cookie only vouches for identity. + cookieCache: { + enabled: true, + maxAge: 5 * 60, + }, + }, }; diff --git a/src/routes/__root.tsx b/src/routes/__root.tsx index a7abfa8..c1e97b5 100644 --- a/src/routes/__root.tsx +++ b/src/routes/__root.tsx @@ -9,6 +9,7 @@ import { import { TanStackRouterDevtoolsPanel } from "@tanstack/react-router-devtools"; import { TanStackDevtools } from "@tanstack/react-devtools"; import { QueryClientProvider } from "@tanstack/react-query"; +import { AutumnProvider } from "autumn-js/react"; import * as React from "react"; import { DefaultCatchBoundary } from "@/client/components/DefaultCatchBoundary"; import { ExportToSheetsModal } from "@/client/components/table/ExportToSheetsModal"; @@ -173,27 +174,37 @@ function RootDocument({ children }: { children: React.ReactNode }) { - - <> - - {children} - - - {showDevtools ? ( - , - defaultOpen: true, - }, - ]} + {/* Keep this the ONLY AutumnProvider. Each provider creates its own + customer cache (autumn-js bundles a private react-query, so it + never shares the app QueryClient), and every extra provider mount + pays its own ~1s getOrCreateCustomer round trip. It only provides + context — nothing fetches until a useCustomer consumer mounts. */} + + + <> + + {children} + + - ) : null} - - + {showDevtools ? ( + , + defaultOpen: true, + }, + ]} + /> + ) : null} + + + diff --git a/src/routes/_app/billing.tsx b/src/routes/_app/billing.tsx index 0e040f9..f34cae5 100644 --- a/src/routes/_app/billing.tsx +++ b/src/routes/_app/billing.tsx @@ -1,5 +1,5 @@ import { createFileRoute, notFound } from "@tanstack/react-router"; -import { AutumnProvider, useCustomer } from "autumn-js/react"; +import { useCustomer } from "autumn-js/react"; import { useEffect, useState } from "react"; import { useSession } from "@/lib/auth-client"; import { isHostedClientAuthMode } from "@/lib/auth-mode"; @@ -31,14 +31,6 @@ export const Route = createFileRoute("/_app/billing")({ }); function BillingPage() { - return ( - - - - ); -} - -function BillingPageContent() { const { data: session, isPending: isSessionPending } = useSession(); const [topUpAmount, setTopUpAmount] = useState("20"); const [isPending, setIsPending] = useState(false); diff --git a/src/routes/_authenticated.subscribe.tsx b/src/routes/_authenticated.subscribe.tsx index 2cee1b7..9037b61 100644 --- a/src/routes/_authenticated.subscribe.tsx +++ b/src/routes/_authenticated.subscribe.tsx @@ -1,5 +1,5 @@ import { Link, createFileRoute, useNavigate } from "@tanstack/react-router"; -import { AutumnProvider, useCustomer } from "autumn-js/react"; +import { useCustomer } from "autumn-js/react"; import { useEffect, useState } from "react"; import { ArrowRight, Settings, User } from "lucide-react"; import { ThemePreferenceMenuItems } from "@/client/components/ThemePreferenceMenuItems"; @@ -41,14 +41,6 @@ export const Route = createFileRoute("/_authenticated/subscribe")({ }); function SubscribePage() { - return ( - - - - ); -} - -function SubscribePageContent() { const navigate = useNavigate(); const { upgrade: isUpgradeFlow, redirect } = Route.useSearch(); const { data: session } = useSession(); diff --git a/src/server/features/rank-tracking/services/RankTrackingService.ts b/src/server/features/rank-tracking/services/RankTrackingService.ts index 1956ca5..717c9bb 100644 --- a/src/server/features/rank-tracking/services/RankTrackingService.ts +++ b/src/server/features/rank-tracking/services/RankTrackingService.ts @@ -269,8 +269,10 @@ async function refreshKeywordMetrics( projectId: string, billingCustomer: BillingCustomerContext, ): Promise<{ updated: number }> { - const config = await getValidatedConfig(configId, projectId); - const keywords = await RankTrackingRepository.getKeywordsForConfig(configId); + const [config, keywords] = await Promise.all([ + getValidatedConfig(configId, projectId), + RankTrackingRepository.getKeywordsForConfig(configId), + ]); if (keywords.length === 0) return { updated: 0 }; const client = createDataforseoClient(billingCustomer); diff --git a/src/server/features/rank-tracking/services/rankTrackingResults.ts b/src/server/features/rank-tracking/services/rankTrackingResults.ts index 0fbaa8d..5ae51e0 100644 --- a/src/server/features/rank-tracking/services/rankTrackingResults.ts +++ b/src/server/features/rank-tracking/services/rankTrackingResults.ts @@ -26,29 +26,28 @@ export async function getLatestResults( rows: RankTrackingRow[]; run: { id: string; lastCheckedAt: string } | null; }> { - const config = await RankTrackingRepository.getConfigById({ - configId, - projectId, - }); - if (!config) { - throw new AppError("INTERNAL_ERROR", "Rank tracking config not found"); - } - - const activeKeywords = - await RankTrackingRepository.getKeywordsForConfig(configId); - - // Get the latest snapshot per keyword per device (across all completed runs) - const currentSnapshots = - await RankTrackingRepository.getLatestSnapshotsForKeywords(configId); - - // Get comparison snapshots from before the target date const days = PERIOD_DAYS[comparePeriod]; const targetDate = toSqliteTimestamp( new Date(Date.now() - days * 24 * 60 * 60 * 1000), ); - const comparisonSnapshots = - await RankTrackingRepository.getSnapshotsBeforeDate(configId, targetDate); + // All four reads key off the inputs alone, so run them in one parallel round + // trip instead of four sequential ones — this endpoint is hot and the DB may + // be a continent away. The project-scoped config lookup doubles as the + // authorization gate for the configId-keyed reads racing alongside it: when + // config is null, throw without returning anything from the other reads. + const [config, activeKeywords, currentSnapshots, comparisonSnapshots] = + await Promise.all([ + RankTrackingRepository.getConfigById({ configId, projectId }), + RankTrackingRepository.getKeywordsForConfig(configId), + // Latest snapshot per keyword per device (across all completed runs) + RankTrackingRepository.getLatestSnapshotsForKeywords(configId), + // Comparison snapshots from before the target date + RankTrackingRepository.getSnapshotsBeforeDate(configId, targetDate), + ]); + if (!config) { + throw new AppError("INTERNAL_ERROR", "Rank tracking config not found"); + } const previousPositions = new Map(); for (const snap of comparisonSnapshots) { diff --git a/wrangler.jsonc b/wrangler.jsonc index 6beef23..1eacb3c 100644 --- a/wrangler.jsonc +++ b/wrangler.jsonc @@ -6,6 +6,18 @@ "compatibility_flags": ["nodejs_compat", "global_fetch_strictly_public"], "observability": { "enabled": true, + // Spans are billed as observability events (free tier 200k/day); add a + // traces head_sampling_rate below 1 if span volume becomes a cost concern. + "traces": { + "enabled": true, + }, + }, + // Smart Placement relocates only the `fetch` handler (cron + Workflow runs + // stay at the edge) toward external subrequests — Postgres via Hyperdrive, + // SEO APIs — not D1/KV/R2 bindings, so it's a DB no-op on the D1 default. + // Must live here: a dashboard-only toggle is wiped by `wrangler deploy`. + "placement": { + "mode": "smart", }, // Uncomment when deploying to the openseo.so Cloudflare account. Self-hosters // should leave this commented (or replace with their own zone) — otherwise