Skip to content

Commit

Permalink
add basic tracking
Browse files Browse the repository at this point in the history
  • Loading branch information
invisal committed Jan 9, 2025
1 parent cd976dc commit adc9593
Show file tree
Hide file tree
Showing 16 changed files with 249 additions and 31 deletions.
14 changes: 14 additions & 0 deletions next.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,20 @@ const nextConfig = {
env: {
NEXT_PUBLIC_STUDIO_VERSION: pkg.version,
},
headers: async () => {
return [
{
source: "/api/events",
headers: [
{ key: "Access-Control-Allow-Origin", value: "*" },
{
key: "Access-Control-Allow-Methods",
value: "GET,DELETE,PATCH,POST,PUT",
},
],
},
];
},
};

module.exports = withMDX(nextConfig);
2 changes: 2 additions & 0 deletions src/app/(public)/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Toaster } from "@/components/ui/sonner";
import { Fragment } from "react";
import Script from "next/script";
import { cn } from "@/lib/utils";
import PageTracker from "@/components/page-tracker";

const inter = Inter({ subsets: ["latin"] });

Expand All @@ -17,6 +18,7 @@ export default async function RootLayout({
<Fragment>{children}</Fragment>
<Toaster />
<Analytics />
<PageTracker />
<Script async defer src="https://buttons.github.io/buttons.js" />
</body>
);
Expand Down
7 changes: 6 additions & 1 deletion src/app/(theme)/client/[[...driver]]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import ClientOnly from "@/components/client-only";
import ClientPageBody from "./page-client";

export default function SessionPage() {
return <ClientPageBody />;
return (
<ClientOnly>
<ClientPageBody />
</ClientOnly>
);
}
23 changes: 13 additions & 10 deletions src/app/(theme)/client/r/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { database } from "@/db/schema";
import { getSessionFromCookie } from "@/lib/auth";
import { and, eq, isNotNull } from "drizzle-orm";
import ClientPageBody from "./page-client";
import ClientOnly from "@/components/client-only";

interface RemoteSessionPageProps {
searchParams: Promise<{ p: string }>;
Expand All @@ -28,15 +29,17 @@ export default async function RemoteSessionPage(props: RemoteSessionPageProps) {
}

return (
<ClientPageBody
token={session.id}
config={{
id: databaseId,
name: databaseInfo.name ?? "",
storage: "remote",
description: databaseInfo.description ?? "",
label: (databaseInfo.color ?? "blue") as SavedConnectionLabel,
}}
/>
<ClientOnly>
<ClientPageBody
token={session.id}
config={{
id: databaseId,
name: databaseInfo.name ?? "",
storage: "remote",
description: databaseInfo.description ?? "",
label: (databaseInfo.color ?? "blue") as SavedConnectionLabel,
}}
/>
</ClientOnly>
);
}
7 changes: 6 additions & 1 deletion src/app/(theme)/client/s/[[...driver]]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import ClientOnly from "@/components/client-only";
import ClientPageBody from "./page-client";

export default function SessionPage() {
return <ClientPageBody />;
return (
<ClientOnly>
<ClientPageBody />
</ClientOnly>
);
}
3 changes: 2 additions & 1 deletion src/app/(theme)/embed/embed-page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { ReactElement } from "react";
import ThemeLayout from "../theme_layout";
import ClientOnly from "@/components/client-only";

export interface EmbedPageProps {
searchParams: Promise<{
Expand Down Expand Up @@ -36,7 +37,7 @@ export function createEmbedPage(render: () => ReactElement) {
disableToggle={disableToggle}
overrideThemeVariables={overrideThemeVariables}
>
{render()}
<ClientOnly>{render()}</ClientOnly>
</ThemeLayout>
);
};
Expand Down
5 changes: 4 additions & 1 deletion src/app/(theme)/playground/client/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { eq, sql } from "drizzle-orm";
import { dbDataset } from "@/db/schema-dataset";
import { Metadata } from "next";
import ThemeLayout from "../../theme_layout";
import ClientOnly from "@/components/client-only";

export const metadata: Metadata = {
title:
Expand Down Expand Up @@ -59,7 +60,9 @@ export default async function PlaygroundEditor(props: PlaygroundEditorProps) {

return (
<ThemeLayout>
<PlaygroundEditorBody preloadDatabase={templateFile} />
<ClientOnly>
<PlaygroundEditorBody preloadDatabase={templateFile} />
</ClientOnly>
</ThemeLayout>
);
}
5 changes: 4 additions & 1 deletion src/app/(theme)/playground/mysql/[roomName]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Metadata } from "next";
import ThemeLayout from "../../../theme_layout";
import MySQLPlaygroundPageClient from "./page-client";
import ClientOnly from "@/components/client-only";

export const metadata: Metadata = {
title:
Expand Down Expand Up @@ -35,7 +36,9 @@ export default async function MySQLPlaygroundEditor(

return (
<ThemeLayout>
<MySQLPlaygroundPageClient roomName={roomName} />
<ClientOnly>
<MySQLPlaygroundPageClient roomName={roomName} />
</ClientOnly>
</ThemeLayout>
);
}
2 changes: 2 additions & 0 deletions src/app/(theme)/theme_layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { Toaster } from "@/components/ui/sonner";
import { Fragment, PropsWithChildren } from "react";
import Script from "next/script";
import { cn } from "@/lib/utils";
import PageTracker from "@/components/page-tracker";

const inter = Inter({ subsets: ["latin"] });

Expand Down Expand Up @@ -36,6 +37,7 @@ export default async function ThemeLayout({
<Toaster />
</ThemeProvider>
<Analytics />
<PageTracker />
<Script async defer src="https://buttons.github.io/buttons.js" />
</body>
);
Expand Down
54 changes: 54 additions & 0 deletions src/app/api/events/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// This API tracks user behavior by associating each user with a unique device ID.
// The collected data helps enhance user experience, identify and fix bugs,
// and gain insights for improving the product.
//
// All recorded data will be stored in the Starbase Database.

import { cookies } from "next/headers";
import zod from "zod";
import { NextRequest, NextResponse } from "next/server";
import { insertTrackingRecord } from "@/lib/api/insert-tracking-record";

const eventBodySchema = zod.object({
events: zod
.array(
zod.object({
name: zod.string().max(255),
data: zod.any().optional(),
})
)
.min(1),
});

export const POST = async (req: NextRequest) => {
// Getting the device id
const cookieStore = await cookies();

let deviceId = cookieStore.get("od-id")?.value;

if (!deviceId) {
deviceId = crypto.randomUUID();

cookieStore.set("od-id", deviceId, {
expires: new Date(Date.now() + 1000 * 60 * 60 * 24 * 365),
});
}

// Get the body
const body = await req.json();
const validate = eventBodySchema.safeParse(body);

if (!validate.success) {
return NextResponse.json({
success: false,
error: validate.error.formErrors,
});
}

// Save the event
await insertTrackingRecord(deviceId, validate.data.events.slice(0, 50));

return NextResponse.json({
success: true,
});
};
16 changes: 16 additions & 0 deletions src/components/client-only.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
"use client";
import { PropsWithChildren, useEffect, useState } from "react";

export default function ClientOnly(props: PropsWithChildren) {
const [clientLoaded, setClientLoaded] = useState(false);

useEffect(() => {
setClientLoaded(typeof window !== "undefined");
}, []);

if (clientLoaded) {
return props.children;
}

return null;
}
43 changes: 43 additions & 0 deletions src/components/page-tracker.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
"use client";

import { addTrackEvent, normalizedPathname } from "@/lib/tracking";
import { usePathname } from "next/navigation";
import { useEffect } from "react";

export default function PageTracker() {
const pathname = usePathname();

// Track page views
useEffect(() => {
const normalized = normalizedPathname(pathname);

addTrackEvent("pageview", {
path: normalized,
full_path: pathname === normalized ? undefined : pathname,
});
}, [pathname]);

// Track unhandle rejection
useEffect(() => {
window.addEventListener("unhandledrejection", (event) => {
if (typeof event.reason === "string") {
addTrackEvent("unhandledrejection", {
message: event.reason,
});
} else if (event.reason?.message) {
addTrackEvent("unhandledrejection", {
message: event.reason.message,
stack: event.reason.stack,
});
} else {
addTrackEvent("unhandledrejection", event.toString());
}
});

return () => {
window.removeEventListener("unhandledrejection", () => {});
};
}, []);

return null;
}
22 changes: 7 additions & 15 deletions src/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,25 +8,22 @@ export const appVersion = pkg.version;
export const env = createEnv({
server: {
BASE_URL: z.string().min(1).optional(),

DATABASE_URL: z.string().min(1).optional(),
DATABASE_AUTH_TOKEN: z.string().min(1).optional(),

DATABASE_ANALYTIC_URL: z.string().optional(),
DATABASE_ANALYTIC_AUTH_TOKEN: z.string().optional(),

GITHUB_CLIENT_ID: z.string().optional(),
GITHUB_CLIENT_SECRET: z.string().optional(),

GOOGLE_CLIENT_ID: z.string().optional(),
GOOGLE_CLIENT_SECRET: z.string().optional(),

ENCRYPTION_KEY: z.string().min(30).optional(),

// R2
// Don't include the bucket name in the URL
R2_URL: z.string().optional(),
R2_PUBLIC_URL: z.string().optional(),
R2_BUCKET: z.string().optional(),
R2_ACCESS_KEY: z.string().optional(),
R2_SECRET_ACCESS_KEY: z.string().optional(),
},

experimental__runtimeEnv: {
DATABASE_URL: process.env.DATABASE_URL,
DATABASE_AUTH_TOKEN: process.env.DATABASE_AUTH_TOKEN,
Expand All @@ -35,12 +32,7 @@ export const env = createEnv({
ENCRYPTION_KEY: process.env.ENCRYPTION_KEY,
GOOGLE_CLIENT_ID: process.env.GOOGLE_CLIENT_ID,
GOOGLE_CLIENT_SECRET: process.env.GOOGLE_CLIENT_SECRET,

// R2
R2_URL: process.env.R2_URL,
R2_PUBLIC_URL: process.env.R2_PUBLIC_URL,
R2_BUCKET: process.env.R2_BUCKET,
R2_ACCESS_KEY: process.env.R2_ACCESS_KEY,
R2_SECRET_ACCESS_KEY: process.env.R2_SECRET_ACCESS_KEY,
DATABASE_ANALYTIC_URL: process.env.DATABASE_ANALYTIC_URL,
DATABASE_ANALYTIC_AUTH_TOKEN: process.env.DATABASE_ANALYTIC_AUTH_TOKEN,
},
});
43 changes: 43 additions & 0 deletions src/lib/api/insert-tracking-record.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
"use server";

import StarbaseDriver from "@/drivers/starbase-driver";
import { type TrackEventItem } from "../tracking";
import { env } from "@/env";

export async function insertTrackingRecord(
deviceId: string,
events: TrackEventItem[]
) {
if (!env.DATABASE_ANALYTIC_URL || !env.DATABASE_ANALYTIC_AUTH_TOKEN) {
return {
success: false,
error: "Analytics database is not configured",
};
}

const trackingDb = new StarbaseDriver(env.DATABASE_ANALYTIC_URL, {
Authorization: "Bearer " + env.DATABASE_ANALYTIC_AUTH_TOKEN,
});

const sql = [
"INSERT INTO events(id, created_at, user_id, event_name, event_data) VALUES",
events
.map(
(event) =>
"(" +
[
crypto.randomUUID(),
Date.now(),
deviceId,
event.name,
JSON.stringify(event.data),
]
.map(trackingDb.escapeValue)
.join(", ") +
")"
)
.join(", "),
].join("");

await trackingDb.query(sql);
}
Loading

0 comments on commit adc9593

Please sign in to comment.