Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions claim-db-worker/app/api/analytics/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
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 });
}

const POSTHOG_API_KEY = env.POSTHOG_API_KEY;
const POSTHOG_PROXY_HOST = env.POSTHOG_API_HOST;

if (!POSTHOG_API_KEY || !POSTHOG_PROXY_HOST) {
return NextResponse.json({ success: true });
}

try {
const {
event,
properties,
}: { event: string; properties: Record<string, any> } =
await request.json();

if (!event) {
return NextResponse.json(
{ error: "Event name required" },
{ status: 400 }
);
}

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: 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 });
}
}
72 changes: 65 additions & 7 deletions claim-db-worker/app/api/auth/callback/route.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,48 @@
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,
getBaseUrl,
} from "@/lib/response-utils";
import { transferProject } from "@/lib/project-transfer";

async function sendServerAnalyticsEvent(
event: string,
properties: Record<string, any>,
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();
Expand Down Expand Up @@ -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",
Expand All @@ -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",
Expand All @@ -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,
Expand Down
27 changes: 2 additions & 25 deletions claim-db-worker/app/api/claim/route.ts
Original file line number Diff line number Diff line change
@@ -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();
Expand All @@ -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<string, any>
) {
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 });
Expand Down
2 changes: 2 additions & 0 deletions claim-db-worker/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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"],
Expand Down Expand Up @@ -47,6 +48,7 @@ export default function RootLayout({
<body
className={`${barlow.className} antialiased bg-custom-gradient backdrop-filter backdrop-blur-md bg-cover font-barlow text-white p-0 min-h-screen w-full`}
>
<PageViewTracker />
<Toaster toastOptions={{ duration: 4000 }} />
<DropProvider>
<div className="flex items-center justify-center flex-col min-h-screen max-w-screen-xl mx-auto w-full">
Expand Down
38 changes: 38 additions & 0 deletions claim-db-worker/components/PageViewTracker.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<Suspense fallback={null}>
<PageViewTrackerContent />
</Suspense>
);
}
70 changes: 13 additions & 57 deletions claim-db-worker/lib/analytics.ts
Original file line number Diff line number Diff line change
@@ -1,62 +1,18 @@
import { getEnv } from "./env";
"use client";

export async function sendPosthogEvent(
export const sendAnalyticsEvent = async (
event: string,
properties: Record<string, any>
) {
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);
}
};