diff --git a/.env.example b/.env.example index e9b7b09..d297e7c 100644 --- a/.env.example +++ b/.env.example @@ -22,3 +22,7 @@ CLERK_SECRET_KEY=some-secret-key # Uploadthing UPLOADTHING_TOKEN='uploadthing-token' + +# Posthog +NEXT_PUBLIC_POSTHOG_KEY=some-posthog-key +NEXT_PUBLIC_POSTHOG_HOST=the-posthog-host diff --git a/README.md b/README.md index a7dea1f..354734d 100644 --- a/README.md +++ b/README.md @@ -89,7 +89,7 @@ Tracking progress on key features and tasks for the project. - [x] 🔗 Sync folder open state with the URL - [x] 🔐 Implement user authentication - [x] 📁 Enable file upload functionality -- [ ] 📊 Add analytics tracking +- [x] 📊 Add analytics tracking ### 📝 Note from 5-28-2025 diff --git a/netlify.toml b/netlify.toml new file mode 100644 index 0000000..fa67400 --- /dev/null +++ b/netlify.toml @@ -0,0 +1,13 @@ +[[redirects]] + from = "/ingest/static/*" + to = "https://us-assets.i.posthog.com/static/:splat" + host = "us-assets.i.posthog.com" + status = 200 + force = true + +[[redirects]] + from = "/ingest/*" + to = "https://us.i.posthog.com/:splat" + host = "us.i.posthog.com" + status = 200 + force = true diff --git a/package.json b/package.json index 527b754..db12f6d 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "lucide-react": "^0.511.0", "mysql2": "^3.14.1", "next": "^15.2.3", + "posthog-js": "^1.257.0", "react": "^19.0.0", "react-dom": "^19.0.0", "tailwind-merge": "^3.3.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0de454c..9ebd051 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -41,6 +41,9 @@ importers: next: specifier: ^15.2.3 version: 15.3.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + posthog-js: + specifier: ^1.257.0 + version: 1.257.0 react: specifier: ^19.0.0 version: 19.1.0 @@ -1281,6 +1284,9 @@ packages: resolution: {integrity: sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==} engines: {node: '>=18'} + core-js@3.44.0: + resolution: {integrity: sha512-aFCtd4l6GvAXwVEh3XbbVqJGHDJt0OZRa+5ePGx3LLwi12WfexqQxcsohb2wgsa/92xtl19Hd66G/L+TaAxDMw==} + cors@2.8.5: resolution: {integrity: sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==} engines: {node: '>= 0.10'} @@ -1717,6 +1723,9 @@ packages: resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==} engines: {node: ^12.20 || >= 14.13} + fflate@0.4.8: + resolution: {integrity: sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA==} + file-entry-cache@8.0.0: resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} engines: {node: '>=16.0.0'} @@ -2374,6 +2383,20 @@ packages: resolution: {integrity: sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==} engines: {node: ^10 || ^12 || >=14} + posthog-js@1.257.0: + resolution: {integrity: sha512-Ujg9RGtWVCu+4tmlRpALSy2ZOZI6JtieSYXIDDdgMWm167KYKvTtbMPHdoBaPWcNu0Km+1hAIBnQFygyn30KhA==} + peerDependencies: + '@rrweb/types': 2.0.0-alpha.17 + rrweb-snapshot: 2.0.0-alpha.17 + peerDependenciesMeta: + '@rrweb/types': + optional: true + rrweb-snapshot: + optional: true + + preact@10.26.9: + resolution: {integrity: sha512-SSjF9vcnF27mJK1XyFMNJzFd5u3pQiATFqoaDy03XuN00u4ziveVVEGt5RKJrDR8MHE/wJo9Nnad56RLzS2RMA==} + prelude-ls@1.2.1: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} @@ -2835,6 +2858,9 @@ packages: resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} engines: {node: '>= 8'} + web-vitals@4.2.4: + resolution: {integrity: sha512-r4DIlprAGwJ7YM11VZp4R884m0Vmgr6EAKe3P+kO0PPj3Unqyvv59rczf6UiGcb9Z8QxZVcqKNwv/g0WNdWwsw==} + which-boxed-primitive@1.1.1: resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==} engines: {node: '>= 0.4'} @@ -3879,6 +3905,8 @@ snapshots: cookie@1.0.2: {} + core-js@3.44.0: {} + cors@2.8.5: dependencies: object-assign: 4.1.1 @@ -4441,6 +4469,8 @@ snapshots: node-domexception: 1.0.0 web-streams-polyfill: 3.3.3 + fflate@0.4.8: {} + file-entry-cache@8.0.0: dependencies: flat-cache: 4.0.1 @@ -5106,6 +5136,15 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 + posthog-js@1.257.0: + dependencies: + core-js: 3.44.0 + fflate: 0.4.8 + preact: 10.26.9 + web-vitals: 4.2.4 + + preact@10.26.9: {} + prelude-ls@1.2.1: {} prettier-plugin-tailwindcss@0.6.11(prettier@3.5.3): @@ -5607,6 +5646,8 @@ snapshots: web-streams-polyfill@3.3.3: {} + web-vitals@4.2.4: {} + which-boxed-primitive@1.1.1: dependencies: is-bigint: 1.1.0 diff --git a/src/app/page.tsx b/src/app/(home)/page.tsx similarity index 100% rename from src/app/page.tsx rename to src/app/(home)/page.tsx diff --git a/src/app/_providers/posthog-provider.tsx b/src/app/_providers/posthog-provider.tsx new file mode 100644 index 0000000..7177df8 --- /dev/null +++ b/src/app/_providers/posthog-provider.tsx @@ -0,0 +1,26 @@ +"use client"; + +import posthog from "posthog-js"; +import { PostHogProvider as PHProvider } from "posthog-js/react"; +import { useEffect } from "react"; + +import UserIdentification from "./user-identification"; + +import { env } from "~/env"; + +export function PostHogProvider({ children }: { children: React.ReactNode }) { + useEffect(() => { + posthog.init(env.NEXT_PUBLIC_POSTHOG_KEY, { + api_host: env.NEXT_PUBLIC_POSTHOG_HOST, + person_profiles: "identified_only", // or 'always' to create profiles for anonymous users as well + defaults: "2025-05-24", + }); + }, []); + + return ( + + + {children} + + ); +} diff --git a/src/app/_providers/user-identification.tsx b/src/app/_providers/user-identification.tsx new file mode 100644 index 0000000..5c1d02b --- /dev/null +++ b/src/app/_providers/user-identification.tsx @@ -0,0 +1,20 @@ +import { useUser } from "@clerk/nextjs"; +import { usePostHog } from "posthog-js/react"; +import { useEffect } from "react"; + +export default function UserIdentification() { + const posthog = usePostHog(); + const { user } = useUser(); + + useEffect(() => { + if (user) { + posthog.identify(user.id, { + email: user.emailAddresses[0]?.emailAddress, + }); + } else { + posthog.reset(); + } + }, [posthog, user]); + + return null; // Do not render anything +} diff --git a/src/app/f/[folderId]/page.tsx b/src/app/f/[folderId]/page.tsx index f788a9b..2b189b9 100644 --- a/src/app/f/[folderId]/page.tsx +++ b/src/app/f/[folderId]/page.tsx @@ -2,7 +2,7 @@ import { z } from "zod"; import * as queries from "~/server/db/queries"; -import DriveContents from "../../drive-contents"; +import DriveContents from "../drive-contents"; export default async function GoogleDriveClone(props: { params: Promise<{ folderId: number }>; diff --git a/src/app/drive-contents.tsx b/src/app/f/drive-contents.tsx similarity index 100% rename from src/app/drive-contents.tsx rename to src/app/f/drive-contents.tsx diff --git a/src/app/file-row.tsx b/src/app/f/file-row.tsx similarity index 100% rename from src/app/file-row.tsx rename to src/app/f/file-row.tsx diff --git a/src/app/layout.tsx b/src/app/layout.tsx index d442c0e..7aa0a8a 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,9 +1,11 @@ -import "~/styles/globals.css"; - import { ClerkProvider } from "@clerk/nextjs"; import { type Metadata } from "next"; import { Geist } from "next/font/google"; +import { PostHogProvider } from "./_providers/posthog-provider"; + +import "~/styles/globals.css"; + export const metadata: Metadata = { title: "Drive Tutorial", description: "It's like Google Drive, but worse!", @@ -20,9 +22,11 @@ export default function RootLayout({ }: Readonly<{ children: React.ReactNode }>) { return ( - - {children} - + + + {children} + + ); } diff --git a/src/env.js b/src/env.js index 9f7c80d..1aedd71 100644 --- a/src/env.js +++ b/src/env.js @@ -23,7 +23,8 @@ export const env = createEnv({ * `NEXT_PUBLIC_`. */ client: { - // NEXT_PUBLIC_CLIENTVAR: z.string(), + NEXT_PUBLIC_POSTHOG_KEY: z.string(), + NEXT_PUBLIC_POSTHOG_HOST: z.string(), }, /** @@ -37,7 +38,8 @@ export const env = createEnv({ SINGLESTORE_HOST: process.env.SINGLESTORE_HOST, SINGLESTORE_PORT: process.env.SINGLESTORE_PORT, SINGLESTORE_DATABASE: process.env.SINGLESTORE_DATABASE, - // NEXT_PUBLIC_CLIENTVAR: process.env.NEXT_PUBLIC_CLIENTVAR, + NEXT_PUBLIC_POSTHOG_KEY: process.env.NEXT_PUBLIC_POSTHOG_KEY, + NEXT_PUBLIC_POSTHOG_HOST: process.env.NEXT_PUBLIC_POSTHOG_HOST, }, /** * Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation. This is especially