Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
2 changes: 1 addition & 1 deletion claim-db-worker/__tests__/callback-api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ vi.mock("@/lib/env", () => ({
limit: vi.fn(() => Promise.resolve({ success: true })),
},
POSTHOG_API_KEY: "test-key",
POSTHOG_API_HOST: "https://app.posthog.com",
POSTHOG_PROXY_HOST: "https://proxyhog.prisma-data.net",
})),
}));

Expand Down
39 changes: 15 additions & 24 deletions claim-db-worker/app/api/analytics/route.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
import { NextRequest, NextResponse } from "next/server";
import { getEnv } from "@/lib/env";
import { sendAnalyticsEvent } from "@/lib/analytics";
import { buildRateLimitKey } from "@/lib/server/ratelimit";

export async function POST(request: NextRequest) {
const env = getEnv();

const url = new URL(request.url);
const key = buildRateLimitKey(request);

// --- Simple rate limiting ---
const { success } = await env.CLAIM_DB_RATE_LIMITER.limit({ key });
if (!success) {
return NextResponse.json(
Expand All @@ -21,16 +20,13 @@ export async function POST(request: NextRequest) {
);
}

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

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

const event = body.event?.trim();

if (!event) {
return NextResponse.json(
Expand All @@ -39,19 +35,14 @@ export async function POST(request: NextRequest) {
);
}

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",
}),
});
if (!event.startsWith("create_db:")) {
return NextResponse.json(
{ error: "Event must start with create_db:" },
{ status: 400 }
);
}

await sendAnalyticsEvent(event, body.properties || {});

return NextResponse.json({ success: true });
} catch (error) {
Expand Down
47 changes: 6 additions & 41 deletions claim-db-worker/app/api/auth/callback/route.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { NextRequest, NextResponse } from "next/server";
import { getEnv } from "@/lib/env";
import { exchangeCodeForToken, validateProject } from "@/lib/auth-utils";
import { sendAnalyticsEvent } from "@/lib/analytics";
import {
redirectToError,
redirectToSuccess,
Expand All @@ -9,41 +10,6 @@ import {
import { transferProject } from "@/lib/project-transfer";
import { buildRateLimitKey } from "@/lib/server/ratelimit";

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 @@ -98,7 +64,7 @@ export async function GET(request: NextRequest) {
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : "Unknown error";
await sendServerAnalyticsEvent(
await sendAnalyticsEvent(
"create_db:claim_failed",
{
"project-id": projectID,
Expand All @@ -115,13 +81,12 @@ export async function GET(request: NextRequest) {
}

// Validate project exists and get project data
let projectData;
try {
projectData = await validateProject(projectID);
await validateProject(projectID);
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : "Unknown error";
await sendServerAnalyticsEvent(
await sendAnalyticsEvent(
"create_db:claim_failed",
{
"project-id": projectID,
Expand Down Expand Up @@ -176,7 +141,7 @@ export async function GET(request: NextRequest) {
};
const databaseId = (databases.data?.[0]?.id ?? "").replace(/^db_/, "");

await sendServerAnalyticsEvent(
await sendAnalyticsEvent(
"create_db:claim_successful",
{
"project-id": projectID,
Expand All @@ -194,7 +159,7 @@ export async function GET(request: NextRequest) {
databaseId
);
} else {
await sendServerAnalyticsEvent(
await sendAnalyticsEvent(
"create_db:claim_failed",
{
"project-id": projectID,
Expand Down
14 changes: 11 additions & 3 deletions claim-db-worker/app/api/claim/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,22 @@ export async function GET(request: NextRequest) {
}

const projectID = url.searchParams.get("projectID");
const utmSource = url.searchParams.get("utm_source");
const utmMedium = url.searchParams.get("utm_medium");

if (!projectID || projectID === "undefined") {
return NextResponse.json({ error: "Missing project ID" }, { status: 400 });
}

await sendAnalyticsEvent("create_db:claim_viewed", {
"project-id": projectID,
});
await sendAnalyticsEvent(
"create_db:claim_viewed",
{
"project-id": projectID,
utm_source: utmSource || undefined,
utm_medium: utmMedium || undefined,
},
request
);

return NextResponse.json({ success: true });
}
28 changes: 12 additions & 16 deletions claim-db-worker/components/PageViewTracker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,29 +2,25 @@

import { Suspense, useEffect } from "react";
import { usePathname, useSearchParams } from "next/navigation";
import { sendAnalyticsEvent } from "@/lib/analytics";
import { sendAnalyticsEvent } from "@/lib/analytics-client";

function PageViewTrackerContent() {
const pathname = usePathname();
const searchParams = useSearchParams();
const search = searchParams?.toString() ?? "";

useEffect(() => {
if (typeof window === "undefined") return;
if (!pathname || 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]);
const fullPath = search ? `${pathname}?${search}` : pathname;
void sendAnalyticsEvent("create_db:claim_page_viewed", {
path: pathname,
full_path: fullPath,
url: window.location.href,
referrer: document.referrer || "",
timestamp: new Date().toISOString(),
});
}, [pathname, search]);

return null;
}
Expand Down
39 changes: 7 additions & 32 deletions claim-db-worker/components/PostHogProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,37 +1,12 @@
"use client";

import posthog, { PostHog } from "posthog-js";
import { PostHogProvider as PHProvider } from "posthog-js/react";

declare global {
interface Window {
posthog: PostHog;
}
}
import { PageViewTracker } from "@/components/PageViewTracker";

export function PostHogProvider({ children }: { children: React.ReactNode }) {
if (
typeof window !== "undefined" &&
process.env.NEXT_PUBLIC_POSTHOG_API_KEY
) {
posthog.init(process.env.NEXT_PUBLIC_POSTHOG_API_KEY || "", {
api_host:
process.env.NEXT_PUBLIC_POSTHOG_API_HOST ||
"https://proxyhog.prisma-data.net",
person_profiles: "identified_only",
capture_pageview: true,
capture_pageleave: true,
autocapture: true,
debug: process.env.NODE_ENV === "development", // Enable debug in development
loaded: (posthog) => {
if (process.env.NODE_ENV === "development") {
console.log("PostHog initialized in debug mode");
// Make posthog available globally for debugging
window.posthog = posthog;
}
},
});
}

return <PHProvider client={posthog}>{children}</PHProvider>;
return (
<>
<PageViewTracker />
{children}
</>
);
}
56 changes: 56 additions & 0 deletions claim-db-worker/lib/analytics-client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
"use client";

const DISTINCT_ID_STORAGE_KEY = "create_db:distinct_id";

function getDistinctId(): string {
if (typeof window === "undefined") {
return "claim-db-worker";
}

try {
const existing = window.localStorage.getItem(DISTINCT_ID_STORAGE_KEY);
if (existing) {
return existing;
}

const nextId = crypto.randomUUID();
window.localStorage.setItem(DISTINCT_ID_STORAGE_KEY, nextId);
return nextId;
} catch {
return "claim-db-worker";
}
}

export async function sendAnalyticsEvent(
event: string,
properties: Record<string, unknown> = {}
): Promise<void> {
if (!event.startsWith("create_db:")) {
return;
}

try {
const response = await fetch("/api/analytics", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
keepalive: true,
body: JSON.stringify({
event,
properties: {
...properties,
distinct_id: getDistinctId(),
},
}),
});

if (!response.ok && process.env.NODE_ENV === "development") {
console.error("Failed to send analytics event:", response.status);
}
} catch (error) {
if (process.env.NODE_ENV === "development") {
console.error("Failed to send analytics event:", error);
}
}
}
Loading
Loading