diff --git a/app/(home)/page.tsx b/app/(home)/page.tsx index 2ab3c79a07..64c0f1a8ee 100644 --- a/app/(home)/page.tsx +++ b/app/(home)/page.tsx @@ -1,5 +1,20 @@ +import { cookies, headers } from "next/headers"; import { Home } from "@/components/home"; +import { + HERO_SLOGAN_VARIANT_HEADER, + HERO_SLOGAN_VARIANT_KEY, + getHeroSloganVariantFromCookieValue, +} from "@/lib/hero-slogan-variant"; -export default function HomePage() { - return ; +export default async function HomePage() { + const [cookieStore, headerStore] = await Promise.all([ + cookies(), + headers(), + ]); + const heroSloganVariant = getHeroSloganVariantFromCookieValue( + headerStore.get(HERO_SLOGAN_VARIANT_HEADER) ?? + cookieStore.get(HERO_SLOGAN_VARIANT_KEY)?.value, + ); + + return ; } diff --git a/components/home/Hero.tsx b/components/home/Hero.tsx index 224ab578bd..b3c82a5257 100644 --- a/components/home/Hero.tsx +++ b/components/home/Hero.tsx @@ -1,60 +1,24 @@ import { Button } from "@/components/ui/button"; import { CornerBox } from "@/components/ui/corner-box"; -import { Heading } from "@/components/ui/heading"; import { Text } from "@/components/ui/text"; -import { TextHighlight } from "@/components/ui"; import { HomeSection } from "@/components/home/HomeSection"; import { EnterpriseLogoGrid } from "@/components/shared/EnterpriseLogoGrid"; -import { cn } from "@/lib/utils"; import { HeroStatsStrip } from "@/components/home/HeroStatsStrip"; +import { HeroSlogan } from "@/components/home/HeroSlogan"; +import type { HeroSloganVariant } from "@/lib/hero-slogan-variant"; -export function Hero() { +export function Hero({ + heroSloganVariant, +}: { + heroSloganVariant: HeroSloganVariant; +}) { return ( - - - Open Source  - - - - AI - - - Engineering - - - - Platform - - - +
Trace and evaluate AI Agents. Collaborate with your team to diff --git a/components/home/HeroSlogan.tsx b/components/home/HeroSlogan.tsx new file mode 100644 index 0000000000..f0e7459df6 --- /dev/null +++ b/components/home/HeroSlogan.tsx @@ -0,0 +1,167 @@ +"use client"; + +import { useEffect, useRef } from "react"; +import { usePostHog } from "posthog-js/react"; +import { Heading } from "@/components/ui/heading"; +import { TextHighlight } from "@/components/ui"; +import { cn } from "@/lib/utils"; +import { usePostHogClientCapture } from "@/src/usePostHogClientCapture"; +import { + HERO_SLOGAN_VARIANT_KEY, + type HeroSloganVariant, +} from "@/lib/hero-slogan-variant"; + +function readStoredVariant(): HeroSloganVariant | null { + try { + const stored = localStorage.getItem(HERO_SLOGAN_VARIANT_KEY); + if (stored === "ai-engineering" || stored === "agent-evals") { + return stored; + } + } catch { + // localStorage unavailable (e.g. private browsing restrictions) + } + return null; +} + +function persistVariant(variant: HeroSloganVariant) { + try { + localStorage.setItem(HERO_SLOGAN_VARIANT_KEY, variant); + } catch { + // ignore write failures + } +} + +const headingClassName = cn( + "flex-col items-center gap-0.5 sm:gap-1 md:gap-1.5 text-center font-medium leading-[105%] max-md:max-w-[500px]", + "[leading-trim:both] [text-edge:cap]", +); + +const desktopWordSpacing = "max-[499px]:pr-1.75 min-[500px]:pr-2"; + +function AiEngineeringSlogan() { + return ( + <> + + Open Source  + + + + AI + + + Engineering + + + + Platform + + + ); +} + +function AgentEvalsSlogan() { + return ( + <> + + Open Source + + + + Agent + + + Evals + + + and + + + Tracing + + + + ); +} + +function trackHeroSloganExposure( + posthog: ReturnType, + capture: ReturnType, + variant: HeroSloganVariant, + isNewAssignment: boolean, +) { + posthog.register({ hero_slogan_variant: variant }); + capture("hero_slogan_exposure", { + variant, + is_new_assignment: isNewAssignment, + }); +} + +export function HeroSlogan({ + initialVariant, +}: { + initialVariant: HeroSloganVariant; +}) { + const posthog = usePostHog(); + const capture = usePostHogClientCapture(); + const trackedRef = useRef(false); + + useEffect(() => { + const hadLocalStorage = readStoredVariant() !== null; + persistVariant(initialVariant); + + if (trackedRef.current) { + return; + } + + const runTracking = () => { + if (trackedRef.current) { + return; + } + trackedRef.current = true; + trackHeroSloganExposure( + posthog, + capture, + initialVariant, + !hadLocalStorage, + ); + }; + + if ((posthog as { __loaded?: boolean }).__loaded) { + runTracking(); + return; + } + + posthog.onSessionId(runTracking); + }, [capture, initialVariant, posthog]); + + return ( + + {initialVariant === "ai-engineering" ? ( + + ) : ( + + )} + + ); +} diff --git a/components/home/index.tsx b/components/home/index.tsx index 19572ac1cd..2c0750d410 100644 --- a/components/home/index.tsx +++ b/components/home/index.tsx @@ -9,11 +9,16 @@ import { Enterprise } from "./Enterprise"; import { WhyLangfuse } from "./WhyLangfuse"; import { GetStartedSection } from "./GetStartedSection"; import { FAQ } from "./FAQ"; +import type { HeroSloganVariant } from "@/lib/hero-slogan-variant"; -export const Home = () => ( +export const Home = ({ + heroSloganVariant, +}: { + heroSloganVariant: HeroSloganVariant; +}) => ( <>
- + diff --git a/lib/hero-slogan-variant.ts b/lib/hero-slogan-variant.ts new file mode 100644 index 0000000000..36859c1c4d --- /dev/null +++ b/lib/hero-slogan-variant.ts @@ -0,0 +1,22 @@ +export const HERO_SLOGAN_VARIANT_HEADER = "x-hero-slogan-variant"; + +export const HERO_SLOGAN_VARIANT_KEY = "langfuse-hero-slogan-variant"; + +export type HeroSloganVariant = "ai-engineering" | "agent-evals"; + +export function isHeroSloganVariant(value: string): value is HeroSloganVariant { + return value === "ai-engineering" || value === "agent-evals"; +} + +export function pickHeroSloganVariant(): HeroSloganVariant { + return Math.random() < 0.5 ? "ai-engineering" : "agent-evals"; +} + +export function getHeroSloganVariantFromCookieValue( + value: string | undefined, +): HeroSloganVariant { + if (value && isHeroSloganVariant(value)) { + return value; + } + return pickHeroSloganVariant(); +} diff --git a/middleware.ts b/middleware.ts new file mode 100644 index 0000000000..7e82141b6c --- /dev/null +++ b/middleware.ts @@ -0,0 +1,38 @@ +import { NextResponse } from "next/server"; +import type { NextRequest } from "next/server"; +import { + HERO_SLOGAN_VARIANT_KEY, + isHeroSloganVariant, + pickHeroSloganVariant, +} from "@/lib/hero-slogan-variant"; + +const HERO_SLOGAN_VARIANT_HEADER = "x-hero-slogan-variant"; + +export function middleware(request: NextRequest) { + const existing = request.cookies.get(HERO_SLOGAN_VARIANT_KEY)?.value; + const variant = + existing && isHeroSloganVariant(existing) + ? existing + : pickHeroSloganVariant(); + + const requestHeaders = new Headers(request.headers); + requestHeaders.set(HERO_SLOGAN_VARIANT_HEADER, variant); + + const response = NextResponse.next({ + request: { headers: requestHeaders }, + }); + + if (!existing || !isHeroSloganVariant(existing)) { + response.cookies.set(HERO_SLOGAN_VARIANT_KEY, variant, { + maxAge: 60 * 60 * 24 * 365, + path: "/", + sameSite: "lax", + }); + } + + return response; +} + +export const config = { + matcher: "/", +}; diff --git a/src/usePostHogClientCapture.ts b/src/usePostHogClientCapture.ts index ae19e493f8..f91b8134cf 100644 --- a/src/usePostHogClientCapture.ts +++ b/src/usePostHogClientCapture.ts @@ -5,6 +5,10 @@ import { usePostHog } from "posthog-js/react"; // This preserves existing PostHog event structure while adding type safety interface EventDefinitions { copy_page: { type: "copy" | "chatgpt" | "claude" | "mcp" }; + hero_slogan_exposure: { + variant: "ai-engineering" | "agent-evals"; + is_new_assignment: boolean; + }; } type EventName = keyof EventDefinitions;