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
|
## What's new
|
||||||
|
|
||||||
- Get GSC Insights inside the app — thanks @mattmacrocket
|
- 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.
|
- (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
|
- Redesigned the app layout
|
||||||
|
|
||||||
## Fixed
|
## 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 { AuditHistorySection } from "@/client/features/audit/launch/AuditHistorySection";
|
||||||
import { LaunchFormCard } from "@/client/features/audit/launch/LaunchFormCard";
|
import { LaunchFormCard } from "@/client/features/audit/launch/LaunchFormCard";
|
||||||
import { useLaunchController } from "@/client/features/audit/launch/useLaunchController";
|
import { useLaunchController } from "@/client/features/audit/launch/useLaunchController";
|
||||||
@ -18,11 +18,7 @@ export function LaunchView(props: LaunchViewProps) {
|
|||||||
return <LaunchContent {...props} isFreePlan={false} />;
|
return <LaunchContent {...props} isFreePlan={false} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return <HostedLaunchView {...props} />;
|
||||||
<AutumnProvider>
|
|
||||||
<HostedLaunchView {...props} />
|
|
||||||
</AutumnProvider>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function HostedLaunchView(props: LaunchViewProps) {
|
function HostedLaunchView(props: LaunchViewProps) {
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { Link } from "@tanstack/react-router";
|
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 { useSession } from "@/lib/auth-client";
|
||||||
import { getCustomerPlanStatus } from "@/client/features/billing/plan-detection";
|
import { getCustomerPlanStatus } from "@/client/features/billing/plan-detection";
|
||||||
import {
|
import {
|
||||||
@ -12,14 +12,6 @@ import {
|
|||||||
} from "@/shared/billing";
|
} from "@/shared/billing";
|
||||||
|
|
||||||
export function FreePlanBanner() {
|
export function FreePlanBanner() {
|
||||||
return (
|
|
||||||
<AutumnProvider>
|
|
||||||
<FreePlanBannerContent />
|
|
||||||
</AutumnProvider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function FreePlanBannerContent() {
|
|
||||||
const { data: session } = useSession();
|
const { data: session } = useSession();
|
||||||
const customerQuery = useCustomer({
|
const customerQuery = useCustomer({
|
||||||
queryOptions: {
|
queryOptions: {
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import type { ReactNode } from "react";
|
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 { useSession } from "@/lib/auth-client";
|
||||||
import { isHostedClientAuthMode } from "@/lib/auth-mode";
|
import { isHostedClientAuthMode } from "@/lib/auth-mode";
|
||||||
import { getCustomerPlanStatus } from "@/client/features/billing/plan-detection";
|
import { getCustomerPlanStatus } from "@/client/features/billing/plan-detection";
|
||||||
@ -23,11 +23,7 @@ export function HostedPlanGate({
|
|||||||
return children(SELF_HOSTED_PLAN_GATE);
|
return children(SELF_HOSTED_PLAN_GATE);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return <HostedPlanGateContent>{children}</HostedPlanGateContent>;
|
||||||
<AutumnProvider>
|
|
||||||
<HostedPlanGateContent>{children}</HostedPlanGateContent>
|
|
||||||
</AutumnProvider>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function HostedPlanGateContent({
|
function HostedPlanGateContent({
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||||
import { AutumnProvider } from "autumn-js/react";
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Loader2 } from "lucide-react";
|
import { Loader2 } from "lucide-react";
|
||||||
import { DEFAULT_LOCATION_CODE } from "@/shared/keyword-locations";
|
import { DEFAULT_LOCATION_CODE } from "@/shared/keyword-locations";
|
||||||
@ -57,9 +56,7 @@ export function OnboardingChat() {
|
|||||||
{!domain ? (
|
{!domain ? (
|
||||||
<SiteForm projectId={projectId} />
|
<SiteForm projectId={projectId} />
|
||||||
) : (
|
) : (
|
||||||
<AutumnProvider>
|
<OnboardingChatConversation projectId={projectId} domain={domain} />
|
||||||
<OnboardingChatConversation projectId={projectId} domain={domain} />
|
|
||||||
</AutumnProvider>
|
|
||||||
)}
|
)}
|
||||||
</StrategyShell>
|
</StrategyShell>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { useMemo, useState } from "react";
|
import { useMemo, useState } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import { AutumnProvider, useCustomer } from "autumn-js/react";
|
import { useCustomer } from "autumn-js/react";
|
||||||
import {
|
import {
|
||||||
getLatestRankResults,
|
getLatestRankResults,
|
||||||
getRankPositionMatrix,
|
getRankPositionMatrix,
|
||||||
@ -57,20 +57,7 @@ function deviceVisibility(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function RankTrackingDomainDetail(props: {
|
export function RankTrackingDomainDetail({
|
||||||
config: RankTrackingConfig;
|
|
||||||
projectId: string;
|
|
||||||
onBack: () => void;
|
|
||||||
onEdit: () => void;
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<AutumnProvider>
|
|
||||||
<RankTrackingDomainDetailInner {...props} />
|
|
||||||
</AutumnProvider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function RankTrackingDomainDetailInner({
|
|
||||||
config,
|
config,
|
||||||
projectId,
|
projectId,
|
||||||
onBack,
|
onBack,
|
||||||
|
|||||||
@ -20,4 +20,17 @@ export const baseAuthOptions = {
|
|||||||
user: {
|
user: {
|
||||||
additionalFields: userAdditionalFields,
|
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 { TanStackRouterDevtoolsPanel } from "@tanstack/react-router-devtools";
|
||||||
import { TanStackDevtools } from "@tanstack/react-devtools";
|
import { TanStackDevtools } from "@tanstack/react-devtools";
|
||||||
import { QueryClientProvider } from "@tanstack/react-query";
|
import { QueryClientProvider } from "@tanstack/react-query";
|
||||||
|
import { AutumnProvider } from "autumn-js/react";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { DefaultCatchBoundary } from "@/client/components/DefaultCatchBoundary";
|
import { DefaultCatchBoundary } from "@/client/components/DefaultCatchBoundary";
|
||||||
import { ExportToSheetsModal } from "@/client/components/table/ExportToSheetsModal";
|
import { ExportToSheetsModal } from "@/client/components/table/ExportToSheetsModal";
|
||||||
@ -173,27 +174,37 @@ function RootDocument({ children }: { children: React.ReactNode }) {
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<ClientOnly>
|
<ClientOnly>
|
||||||
<QueryClientProvider client={queryClient}>
|
{/* Keep this the ONLY AutumnProvider. Each provider creates its own
|
||||||
<>
|
customer cache (autumn-js bundles a private react-query, so it
|
||||||
<PostHogBootstrap />
|
never shares the app QueryClient), and every extra provider mount
|
||||||
{children}
|
pays its own ~1s getOrCreateCustomer round trip. It only provides
|
||||||
<ExportToSheetsModal />
|
context — nothing fetches until a useCustomer consumer mounts. */}
|
||||||
<Toaster position="bottom-right" mobileOffset={{ bottom: 100 }} />
|
<AutumnProvider>
|
||||||
{showDevtools ? (
|
<QueryClientProvider client={queryClient}>
|
||||||
<TanStackDevtools
|
<>
|
||||||
config={{ position: "bottom-right" }}
|
<PostHogBootstrap />
|
||||||
eventBusConfig={{ connectToServerBus: true }}
|
{children}
|
||||||
plugins={[
|
<ExportToSheetsModal />
|
||||||
{
|
<Toaster
|
||||||
name: "TanStack Router",
|
position="bottom-right"
|
||||||
render: <TanStackRouterDevtoolsPanel />,
|
mobileOffset={{ bottom: 100 }}
|
||||||
defaultOpen: true,
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
/>
|
/>
|
||||||
) : null}
|
{showDevtools ? (
|
||||||
</>
|
<TanStackDevtools
|
||||||
</QueryClientProvider>
|
config={{ position: "bottom-right" }}
|
||||||
|
eventBusConfig={{ connectToServerBus: true }}
|
||||||
|
plugins={[
|
||||||
|
{
|
||||||
|
name: "TanStack Router",
|
||||||
|
render: <TanStackRouterDevtoolsPanel />,
|
||||||
|
defaultOpen: true,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</>
|
||||||
|
</QueryClientProvider>
|
||||||
|
</AutumnProvider>
|
||||||
</ClientOnly>
|
</ClientOnly>
|
||||||
<Scripts />
|
<Scripts />
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { createFileRoute, notFound } from "@tanstack/react-router";
|
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 { useEffect, useState } from "react";
|
||||||
import { useSession } from "@/lib/auth-client";
|
import { useSession } from "@/lib/auth-client";
|
||||||
import { isHostedClientAuthMode } from "@/lib/auth-mode";
|
import { isHostedClientAuthMode } from "@/lib/auth-mode";
|
||||||
@ -31,14 +31,6 @@ export const Route = createFileRoute("/_app/billing")({
|
|||||||
});
|
});
|
||||||
|
|
||||||
function BillingPage() {
|
function BillingPage() {
|
||||||
return (
|
|
||||||
<AutumnProvider>
|
|
||||||
<BillingPageContent />
|
|
||||||
</AutumnProvider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function BillingPageContent() {
|
|
||||||
const { data: session, isPending: isSessionPending } = useSession();
|
const { data: session, isPending: isSessionPending } = useSession();
|
||||||
const [topUpAmount, setTopUpAmount] = useState("20");
|
const [topUpAmount, setTopUpAmount] = useState("20");
|
||||||
const [isPending, setIsPending] = useState(false);
|
const [isPending, setIsPending] = useState(false);
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { Link, createFileRoute, useNavigate } from "@tanstack/react-router";
|
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 { useEffect, useState } from "react";
|
||||||
import { ArrowRight, Settings, User } from "lucide-react";
|
import { ArrowRight, Settings, User } from "lucide-react";
|
||||||
import { ThemePreferenceMenuItems } from "@/client/components/ThemePreferenceMenuItems";
|
import { ThemePreferenceMenuItems } from "@/client/components/ThemePreferenceMenuItems";
|
||||||
@ -41,14 +41,6 @@ export const Route = createFileRoute("/_authenticated/subscribe")({
|
|||||||
});
|
});
|
||||||
|
|
||||||
function SubscribePage() {
|
function SubscribePage() {
|
||||||
return (
|
|
||||||
<AutumnProvider>
|
|
||||||
<SubscribePageContent />
|
|
||||||
</AutumnProvider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function SubscribePageContent() {
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { upgrade: isUpgradeFlow, redirect } = Route.useSearch();
|
const { upgrade: isUpgradeFlow, redirect } = Route.useSearch();
|
||||||
const { data: session } = useSession();
|
const { data: session } = useSession();
|
||||||
|
|||||||
@ -269,8 +269,10 @@ async function refreshKeywordMetrics(
|
|||||||
projectId: string,
|
projectId: string,
|
||||||
billingCustomer: BillingCustomerContext,
|
billingCustomer: BillingCustomerContext,
|
||||||
): Promise<{ updated: number }> {
|
): Promise<{ updated: number }> {
|
||||||
const config = await getValidatedConfig(configId, projectId);
|
const [config, keywords] = await Promise.all([
|
||||||
const keywords = await RankTrackingRepository.getKeywordsForConfig(configId);
|
getValidatedConfig(configId, projectId),
|
||||||
|
RankTrackingRepository.getKeywordsForConfig(configId),
|
||||||
|
]);
|
||||||
if (keywords.length === 0) return { updated: 0 };
|
if (keywords.length === 0) return { updated: 0 };
|
||||||
|
|
||||||
const client = createDataforseoClient(billingCustomer);
|
const client = createDataforseoClient(billingCustomer);
|
||||||
|
|||||||
@ -26,29 +26,28 @@ export async function getLatestResults(
|
|||||||
rows: RankTrackingRow[];
|
rows: RankTrackingRow[];
|
||||||
run: { id: string; lastCheckedAt: string } | null;
|
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 days = PERIOD_DAYS[comparePeriod];
|
||||||
const targetDate = toSqliteTimestamp(
|
const targetDate = toSqliteTimestamp(
|
||||||
new Date(Date.now() - days * 24 * 60 * 60 * 1000),
|
new Date(Date.now() - days * 24 * 60 * 60 * 1000),
|
||||||
);
|
);
|
||||||
|
|
||||||
const comparisonSnapshots =
|
// All four reads key off the inputs alone, so run them in one parallel round
|
||||||
await RankTrackingRepository.getSnapshotsBeforeDate(configId, targetDate);
|
// 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>();
|
const previousPositions = new Map<string, number | null>();
|
||||||
for (const snap of comparisonSnapshots) {
|
for (const snap of comparisonSnapshots) {
|
||||||
|
|||||||
@ -6,6 +6,18 @@
|
|||||||
"compatibility_flags": ["nodejs_compat", "global_fetch_strictly_public"],
|
"compatibility_flags": ["nodejs_compat", "global_fetch_strictly_public"],
|
||||||
"observability": {
|
"observability": {
|
||||||
"enabled": true,
|
"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
|
// Uncomment when deploying to the openseo.so Cloudflare account. Self-hosters
|
||||||
// should leave this commented (or replace with their own zone) — otherwise
|
// should leave this commented (or replace with their own zone) — otherwise
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user