diff --git a/apps/api/src/routes/dev-plans.ts b/apps/api/src/routes/dev-plans.ts index 9bd65d0f72..5dddc3787f 100644 --- a/apps/api/src/routes/dev-plans.ts +++ b/apps/api/src/routes/dev-plans.ts @@ -7,6 +7,7 @@ import { executeSelfRefund, isSelfRefundCandidateType, } from "@/lib/self-refund.js"; +import { posthog } from "@/posthog.js"; import { ensureStripeCustomer, finalizeDevPlanSetupSession, @@ -3216,6 +3217,17 @@ devPlans.openapi(redeemResetPass, async (c) => { }, }); + posthog.capture({ + distinctId: user.id, + event: "reset_pass_redeemed", + groups: { organization: personalOrg.id }, + properties: { + devPlan: tier, + source, + organization: personalOrg.id, + }, + }); + return c.json({ success: true, source, diff --git a/apps/code/src/app/dashboard/components/UsageOverview.tsx b/apps/code/src/app/dashboard/components/UsageOverview.tsx index 5110bbd84c..c68029f84d 100644 --- a/apps/code/src/app/dashboard/components/UsageOverview.tsx +++ b/apps/code/src/app/dashboard/components/UsageOverview.tsx @@ -2,7 +2,10 @@ import { format, formatDistanceToNowStrict } from "date-fns"; import { Activity, Coins, Cpu, Gem, TrendingUp } from "lucide-react"; +import { usePostHog } from "posthog-js/react"; +import { useEffect } from "react"; +import { useAppConfig } from "@/lib/config"; import { useApi } from "@/lib/fetch-client"; import { AgentModelUsageChart } from "./AgentModelUsageChart"; @@ -219,6 +222,8 @@ export default function UsageOverview({ cycle = "monthly", }: UsageOverviewProps) { const api = useApi(); + const posthog = usePostHog(); + const { posthogKey } = useAppConfig(); const tierKey = planName.toLowerCase(); // The monthly credit pool (not the weekly premium allowance) is the hard @@ -226,6 +231,28 @@ export default function UsageOverview({ // card gives way to the upgrade/PAYG promo. const monthlyExhausted = creditsLimit > 0 && creditsUsed >= creditsLimit; + // Top of the Reset Pass upsell funnel: the user sees the exhausted weekly + // premium meter. reset_pass_purchased/redeemed are captured server-side, + // so without this event the funnel has a bottom but no top. + const weeklyExhausted = + premiumWeeklyLimit > 0 && premiumCreditsUsed >= premiumWeeklyLimit; + useEffect(() => { + if (!posthogKey || !weeklyExhausted) { + return; + } + posthog.capture("devpass_weekly_cap_hit_viewed", { + tier: tierKey, + weeklyLimit: premiumWeeklyLimit, + weeklyUsed: premiumCreditsUsed, + resetsAt: premiumWeekResetsAt, + purchasedPasses: resetPasses, + includedPassesRemaining: includedResetPassesRemaining, + monthlyExhausted, + }); + // Fire once per exhausted dashboard view, not on every prop tick. + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [posthogKey, weeklyExhausted]); + const { data: activity } = api.useQuery( "get", "/activity", diff --git a/apps/playground/next.config.ts b/apps/playground/next.config.ts index 6805f7c64f..37a3ebcf1d 100644 --- a/apps/playground/next.config.ts +++ b/apps/playground/next.config.ts @@ -26,6 +26,21 @@ const nextConfig: NextConfig = { "@json-render/react-pdf", "@json-render/image", ], + async rewrites() { + return [ + // First-party PostHog ingestion proxy — ad blockers block + // *.posthog.com directly, silently dropping client events. The + // client is configured with api_host: "/ingest" (providers.tsx). + { + source: "/ingest/static/:path*", + destination: "https://us-assets.i.posthog.com/static/:path*", + }, + { + source: "/ingest/:path*", + destination: "https://us.i.posthog.com/:path*", + }, + ]; + }, typescript: { ignoreBuildErrors: true, }, diff --git a/apps/playground/src/components/providers.tsx b/apps/playground/src/components/providers.tsx index e7e17231e6..f769bd8634 100644 --- a/apps/playground/src/components/providers.tsx +++ b/apps/playground/src/components/providers.tsx @@ -41,16 +41,23 @@ export function Providers({ children, config }: ProvidersProps) { const host = config.posthogHost; const init = () => { posthog.init(key, { - api_host: host, + // Ingest through our own origin (see the /ingest rewrites in + // next.config.ts) so ad blockers that block *.posthog.com don't + // silently drop client events. + api_host: "/ingest", + ui_host: host, capture_pageview: "history_change", autocapture: true, }); }; + // Captures fired before init() are dropped by posthog-js, so the idle + // deferral must be bounded — a busy main thread (e.g. the chat page) + // can starve requestIdleCallback long enough for a user to act. if (typeof requestIdleCallback !== "undefined") { - const id = requestIdleCallback(init); + const id = requestIdleCallback(init, { timeout: 800 }); return () => cancelIdleCallback(id); } - const timer = setTimeout(init, 1000); + const timer = setTimeout(init, 300); return () => clearTimeout(timer); }, [config.posthogKey, config.posthogHost]); diff --git a/apps/ui/next.config.ts b/apps/ui/next.config.ts index b2f4fd7a9a..301d4a73f4 100644 --- a/apps/ui/next.config.ts +++ b/apps/ui/next.config.ts @@ -422,6 +422,17 @@ const nextConfig: NextConfig = { source: "/docs-health", destination: "https://docs.llmgateway.io/health", }, + // First-party PostHog ingestion proxy — ad blockers block + // *.posthog.com directly, silently dropping client events. The + // client is configured with api_host: "/ingest" (providers.tsx). + { + source: "/ingest/static/:path*", + destination: "https://us-assets.i.posthog.com/static/:path*", + }, + { + source: "/ingest/:path*", + destination: "https://us.i.posthog.com/:path*", + }, ]; }, typescript: { diff --git a/apps/ui/src/components/blog/blog-cta.tsx b/apps/ui/src/components/blog/blog-cta.tsx new file mode 100644 index 0000000000..973edc95a1 --- /dev/null +++ b/apps/ui/src/components/blog/blog-cta.tsx @@ -0,0 +1,112 @@ +"use client"; + +import Link from "next/link"; +import { usePathname } from "next/navigation"; +import { usePostHog } from "posthog-js/react"; + +import { Button } from "@/lib/components/button"; + +// Conversion card embeddable in markdown content via the `BlogCta` override in +// getMarkdownOptions. The DevPass variant leans into the passport/boarding-pass +// brand language used across DevPass surfaces. +export function BlogCta({ + variant = "devpass", + location = "inline", +}: { + variant?: "devpass" | "gateway"; + location?: string; +}) { + const posthog = usePostHog(); + const pathname = usePathname(); + const post = pathname?.split("/").pop() ?? "unknown"; + + const track = (cta: string) => { + posthog.capture("cta_clicked", { + location: `blog_${location}`, + cta, + post, + }); + }; + + if (variant === "gateway") { + return ( +
+
+ LLM Gateway +
+

+ One API key for every model. +

+

+ Route to 200+ models with automatic failover, caching, and real-time + cost analytics. Free to start — no credit card required. +

+
+ + track("browse_models")} + className="text-sm text-muted-foreground underline underline-offset-4 transition-colors hover:text-foreground" + > + Browse all models + +
+
+ ); + } + + return ( +
+
+
+
+ DevPass · Boarding pass +
+
+ No. DP-{post.slice(0, 6).toUpperCase()} +
+
+

+ Every frontier model. One flat rate. +

+

+ Run Claude, GPT-5.5, Gemini, and 200+ more through Claude Code, + Cursor, or whatever tool you already use. From $29/mo — no token math, + no surprise invoices. +

+
+ + track("compare_devpass_plans")} + className="text-sm text-muted-foreground underline underline-offset-4 transition-colors hover:text-foreground" + > + Compare plans + +
+
+
+ ); +} diff --git a/apps/ui/src/components/landing/navbar.tsx b/apps/ui/src/components/landing/navbar.tsx index 03c45d25df..868a68bb5c 100644 --- a/apps/ui/src/components/landing/navbar.tsx +++ b/apps/ui/src/components/landing/navbar.tsx @@ -30,6 +30,7 @@ import { Zap, } from "lucide-react"; import Link from "next/link"; +import { usePostHog } from "posthog-js/react"; import { useEffect, useState } from "react"; import { AuthLink } from "@/components/shared/auth-link"; @@ -70,8 +71,12 @@ function IconMenuItem({ gradient: string; external?: boolean; }) { + const posthog = usePostHog(); const linkClassName = cn( - "group/product flex items-start gap-3 select-none rounded-lg p-3 no-underline outline-none transition-all duration-300 bg-linear-to-br from-transparent to-transparent", + // flex-row is load-bearing: NavigationMenuLink's base classes include + // flex-col and are concatenated onto this link via Radix Slot, so the + // direction must be asserted explicitly or the card stacks vertically. + "group/product flex flex-row items-start gap-3 select-none rounded-lg p-3 no-underline outline-none transition-all duration-300 bg-linear-to-br from-transparent to-transparent", gradient, "hover:shadow-lg focus:shadow-md", ); @@ -96,6 +101,10 @@ function IconMenuItem({ ); + const handleClick = () => { + posthog.capture("nav_link_clicked", { link: title, area: "dropdown" }); + }; + return (
  • @@ -105,11 +114,17 @@ function IconMenuItem({ target="_blank" rel="noopener noreferrer" className={linkClassName} + onClick={handleClick} > {inner} ) : ( - + {inner} )} @@ -130,9 +145,14 @@ export const Navbar = ({ providers?: ApiProvider[]; }) => { const config = useAppConfig(); + const posthog = usePostHog(); const { isAuthenticated: hasSession, isLoading } = useSessionStatus(); const isAuthenticated = hasSession && !isLoading; + const trackNav = (link: string) => { + posthog.capture("nav_link_clicked", { link, area: "navbar" }); + }; + const productsLinks: Array<{ title: string; href: string; @@ -421,7 +441,7 @@ export const Navbar = ({ prefetch={true} > - + LLM Gateway @@ -437,18 +457,58 @@ export const Navbar = ({ {/* Desktop center nav */} -
    -
    +
    +
    - + + {/* Most-clicked destinations surfaced as direct links — + DevPass (top product) and Models (top resource) per + PostHog page traffic; Chat joins on wider screens. */} + + + trackNav("DevPass")} + className="text-muted-foreground hover:text-accent-foreground block duration-150 px-3 py-2 whitespace-nowrap" + > + DevPass + + + + + + + trackNav("Chat")} + className="text-muted-foreground hover:text-accent-foreground block duration-150 px-3 py-2 whitespace-nowrap" + > + Chat + + + + + + + trackNav("Models")} + className="text-muted-foreground hover:text-accent-foreground block duration-150 px-3 py-2 whitespace-nowrap" + > + Models + + + + {/* Products dropdown */} - + Products - +