diff --git a/claim-db-worker/app/api/analytics/route.ts b/claim-db-worker/app/api/analytics/route.ts new file mode 100644 index 0000000..fde1a48 --- /dev/null +++ b/claim-db-worker/app/api/analytics/route.ts @@ -0,0 +1,51 @@ +import { NextRequest, NextResponse } from "next/server"; +import { getEnv } from "@/lib/env"; + +export async function POST(request: NextRequest) { + const env = getEnv(); + + const rateLimitResult = await env.CLAIM_DB_RATE_LIMITER.limit({ + key: request.url, + }); + if (!rateLimitResult.success) { + return NextResponse.json({ error: "Rate limit exceeded" }, { status: 429 }); + } + + if (!env.POSTHOG_API_KEY || !env.POSTHOG_API_HOST) { + return NextResponse.json({ success: true }); + } + + try { + const { + event, + properties, + }: { event: string; properties: Record } = + await request.json(); + + if (!event) { + return NextResponse.json( + { error: "Event name required" }, + { status: 400 } + ); + } + + await fetch(`${env.POSTHOG_API_HOST}/e`, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${env.POSTHOG_API_KEY}`, + }, + body: JSON.stringify({ + api_key: env.POSTHOG_API_KEY, + event, + properties: properties || {}, + distinct_id: "web-claim", + }), + }); + + return NextResponse.json({ success: true }); + } catch (error) { + console.error("Failed to send PostHog event:", error); + return NextResponse.json({ error: "Analytics failed" }, { status: 500 }); + } +} diff --git a/claim-db-worker/app/api/auth/callback/route.ts b/claim-db-worker/app/api/auth/callback/route.ts index 574b34d..d0f1d2b 100644 --- a/claim-db-worker/app/api/auth/callback/route.ts +++ b/claim-db-worker/app/api/auth/callback/route.ts @@ -1,7 +1,6 @@ import { NextRequest } from "next/server"; import { getEnv } from "@/lib/env"; import { exchangeCodeForToken, validateProject } from "@/lib/auth-utils"; -import { trackClaimSuccess, trackClaimFailure } from "@/lib/analytics"; import { redirectToError, redirectToSuccess, @@ -9,6 +8,41 @@ import { } from "@/lib/response-utils"; import { transferProject } from "@/lib/project-transfer"; +async function sendServerAnalyticsEvent( + event: string, + properties: Record, + request: NextRequest +) { + const env = getEnv(); + + if (!env.POSTHOG_API_KEY || !env.POSTHOG_API_HOST) { + return; + } + + try { + await fetch(`${env.POSTHOG_API_HOST}/e`, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${env.POSTHOG_API_KEY}`, + }, + body: JSON.stringify({ + api_key: env.POSTHOG_API_KEY, + event, + properties: { + ...properties, + $current_url: request.url, + $user_agent: request.headers.get("user-agent"), + }, + distinct_id: "server-claim", + timestamp: new Date().toISOString(), + }), + }); + } catch (error) { + console.error("Failed to send server analytics event:", error); + } +} + export async function GET(request: NextRequest) { try { const env = getEnv(); @@ -59,7 +93,14 @@ export async function GET(request: NextRequest) { } catch (error) { const errorMessage = error instanceof Error ? error.message : "Unknown error"; - await trackClaimFailure(projectID, 0, errorMessage); + await sendServerAnalyticsEvent( + "create_db:claim_failed", + { + "project-id": projectID, + error: errorMessage, + }, + request + ); return redirectToError( request, "Authentication Failed", @@ -74,6 +115,14 @@ export async function GET(request: NextRequest) { } catch (error) { const errorMessage = error instanceof Error ? error.message : "Unknown error"; + await sendServerAnalyticsEvent( + "create_db:claim_failed", + { + "project-id": projectID, + error: errorMessage, + }, + request + ); return redirectToError( request, "Project Not Found", @@ -89,13 +138,22 @@ export async function GET(request: NextRequest) { ); if (transferResult.success) { - await trackClaimSuccess(projectID); + await sendServerAnalyticsEvent( + "create_db:claim_successful", + { + "project-id": projectID, + }, + request + ); return redirectToSuccess(request, projectID); } else { - await trackClaimFailure( - projectID, - transferResult.status!, - transferResult.error! + await sendServerAnalyticsEvent( + "create_db:claim_failed", + { + "project-id": projectID, + error: transferResult.error!, + }, + request ); return redirectToError( request, diff --git a/claim-db-worker/app/api/claim/route.ts b/claim-db-worker/app/api/claim/route.ts index d869cca..4dbdcb8 100644 --- a/claim-db-worker/app/api/claim/route.ts +++ b/claim-db-worker/app/api/claim/route.ts @@ -1,5 +1,6 @@ import { NextRequest, NextResponse } from "next/server"; import { getEnv } from "@/lib/env"; +import { sendAnalyticsEvent } from "@/lib/analytics"; export async function GET(request: NextRequest) { const env = getEnv(); @@ -12,38 +13,14 @@ export async function GET(request: NextRequest) { return NextResponse.json({ error: "Rate limit exceeded" }, { status: 429 }); } - async function sendPosthogEvent( - event: string, - properties: Record - ) { - const POSTHOG_API_KEY = env.POSTHOG_API_KEY; - const POSTHOG_PROXY_HOST = env.POSTHOG_API_HOST; - - await fetch(`${POSTHOG_PROXY_HOST}/e`, { - method: "POST", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${POSTHOG_API_KEY}`, - }, - body: JSON.stringify({ - api_key: POSTHOG_API_KEY, - event, - properties, - distinct_id: "web-claim", - }), - }); - } - const projectID = searchParams.get("projectID"); if (!projectID || projectID === "undefined") { return NextResponse.json({ error: "Missing project ID" }, { status: 400 }); } - await sendPosthogEvent("create_db:claim_page_viewed", { + await sendAnalyticsEvent("create_db:claim_viewed", { "project-id": projectID, - "utm-source": searchParams.get("utm_source") || "unknown", - "utm-medium": searchParams.get("utm_medium") || "unknown", }); return NextResponse.json({ success: true }); diff --git a/claim-db-worker/app/layout.tsx b/claim-db-worker/app/layout.tsx index e0aca9e..a56b53b 100644 --- a/claim-db-worker/app/layout.tsx +++ b/claim-db-worker/app/layout.tsx @@ -5,6 +5,7 @@ import { Navbar } from "../components/Navbar"; import { Footer } from "../components/Footer"; import { DropProvider } from "./contexts/DropContext"; import { Toaster } from "react-hot-toast"; +import { PageViewTracker } from "@/components/PageViewTracker"; const barlow = Barlow({ weight: ["400", "500", "700", "800", "900"], @@ -47,6 +48,7 @@ export default function RootLayout({ +
diff --git a/claim-db-worker/components/PageViewTracker.tsx b/claim-db-worker/components/PageViewTracker.tsx new file mode 100644 index 0000000..3294c37 --- /dev/null +++ b/claim-db-worker/components/PageViewTracker.tsx @@ -0,0 +1,38 @@ +"use client"; + +import { Suspense, useEffect } from "react"; +import { usePathname, useSearchParams } from "next/navigation"; +import { sendAnalyticsEvent } from "@/lib/analytics"; + +function PageViewTrackerContent() { + const pathname = usePathname(); + const searchParams = useSearchParams(); + + useEffect(() => { + if (typeof window === "undefined") return; + + if (pathname) { + const url = window.location.href; + const search = searchParams?.toString(); + const fullPath = search ? `${pathname}?${search}` : pathname; + + sendAnalyticsEvent("create_db:claim_page_viewed", { + path: pathname, + full_path: fullPath, + url: url, + referrer: document.referrer || "", + timestamp: new Date().toISOString(), + }); + } + }, [pathname, searchParams]); + + return null; +} + +export function PageViewTracker() { + return ( + + + + ); +} diff --git a/claim-db-worker/lib/analytics.ts b/claim-db-worker/lib/analytics.ts index ecd2aa4..438400c 100644 --- a/claim-db-worker/lib/analytics.ts +++ b/claim-db-worker/lib/analytics.ts @@ -1,62 +1,18 @@ -import { getEnv } from "./env"; +"use client"; -export async function sendPosthogEvent( +export const sendAnalyticsEvent = async ( event: string, properties: Record -) { - const env = getEnv(); - const POSTHOG_API_KEY = env.POSTHOG_API_KEY; - const POSTHOG_PROXY_HOST = env.POSTHOG_API_HOST; - - // Skip analytics if PostHog is not configured - if (!POSTHOG_API_KEY || !POSTHOG_PROXY_HOST) { - return; - } - - try { - await fetch(`${POSTHOG_PROXY_HOST}/e`, { - method: "POST", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${POSTHOG_API_KEY}`, - }, - body: JSON.stringify({ - api_key: POSTHOG_API_KEY, - event, - properties, - distinct_id: "web-claim", - }), - }); - } catch (error) { - console.error("Failed to send PostHog event:", error); - } -} - -export async function trackClaimSuccess(projectID: string) { - const env = getEnv(); - - try { - env.CREATE_DB_DATASET.writeDataPoint({ - blobs: ["database_claimed"], - indexes: ["claim_db"], - }); - } catch (error) { - console.error("Failed to write analytics data point:", error); - } - - await sendPosthogEvent("create_db:claim_successful", { - "project-id": projectID, +) => { + const response = await fetch(`${window.location.origin}/api/analytics`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ event, properties }), }); -} -export async function trackClaimFailure( - projectID: string, - status: number, - error: string -) { - await sendPosthogEvent("create_db:claim_failed", { - "project-id": projectID, - status, - error, - }); -} + if (!response.ok) { + console.error("Failed to send analytics event:", response); + } +};