perf: cut cross-region latency (Smart Placement, session cookie cache, shared Autumn provider, parallel queries) (#359)
This commit is contained in:
parent
c9bcc444b4
commit
828fb17073
@ -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
|
||||
|
||||
@ -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 <LaunchContent {...props} isFreePlan={false} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<AutumnProvider>
|
||||
<HostedLaunchView {...props} />
|
||||
</AutumnProvider>
|
||||
);
|
||||
return <HostedLaunchView {...props} />;
|
||||
}
|
||||
|
||||
function HostedLaunchView(props: LaunchViewProps) {
|
||||
|
||||
@ -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 (
|
||||
<AutumnProvider>
|
||||
<FreePlanBannerContent />
|
||||
</AutumnProvider>
|
||||
);
|
||||
}
|
||||
|
||||
function FreePlanBannerContent() {
|
||||
const { data: session } = useSession();
|
||||
const customerQuery = useCustomer({
|
||||
queryOptions: {
|
||||
|
||||
@ -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 (
|
||||
<AutumnProvider>
|
||||
<HostedPlanGateContent>{children}</HostedPlanGateContent>
|
||||
</AutumnProvider>
|
||||
);
|
||||
return <HostedPlanGateContent>{children}</HostedPlanGateContent>;
|
||||
}
|
||||
|
||||
function HostedPlanGateContent({
|
||||
|
||||
@ -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 ? (
|
||||
<SiteForm projectId={projectId} />
|
||||
) : (
|
||||
<AutumnProvider>
|
||||
<OnboardingChatConversation projectId={projectId} domain={domain} />
|
||||
</AutumnProvider>
|
||||
<OnboardingChatConversation projectId={projectId} domain={domain} />
|
||||
)}
|
||||
</StrategyShell>
|
||||
);
|
||||
|
||||
@ -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 (
|
||||
<AutumnProvider>
|
||||
<RankTrackingDomainDetailInner {...props} />
|
||||
</AutumnProvider>
|
||||
);
|
||||
}
|
||||
|
||||
function RankTrackingDomainDetailInner({
|
||||
export function RankTrackingDomainDetail({
|
||||
config,
|
||||
projectId,
|
||||
onBack,
|
||||
|
||||
@ -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,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@ -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 }) {
|
||||
</head>
|
||||
<body>
|
||||
<ClientOnly>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<>
|
||||
<PostHogBootstrap />
|
||||
{children}
|
||||
<ExportToSheetsModal />
|
||||
<Toaster position="bottom-right" mobileOffset={{ bottom: 100 }} />
|
||||
{showDevtools ? (
|
||||
<TanStackDevtools
|
||||
config={{ position: "bottom-right" }}
|
||||
eventBusConfig={{ connectToServerBus: true }}
|
||||
plugins={[
|
||||
{
|
||||
name: "TanStack Router",
|
||||
render: <TanStackRouterDevtoolsPanel />,
|
||||
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. */}
|
||||
<AutumnProvider>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<>
|
||||
<PostHogBootstrap />
|
||||
{children}
|
||||
<ExportToSheetsModal />
|
||||
<Toaster
|
||||
position="bottom-right"
|
||||
mobileOffset={{ bottom: 100 }}
|
||||
/>
|
||||
) : null}
|
||||
</>
|
||||
</QueryClientProvider>
|
||||
{showDevtools ? (
|
||||
<TanStackDevtools
|
||||
config={{ position: "bottom-right" }}
|
||||
eventBusConfig={{ connectToServerBus: true }}
|
||||
plugins={[
|
||||
{
|
||||
name: "TanStack Router",
|
||||
render: <TanStackRouterDevtoolsPanel />,
|
||||
defaultOpen: true,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
) : null}
|
||||
</>
|
||||
</QueryClientProvider>
|
||||
</AutumnProvider>
|
||||
</ClientOnly>
|
||||
<Scripts />
|
||||
</body>
|
||||
|
||||
@ -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 (
|
||||
<AutumnProvider>
|
||||
<BillingPageContent />
|
||||
</AutumnProvider>
|
||||
);
|
||||
}
|
||||
|
||||
function BillingPageContent() {
|
||||
const { data: session, isPending: isSessionPending } = useSession();
|
||||
const [topUpAmount, setTopUpAmount] = useState("20");
|
||||
const [isPending, setIsPending] = useState(false);
|
||||
|
||||
@ -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 (
|
||||
<AutumnProvider>
|
||||
<SubscribePageContent />
|
||||
</AutumnProvider>
|
||||
);
|
||||
}
|
||||
|
||||
function SubscribePageContent() {
|
||||
const navigate = useNavigate();
|
||||
const { upgrade: isUpgradeFlow, redirect } = Route.useSearch();
|
||||
const { data: session } = useSession();
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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<string, number | null>();
|
||||
for (const snap of comparisonSnapshots) {
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user