From 82b19ce60ee638b769b1e95dd1376d72751c3c7f Mon Sep 17 00:00:00 2001 From: shubhamxdd Date: Thu, 21 May 2026 20:09:22 +0530 Subject: [PATCH 1/7] feat: integrate PostHog analytics into frontend and backend --- .env.example | 4 + backend/app/config.py | 5 + backend/app/main.py | 6 + backend/requirements.txt | 2 + frontend/.env.example | 2 + frontend/package-lock.json | 436 ++++++++++++++++++++++++++++++++++++- frontend/package.json | 1 + frontend/src/App.tsx | 5 + frontend/src/main.tsx | 8 + 9 files changed, 467 insertions(+), 2 deletions(-) diff --git a/.env.example b/.env.example index c3965bb..5f5bec4 100644 --- a/.env.example +++ b/.env.example @@ -19,3 +19,7 @@ VITE_API_URL=http://127.0.0.1:8001/api POSTGRES_USER=user POSTGRES_PASSWORD=password POSTGRES_DB=pyqdb + +# PostHog +POSTHOG_API_KEY= +POSTHOG_HOST=https://us.i.posthog.com diff --git a/backend/app/config.py b/backend/app/config.py index a3ac0a7..199b9bc 100644 --- a/backend/app/config.py +++ b/backend/app/config.py @@ -32,6 +32,11 @@ class Settings(BaseSettings): QUESTIONS_LIMIT: int = 30 PAPERS_LIMIT: int = 3 RESOURCES_LIMIT: int = 3 + + # PostHog Settings + POSTHOG_API_KEY: str = "" + POSTHOG_HOST: str = "https://us.i.posthog.com" + class Config: env_file = ".env" diff --git a/backend/app/main.py b/backend/app/main.py index 4512a20..84084e6 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -2,9 +2,15 @@ from fastapi.middleware.cors import CORSMiddleware from .routers import auth, resources, solver, papers from .config import settings +from posthog import Posthog app = FastAPI(title="PYQ Solver API") +# Initialize PostHog +ph_client = Posthog(settings.POSTHOG_API_KEY, host=settings.POSTHOG_HOST) +if not settings.POSTHOG_API_KEY: + ph_client.disabled = True + # Configure CORS app.add_middleware( CORSMiddleware, diff --git a/backend/requirements.txt b/backend/requirements.txt index 5043a18..ff116a8 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -19,3 +19,5 @@ python-dotenv pypdfium2 pillow email-validator +posthog +posthog \ No newline at end of file diff --git a/frontend/.env.example b/frontend/.env.example index 194386d..ac4f1f4 100644 --- a/frontend/.env.example +++ b/frontend/.env.example @@ -1 +1,3 @@ VITE_API_URL=http://127.0.0.1:8001/api +VITE_POSTHOG_KEY= +VITE_POSTHOG_HOST=https://us.i.posthog.com diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 168a340..7277aa8 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -15,6 +15,7 @@ "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "lucide-react": "^1.16.0", + "posthog-js": "^1.218.1", "radix-ui": "^1.4.3", "react": "^19.2.6", "react-dom": "^19.2.6", @@ -1247,6 +1248,252 @@ "integrity": "sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==", "license": "MIT" }, + "node_modules/@opentelemetry/api": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.1.tgz", + "integrity": "sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==", + "license": "Apache-2.0", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/api-logs": { + "version": "0.208.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.208.0.tgz", + "integrity": "sha512-CjruKY9V6NMssL/T1kAFgzosF1v9o6oeN+aX5JB/C/xPNtmgIJqcXHG7fA82Ou1zCpWGl4lROQUKwUNE1pMCyg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api": "^1.3.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/core": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.2.0.tgz", + "integrity": "sha512-FuabnnUm8LflnieVxs6eP7Z383hgQU4W1e3KJS6aOG3RxWxcHyBxH8fDMHNgu/gFx/M2jvTOW/4/PHhLz6bjWw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-logs-otlp-http": { + "version": "0.208.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-logs-otlp-http/-/exporter-logs-otlp-http-0.208.0.tgz", + "integrity": "sha512-jOv40Bs9jy9bZVLo/i8FwUiuCvbjWDI+ZW13wimJm4LjnlwJxGgB+N/VWOZUTpM+ah/awXeQqKdNlpLf2EjvYg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.208.0", + "@opentelemetry/core": "2.2.0", + "@opentelemetry/otlp-exporter-base": "0.208.0", + "@opentelemetry/otlp-transformer": "0.208.0", + "@opentelemetry/sdk-logs": "0.208.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/otlp-exporter-base": { + "version": "0.208.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.208.0.tgz", + "integrity": "sha512-gMd39gIfVb2OgxldxUtOwGJYSH8P1kVFFlJLuut32L6KgUC4gl1dMhn+YC2mGn0bDOiQYSk/uHOdSjuKp58vvA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.2.0", + "@opentelemetry/otlp-transformer": "0.208.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/otlp-transformer": { + "version": "0.208.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-transformer/-/otlp-transformer-0.208.0.tgz", + "integrity": "sha512-DCFPY8C6lAQHUNkzcNT9R+qYExvsk6C5Bto2pbNxgicpcSWbe2WHShLxkOxIdNcBiYPdVHv/e7vH7K6TI+C+fQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.208.0", + "@opentelemetry/core": "2.2.0", + "@opentelemetry/resources": "2.2.0", + "@opentelemetry/sdk-logs": "0.208.0", + "@opentelemetry/sdk-metrics": "2.2.0", + "@opentelemetry/sdk-trace-base": "2.2.0", + "protobufjs": "^7.3.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/otlp-transformer/node_modules/@opentelemetry/resources": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.2.0.tgz", + "integrity": "sha512-1pNQf/JazQTMA0BiO5NINUzH0cbLbbl7mntLa4aJNmCCXSj0q03T5ZXXL0zw4G55TjdL9Tz32cznGClf+8zr5A==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.2.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/resources": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.7.1.tgz", + "integrity": "sha512-DeT6KKolmC4e/dRQvMQ/RwlnzhaqeiFOXY5ngoOPJ07GgVVKxZOg9EcrNZb5aTzUn+iCrJldAgOfQm1O/QfPAQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.7.1", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/resources/node_modules/@opentelemetry/core": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.7.1.tgz", + "integrity": "sha512-QAqIj32AtK6+pEVNG7EOVxHdE06RP+FM5qpiEJ4RtDcFIqKUZHYhl7/7UY5efhwmwNAg7j8QbJVBLxMerc0+gw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-logs": { + "version": "0.208.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.208.0.tgz", + "integrity": "sha512-QlAyL1jRpOeaqx7/leG1vJMp84g0xKP6gJmfELBpnI4O/9xPX+Hu5m1POk9Kl+veNkyth5t19hRlN6tNY1sjbA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.208.0", + "@opentelemetry/core": "2.2.0", + "@opentelemetry/resources": "2.2.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.4.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-logs/node_modules/@opentelemetry/resources": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.2.0.tgz", + "integrity": "sha512-1pNQf/JazQTMA0BiO5NINUzH0cbLbbl7mntLa4aJNmCCXSj0q03T5ZXXL0zw4G55TjdL9Tz32cznGClf+8zr5A==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.2.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-metrics": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.2.0.tgz", + "integrity": "sha512-G5KYP6+VJMZzpGipQw7Giif48h6SGQ2PFKEYCybeXJsOCB4fp8azqMAAzE5lnnHK3ZVwYQrgmFbsUJO/zOnwGw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.2.0", + "@opentelemetry/resources": "2.2.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.9.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-metrics/node_modules/@opentelemetry/resources": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.2.0.tgz", + "integrity": "sha512-1pNQf/JazQTMA0BiO5NINUzH0cbLbbl7mntLa4aJNmCCXSj0q03T5ZXXL0zw4G55TjdL9Tz32cznGClf+8zr5A==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.2.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-trace-base": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.2.0.tgz", + "integrity": "sha512-xWQgL0Bmctsalg6PaXExmzdedSp3gyKV8mQBwK/j9VGdCDu2fmXIb2gAehBKbkXCpJ4HPkgv3QfoJWRT4dHWbw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.2.0", + "@opentelemetry/resources": "2.2.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-trace-base/node_modules/@opentelemetry/resources": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.2.0.tgz", + "integrity": "sha512-1pNQf/JazQTMA0BiO5NINUzH0cbLbbl7mntLa4aJNmCCXSj0q03T5ZXXL0zw4G55TjdL9Tz32cznGClf+8zr5A==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.2.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/semantic-conventions": { + "version": "1.41.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.41.1.tgz", + "integrity": "sha512-/UhIkaZgPutTFmQ7RnIJGgDXZmtEJ7Dvi86xNTFWcnRxVRNk/aotsqDJYeEvDP+FSMB2SdW+pQzNMcWP0rwuNA==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, "node_modules/@oxc-project/types": { "version": "0.130.0", "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.130.0.tgz", @@ -1257,6 +1504,84 @@ "url": "https://github.com/sponsors/Boshen" } }, + "node_modules/@posthog/core": { + "version": "1.29.7", + "resolved": "https://registry.npmjs.org/@posthog/core/-/core-1.29.7.tgz", + "integrity": "sha512-WcBD9/YQVGI9r/5+/IGeaPgsmTIg0YfyzaTei5TNlhmAeFOccnhs269rhtQJcAXngZFpvWSj+RTxX2ONdgxBDQ==", + "license": "MIT", + "dependencies": { + "@posthog/types": "1.374.4" + } + }, + "node_modules/@posthog/types": { + "version": "1.374.4", + "resolved": "https://registry.npmjs.org/@posthog/types/-/types-1.374.4.tgz", + "integrity": "sha512-OHBo+gReFwPJtt/yLY6xxa1EYMp7Ti07O1C1KE9ZXXyyuLNqekRaHZxJ/SKUfEvt1LhFV/9sioz8O0xfsSffsQ==", + "license": "MIT" + }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.5.tgz", + "integrity": "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.1.tgz", + "integrity": "sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==", + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/inquire": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.2.tgz", + "integrity": "sha512-pa0vFRuws4wkvaXKK1uXZMAwAX4/t8ANaJo45iw/oQHNQ9q5xUzwgFmVJGXiga2BeN+zpX7Vf9vmsiIa2J+MUw==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.1.tgz", + "integrity": "sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg==", + "license": "BSD-3-Clause" + }, "node_modules/@radix-ui/number": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz", @@ -3438,6 +3763,7 @@ "version": "19.2.14", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", + "dev": true, "license": "MIT", "dependencies": { "csstype": "^3.2.2" @@ -3447,7 +3773,7 @@ "version": "19.2.3", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", - "devOptional": true, + "dev": true, "license": "MIT", "peerDependencies": { "@types/react": "^19.2.0" @@ -3468,6 +3794,13 @@ "integrity": "sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA==", "license": "MIT" }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "license": "MIT", + "optional": true + }, "node_modules/@types/unist": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", @@ -4469,6 +4802,17 @@ "node": ">=6.6.0" } }, + "node_modules/core-js": { + "version": "3.49.0", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.49.0.tgz", + "integrity": "sha512-es1U2+YTtzpwkxVLwAFdSpaIMyQaq0PBgm3YD1W3Qpsn1NAmO3KSgZfu+oGSWVu6NvLHoHCV/aYcsE5wiB7ALg==", + "hasInstallScript": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, "node_modules/cors": { "version": "2.8.6", "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", @@ -4718,6 +5062,15 @@ "node": ">=0.3.1" } }, + "node_modules/dompurify": { + "version": "3.4.5", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.5.tgz", + "integrity": "sha512-OrwIBKsdNSVEeubdJ1HBv/wNENRM9ytAVCv7YXt//A3vPdVMNuACRqK9mXCGCBW2ln7BT/A4X0jXHo2Gu89miA==", + "license": "(MPL-2.0 OR Apache-2.0)", + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, "node_modules/dotenv": { "version": "17.4.2", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz", @@ -5392,6 +5745,12 @@ "node": "^12.20 || >= 14.13" } }, + "node_modules/fflate": { + "version": "0.4.8", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.4.8.tgz", + "integrity": "sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA==", + "license": "MIT" + }, "node_modules/figures": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/figures/-/figures-6.1.0.tgz", @@ -6724,6 +7083,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" + }, "node_modules/longest-streak": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", @@ -8005,6 +8370,27 @@ "dev": true, "license": "MIT" }, + "node_modules/posthog-js": { + "version": "1.374.4", + "resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.374.4.tgz", + "integrity": "sha512-6RtCHzeUKsfkd21QFXnOUbBhorVqemZ57xuJwxEpb0fTj4wrX+tlItm7seY0LsX5LXLRHPxA02uwL/DaGfWXKg==", + "license": "SEE LICENSE IN LICENSE", + "dependencies": { + "@opentelemetry/api": "^1.9.0", + "@opentelemetry/api-logs": "^0.208.0", + "@opentelemetry/exporter-logs-otlp-http": "^0.208.0", + "@opentelemetry/resources": "^2.2.0", + "@opentelemetry/sdk-logs": "^0.208.0", + "@posthog/core": "1.29.7", + "@posthog/types": "1.374.4", + "core-js": "^3.38.1", + "dompurify": "^3.3.2", + "fflate": "^0.4.8", + "preact": "^10.28.2", + "query-selector-shadow-dom": "^1.0.1", + "web-vitals": "^5.1.0" + } + }, "node_modules/powershell-utils": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/powershell-utils/-/powershell-utils-0.1.0.tgz", @@ -8017,6 +8403,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/preact": { + "version": "10.29.2", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.29.2.tgz", + "integrity": "sha512-7tNmwg/7mzzAoB/8kSg6Hl37JraAZw3Z3A0JSY7VXlZwo82Xn0G7wKbNNs2qoF4ZEEsQGTwDAroNdqKs1ofJxQ==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -8074,6 +8470,30 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/protobufjs": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.0.tgz", + "integrity": "sha512-LtESOsMPTZgyYtwxhvdgdjGL0HmXEaRA/hVD6sol4zA60hVXXXP/SGmxnqDbgGE8gy7pYex7cym+5vYPcmaXBQ==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.5", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.1", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.2", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.1", + "@types/node": ">=13.7.0", + "long": "^5.3.2" + }, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -8121,6 +8541,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/query-selector-shadow-dom": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/query-selector-shadow-dom/-/query-selector-shadow-dom-1.0.1.tgz", + "integrity": "sha512-lT5yCqEBgfoMYpf3F2xQRK7zEr1rhIIZuceDK6+xRkJQ4NMbHTwXqk4NkwDwQMNqXgG9r9fyHnzwNVs6zV5KRw==", + "license": "MIT" + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -9357,7 +9783,7 @@ "version": "6.0.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", - "devOptional": true, + "dev": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -9754,6 +10180,12 @@ "node": ">= 8" } }, + "node_modules/web-vitals": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-5.2.0.tgz", + "integrity": "sha512-i2z98bEmaCqSDiHEDu+gHl/dmR4Q+TxFmG3/13KkMO+o8UxQzCqWaDRCiLgEa41nlO4VpXSI0ASa1xWmO9sBlA==", + "license": "Apache-2.0" + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/frontend/package.json b/frontend/package.json index d55ef8f..ad5aa00 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -17,6 +17,7 @@ "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "lucide-react": "^1.16.0", + "posthog-js": "^1.218.1", "radix-ui": "^1.4.3", "react": "^19.2.6", "react-dom": "^19.2.6", diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index eb47ecd..f20491a 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -17,6 +17,7 @@ import api from './api/auth'; import { resourcesApi } from './api/resources'; import { papersApi } from './api/papers'; import { useEffect } from 'react'; +import posthog from 'posthog-js'; import { TooltipProvider } from "@/components/ui/tooltip" import { @@ -76,6 +77,10 @@ function Dashboard() { useEffect(() => { if (user && token) { setAuth(user, token); + posthog.identify(user.id, { + email: user.email, + plan: user.plan, + }); } }, [user, token, setAuth]); diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index bef5202..0c0d524 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -1,8 +1,16 @@ import { StrictMode } from 'react' import { createRoot } from 'react-dom/client' +import posthog from 'posthog-js' import './index.css' import App from './App.tsx' +if (import.meta.env.VITE_POSTHOG_KEY) { + posthog.init(import.meta.env.VITE_POSTHOG_KEY, { + api_host: import.meta.env.VITE_POSTHOG_HOST || 'https://us.i.posthog.com', + person_profiles: 'identified_only', + }) +} + createRoot(document.getElementById('root')!).render( From 57aefbb4c925e4dc5d51d6b5e153b260a1dd2fd1 Mon Sep 17 00:00:00 2001 From: shubhamxdd Date: Thu, 21 May 2026 20:54:12 +0530 Subject: [PATCH 2/7] feat: integrate PostHog analytics tracking across various components of backend 1. Authentication * user_registered: Captures when a new account is created. * Properties: email, method (password vs. google). * user_logged_in: Captures every session start. * Properties: method. 2. Document Management (Resources) * resource_upload_started: Triggers as soon as a file hits our API. * Properties: filename, content_type, size_mb, type (notes/syllabus/etc). * resource_processed: Triggers in the Worker when OCR/Text extraction finishes. * Properties: num_pages, filename, type. * resource_processing_failed: Triggers if the AI fails to read the PDF. * Properties: error (the specific reason it failed). * resource_deleted: Triggers when a user removes a file. 3. AI Tutor (Solver) * chat_session_created: Triggers when a new chat history starts. * ai_question_asked: Triggers for every message sent to the AI. * Properties: num_resources (how many files the AI is looking at), question_length (char count), delivery_mode. 4. Exam Generator * paper_format_detected: Triggers when the AI successfully reads a syllabus and extracts a pattern (MCQs, Short Qs, etc). * paper_generation_started: Triggers when the generation job is queued. * paper_generated: Triggers in the Worker when the questions are ready. * Properties: num_questions. * paper_generation_failed: Triggers if the generator crashes. * Properties: error. * paper_pdf_downloaded: Triggers when the user clicks "Export to PDF". * Properties: mode (Study Guide vs. Question Paper). --- backend/app/analytics.py | 16 +++++++++++++++ backend/app/main.py | 7 +------ backend/app/routers/auth.py | 7 +++++++ backend/app/routers/papers.py | 20 +++++++++++++++++++ backend/app/routers/resources.py | 13 ++++++++++++ backend/app/routers/solver.py | 11 ++++++++++- backend/app/workers/tasks.py | 34 ++++++++++++++++++++++++++++++++ 7 files changed, 101 insertions(+), 7 deletions(-) create mode 100644 backend/app/analytics.py diff --git a/backend/app/analytics.py b/backend/app/analytics.py new file mode 100644 index 0000000..dc38fbd --- /dev/null +++ b/backend/app/analytics.py @@ -0,0 +1,16 @@ +from posthog import Posthog +from .config import settings + +# Initialize PostHog client +ph_client = Posthog( + settings.POSTHOG_API_KEY, + host=settings.POSTHOG_HOST, + debug=True # Helpful for debugging delivery issues +) + +# Gracefully disable if no API key is provided +if not settings.POSTHOG_API_KEY: + print("⚠️ [POSTHOG] Disabled: No API Key found in settings") + ph_client.disabled = True +else: + print(f"✅ [POSTHOG] Initialized with Key: {settings.POSTHOG_API_KEY[:8]}***") diff --git a/backend/app/main.py b/backend/app/main.py index 84084e6..63f63fb 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -2,15 +2,10 @@ from fastapi.middleware.cors import CORSMiddleware from .routers import auth, resources, solver, papers from .config import settings -from posthog import Posthog +from .analytics import ph_client app = FastAPI(title="PYQ Solver API") -# Initialize PostHog -ph_client = Posthog(settings.POSTHOG_API_KEY, host=settings.POSTHOG_HOST) -if not settings.POSTHOG_API_KEY: - ph_client.disabled = True - # Configure CORS app.add_middleware( CORSMiddleware, diff --git a/backend/app/routers/auth.py b/backend/app/routers/auth.py index 81b090c..86e6cd3 100644 --- a/backend/app/routers/auth.py +++ b/backend/app/routers/auth.py @@ -8,6 +8,7 @@ from ..schemas.user import UserCreate, UserLogin, UserOut, Token, TokenData, GoogleLogin from ..services.security import get_password_hash, verify_password, create_access_token from ..config import settings +from ..analytics import ph_client import httpx router = APIRouter(prefix="/auth", tags=["auth"]) @@ -57,6 +58,9 @@ async def register(user_in: UserCreate, db: AsyncSession = Depends(get_db)): db.add(new_user) await db.commit() await db.refresh(new_user) + + ph_client.capture("user_registered", distinct_id=str(new_user.id), properties={"email": new_user.email, "method": "password"}) + return new_user @router.post("/login", response_model=Token) @@ -72,6 +76,7 @@ async def login(user_in: UserLogin, db: AsyncSession = Depends(get_db)): ) access_token = create_access_token(subject=user.id) + ph_client.capture("user_logged_in", distinct_id=str(user.id), properties={"method": "password"}) return {"access_token": access_token, "token_type": "bearer"} @router.post("/google", response_model=Token) @@ -110,6 +115,7 @@ async def google_login(data: GoogleLogin, db: AsyncSession = Depends(get_db)): db.add(user) await db.commit() await db.refresh(user) + ph_client.capture("user_registered", distinct_id=str(user.id), properties={"email": email, "method": "google"}) elif not user.google_id: # Link existing email account to Google ID user.google_id = google_id @@ -117,6 +123,7 @@ async def google_login(data: GoogleLogin, db: AsyncSession = Depends(get_db)): await db.refresh(user) access_token = create_access_token(subject=user.id) + ph_client.capture("user_logged_in", distinct_id=str(user.id), properties={"method": "google"}) return {"access_token": access_token, "token_type": "bearer"} @router.get("/me", response_model=UserOut) diff --git a/backend/app/routers/papers.py b/backend/app/routers/papers.py index 629784c..e69332d 100644 --- a/backend/app/routers/papers.py +++ b/backend/app/routers/papers.py @@ -24,6 +24,7 @@ from arq.connections import RedisSettings from ..services.pdf import generate_paper_pdf from ..services.storage import storage_service +from ..analytics import ph_client logger = logging.getLogger(__name__) @@ -68,6 +69,12 @@ async def detect_format( clean_json = clean_json[:-3] format_config = json.loads(clean_json) + + ph_client.capture("paper_format_detected", distinct_id=str(current_user.id), properties={ + "resource_id": str(data.resource_id), + "format": format_config.get("format_name", "unknown") + }) + return format_config except Exception as e: raise HTTPException(status_code=500, detail=f"Failed to detect format: {str(e)}") @@ -138,6 +145,12 @@ async def create_paper( await db.commit() await db.refresh(new_paper) + ph_client.capture("paper_generation_started", distinct_id=str(current_user.id), properties={ + "paper_id": str(new_paper.id), + "num_resources": len(data.resources), + "delivery_mode": data.delivery_mode + }) + from ..models.job import Job new_job = Job( user_id=current_user.id, @@ -264,6 +277,8 @@ async def delete_paper( await db.delete(paper) await db.commit() + ph_client.capture("paper_deleted", distinct_id=str(current_user.id), properties={"paper_id": str(paper_id)}) + return {"message": "Paper deleted successfully"} @router.get("/{paper_id}/output", response_model=PaperOutputOut) @@ -383,4 +398,9 @@ async def get_paper_pdf_url( await db.commit() + ph_client.capture("paper_pdf_downloaded", distinct_id=str(current_user.id), properties={ + "paper_id": str(paper_id), + "mode": mode + }) + return {"url": pdf_url} diff --git a/backend/app/routers/resources.py b/backend/app/routers/resources.py index 11b4505..7375580 100644 --- a/backend/app/routers/resources.py +++ b/backend/app/routers/resources.py @@ -10,6 +10,7 @@ from ..schemas.resource import ResourceOut, ResourceUpdate from ..routers.auth import get_current_user from ..services.storage import storage_service +from ..analytics import ph_client from arq import create_pool from ..config import settings from arq.connections import RedisSettings @@ -94,6 +95,13 @@ async def upload_resource( detail="Failed to upload file to storage" ) + ph_client.capture("resource_upload_started", distinct_id=str(current_user.id), properties={ + "filename": file.filename, + "content_type": file.content_type, + "size_mb": total_size / (1024 * 1024), + "type": type + }) + try: # Create DB record (not committed yet) new_resource = Resource( @@ -187,6 +195,8 @@ async def delete_resource( await db.delete(resource) await db.commit() + ph_client.capture("resource_deleted", distinct_id=str(current_user.id), properties={"resource_id": str(resource_id)}) + return {"message": "Resource deleted successfully"} @router.patch("/{resource_id}", response_model=ResourceOut) @@ -246,6 +256,8 @@ async def retry_extraction( # Only commit if enqueue was successful await db.commit() await db.refresh(resource) + + ph_client.capture("resource_retry_extraction", distinct_id=str(current_user.id), properties={"resource_id": str(resource_id)}) except Exception as e: await db.rollback() raise HTTPException( @@ -278,5 +290,6 @@ async def stop_processing( resource.status = "failed" await db.commit() await db.refresh(resource) + ph_client.capture("resource_stop_processing", distinct_id=str(current_user.id), properties={"resource_id": str(resource_id)}) return resource diff --git a/backend/app/routers/solver.py b/backend/app/routers/solver.py index 92b82cd..5b79560 100644 --- a/backend/app/routers/solver.py +++ b/backend/app/routers/solver.py @@ -17,6 +17,7 @@ from ..llm.client import open_router_client from ..llm.prompts import SOLVER_SYSTEM, SOLVER_USER_TEMPLATE from ..config import settings +from ..analytics import ph_client router = APIRouter(prefix="/solver", tags=["solver"]) @@ -32,6 +33,7 @@ async def create_session( db.add(new_session) await db.commit() await db.refresh(new_session) + ph_client.capture("chat_session_created", distinct_id=str(current_user.id), properties={"session_id": str(new_session.id)}) return new_session @router.get("/sessions", response_model=List[ChatSessionOut]) @@ -209,6 +211,13 @@ async def ask_question( current_user.questions_used += 1 await db.commit() + ph_client.capture("ai_question_asked", distinct_id=str(current_user.id), properties={ + "session_id": str(session_id), + "num_resources": len(data.resource_ids), + "delivery_mode": data.delivery_mode, + "question_length": len(data.content) + }) + # 5. Prepare LLM Call messages = [ {"role": "system", "content": SOLVER_SYSTEM}, @@ -239,4 +248,4 @@ async def event_generator(): answer_rec.status = "failed" await async_db.commit() - return StreamingResponse(event_generator(), media_type="text/event-stream") + return StreamingResponse(event_generator(), media_type="text_event-stream") diff --git a/backend/app/workers/tasks.py b/backend/app/workers/tasks.py index d86897c..4f42609 100644 --- a/backend/app/workers/tasks.py +++ b/backend/app/workers/tasks.py @@ -8,6 +8,7 @@ from ..models.resource import Resource from ..config import settings from ..services.storage import storage_service +from ..analytics import ph_client import httpx import pypdfium2 as pdfium from PIL import Image @@ -165,6 +166,14 @@ async def extraction_task(ctx, resource_id: str, job_id: str = None): print(f"💾 [STEP 3/3] Saving extracted text to DB...") await db.commit() + + ph_client.capture("resource_processed", distinct_id=str(resource.user_id), properties={ + "resource_id": str(resource_id), + "filename": resource.filename, + "num_pages": num_pages, + "type": resource.type + }) + print(f"🏁 [TASK COMPLETE] Resource {resource_id} is now READY.\n") elif resource.filename.lower().endswith('.txt'): @@ -180,6 +189,13 @@ async def extraction_task(ctx, resource_id: str, job_id: str = None): job.completed_at = datetime.utcnow() await db.commit() + + ph_client.capture("resource_processed", distinct_id=str(resource.user_id), properties={ + "resource_id": str(resource_id), + "filename": resource.filename, + "type": resource.type + }) + print("🏁 [TASK COMPLETE] Text file is READY.\n") else: @@ -217,6 +233,12 @@ async def extraction_task(ctx, resource_id: str, job_id: str = None): j.completed_at = datetime.utcnow() await err_db.commit() + + ph_client.capture("resource_processing_failed", distinct_id=str(res_to_fail.user_id), properties={ + "resource_id": str(resource_id), + "error": str(e) + }) + print(f"📉 [DB] Resource {resource_id} marked as FAILED.") except Exception as final_err: print(f"💀 [FATAL] Could not even mark as failed: {final_err}") @@ -359,6 +381,12 @@ async def generate_paper_task(ctx, paper_id: str, job_id: str = None): job.completed_at = datetime.utcnow() await db.commit() + + ph_client.capture("paper_generated", distinct_id=str(paper.user_id), properties={ + "paper_id": str(paper_id), + "num_questions": len(questions) + }) + print(f"🏁 [TASK COMPLETE] Paper {paper_id} is READY.\n") except Exception as e: @@ -382,6 +410,12 @@ async def generate_paper_task(ctx, paper_id: str, job_id: str = None): j.completed_at = datetime.utcnow() await err_db.commit() + + ph_client.capture("paper_generation_failed", distinct_id=str(paper_to_fail.user_id), properties={ + "paper_id": str(paper_id), + "error": str(e) + }) + print(f"📉 [DB] Paper {paper_id} marked as FAILED.") except: pass From da0f24cebd0e1a25fb12b2f84ac31dca12685728 Mon Sep 17 00:00:00 2001 From: shubhamxdd Date: Thu, 21 May 2026 21:12:23 +0530 Subject: [PATCH 3/7] feat: implement PostHog analytics tracking for user actions in Generator, Login, Register, and Solver components --- frontend/src/lib/analytics.ts | 22 ++++++++++++++++++++++ frontend/src/pages/Generator.tsx | 3 +++ frontend/src/pages/Login.tsx | 9 +++++++++ frontend/src/pages/Register.tsx | 2 ++ frontend/src/pages/Solver.tsx | 7 +++++++ 5 files changed, 43 insertions(+) create mode 100644 frontend/src/lib/analytics.ts diff --git a/frontend/src/lib/analytics.ts b/frontend/src/lib/analytics.ts new file mode 100644 index 0000000..033b0fa --- /dev/null +++ b/frontend/src/lib/analytics.ts @@ -0,0 +1,22 @@ +import posthog from 'posthog-js'; + +/** + * Capture a custom event with properties + */ +export const trackEvent = (event: string, properties?: Record) => { + posthog.capture(event, properties); +}; + +/** + * Identify user and set person properties + */ +export const identifyUser = (userId: string, properties?: Record) => { + posthog.identify(userId, properties); +}; + +/** + * Reset user identity on logout + */ +export const resetUser = () => { + posthog.reset(); +}; diff --git a/frontend/src/pages/Generator.tsx b/frontend/src/pages/Generator.tsx index 040268e..3c1b786 100644 --- a/frontend/src/pages/Generator.tsx +++ b/frontend/src/pages/Generator.tsx @@ -50,6 +50,7 @@ import { Checkbox } from "@/components/ui/checkbox" import { toast } from 'react-hot-toast'; import api from '@/api/auth'; import { UpgradeModal } from '../components/UpgradeModal'; +import { trackEvent } from '@/lib/analytics'; export default function Generator() { const queryClient = useQueryClient(); @@ -148,12 +149,14 @@ export default function Generator() { setFormatConfig(null); navigate(`/generator/${newPaper.id}`); toast.success('Paper generation started!'); + trackEvent('paper_generation_clicked', { title }); }, onError: (error: any) => { if (error.response?.status === 403) { setUpgradeMessage(error.response?.data?.detail || "You've reached your monthly paper limit."); setIsUpgradeModalOpen(true); setIsCreateOpen(false); + trackEvent('limit_reached_modal_viewed', { feature: 'generator', message: upgradeMessage }); } else { toast.error(error.response?.data?.detail || 'Failed to create paper.'); } diff --git a/frontend/src/pages/Login.tsx b/frontend/src/pages/Login.tsx index 214855d..c55a539 100644 --- a/frontend/src/pages/Login.tsx +++ b/frontend/src/pages/Login.tsx @@ -17,6 +17,7 @@ import { import { Input } from "@/components/ui/input" import { Label } from "@/components/ui/label" import { Zap, Loader2 } from "lucide-react" +import { identifyUser, trackEvent } from '@/lib/analytics'; const loginSchema = z.object({ email: z.string().email('Invalid email address'), @@ -55,6 +56,14 @@ export default function Login() { }); setAuth(userResponse.data, access_token); + + // Analytics + identifyUser(userResponse.data.id, { + email: userResponse.data.email, + plan: userResponse.data.plan + }); + trackEvent('login_success', { method: 'password' }); + navigate('/'); } catch (err: any) { setError(err.response?.data?.detail || 'Login failed. Please check your credentials.'); diff --git a/frontend/src/pages/Register.tsx b/frontend/src/pages/Register.tsx index bac4afd..2c59f32 100644 --- a/frontend/src/pages/Register.tsx +++ b/frontend/src/pages/Register.tsx @@ -16,6 +16,7 @@ import { import { Input } from "@/components/ui/input" import { Label } from "@/components/ui/label" import { Zap, Loader2 } from "lucide-react" +import { trackEvent } from '@/lib/analytics'; const registerSchema = z.object({ email: z.string().email('Invalid email address'), @@ -53,6 +54,7 @@ export default function Register() { email: data.email, password: data.password, }); + trackEvent('registration_success', { email: data.email }); navigate('/login'); } catch (err: any) { setError(err.response?.data?.detail || 'Registration failed. Please try again.'); diff --git a/frontend/src/pages/Solver.tsx b/frontend/src/pages/Solver.tsx index ce21dd6..f9e2c16 100644 --- a/frontend/src/pages/Solver.tsx +++ b/frontend/src/pages/Solver.tsx @@ -39,6 +39,7 @@ import { SheetTitle, } from "@/components/ui/sheet" import { UpgradeModal } from '../components/UpgradeModal'; +import { trackEvent } from '../lib/analytics'; export default function Solver() { const queryClient = useQueryClient(); @@ -189,10 +190,16 @@ export default function Solver() { const errData = await response.json(); setUpgradeMessage(errData.detail || "You've reached your question limit."); setIsUpgradeModalOpen(true); + trackEvent('limit_reached_modal_viewed', { feature: 'solver', message: upgradeMessage }); setMessages(prev => prev.slice(0, -1)); setIsStreaming(false); return; } + + trackEvent('question_submitted', { + session_id: activeSessionId, + num_resources: selectedResources.length + }); if (!response.ok) { const errData = await response.json(); From b61961d9ded2c61694298fa8e421310b1da2ee30 Mon Sep 17 00:00:00 2001 From: shubhamxdd Date: Thu, 21 May 2026 21:17:52 +0530 Subject: [PATCH 4/7] feat: add PostHog analytics tracking for dashboard and landing page views, and resource upload events --- frontend/src/App.tsx | 2 ++ frontend/src/pages/Landing.tsx | 4 ++++ frontend/src/pages/Resources.tsx | 3 +++ 3 files changed, 9 insertions(+) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index f20491a..d8abab2 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -43,6 +43,7 @@ import { } from "lucide-react" import { cn } from './lib/utils'; import { WelcomeBanner } from './components/WelcomeBanner'; +import { trackEvent } from './lib/analytics'; const queryClient = new QueryClient(); @@ -81,6 +82,7 @@ function Dashboard() { email: user.email, plan: user.plan, }); + trackEvent('dashboard_viewed'); } }, [user, token, setAuth]); diff --git a/frontend/src/pages/Landing.tsx b/frontend/src/pages/Landing.tsx index b36a525..d0505f2 100644 --- a/frontend/src/pages/Landing.tsx +++ b/frontend/src/pages/Landing.tsx @@ -29,6 +29,7 @@ import { import { Badge } from '@/components/ui/badge'; import { useState, useEffect } from 'react'; import { cn } from '@/lib/utils'; +import { trackEvent } from '@/lib/analytics'; export default function Landing() { const token = useAuthStore((state) => state.token); @@ -39,6 +40,9 @@ export default function Landing() { const [openFaq, setOpenFaq] = useState(null); useEffect(() => { + // Analytics + trackEvent('landing_page_viewed'); + // Check initial theme const storedTheme = localStorage.getItem('theme'); const isDarkTheme = storedTheme === 'dark' || (storedTheme === null && window.matchMedia('(prefers-color-scheme: dark)').matches); diff --git a/frontend/src/pages/Resources.tsx b/frontend/src/pages/Resources.tsx index f0ca258..a7c8b85 100644 --- a/frontend/src/pages/Resources.tsx +++ b/frontend/src/pages/Resources.tsx @@ -56,6 +56,7 @@ import { SelectValue, } from "@/components/ui/select" import { UpgradeModal } from '../components/UpgradeModal'; +import { trackEvent } from '../lib/analytics'; export default function Resources() { const queryClient = useQueryClient(); @@ -89,11 +90,13 @@ export default function Resources() { queryClient.invalidateQueries({ queryKey: ['resources'] }); setFile(null); toast.success('File uploaded successfully!'); + trackEvent('resource_upload_clicked', { type }); }, onError: (error: any) => { if (error.response?.status === 403) { setUpgradeMessage(error.response?.data?.detail || "You've reached your resource storage limit."); setIsUpgradeModalOpen(true); + trackEvent('limit_reached_modal_viewed', { feature: 'resources', message: upgradeMessage }); } else { const message = error.response?.data?.detail || 'Failed to upload file.'; toast.error(message); From 3851a40b2d70d13602db71f17aa1cb53bd4bdfc2 Mon Sep 17 00:00:00 2001 From: shubhamxdd Date: Thu, 21 May 2026 21:27:26 +0530 Subject: [PATCH 5/7] feat: add PostHog analytics tracking for button clicks in UpgradeModal and Landing page --- frontend/src/components/UpgradeModal.tsx | 12 ++++++++++-- frontend/src/pages/Landing.tsx | 14 ++++++++++---- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/frontend/src/components/UpgradeModal.tsx b/frontend/src/components/UpgradeModal.tsx index 961b64e..56e4182 100644 --- a/frontend/src/components/UpgradeModal.tsx +++ b/frontend/src/components/UpgradeModal.tsx @@ -8,6 +8,7 @@ import { } from "./ui/dialog"; import { Button } from "./ui/button"; import { CheckCircle2, Zap, FileText, FileEdit, Crown } from "lucide-react"; +import { trackEvent } from "../lib/analytics"; interface UpgradeModalProps { isOpen: boolean; @@ -67,6 +68,10 @@ export function UpgradeModal({ isOpen, onClose, message }: UpgradeModalProps) { - ); -} +} \ No newline at end of file diff --git a/frontend/src/pages/Landing.tsx b/frontend/src/pages/Landing.tsx index d0505f2..3e81f85 100644 --- a/frontend/src/pages/Landing.tsx +++ b/frontend/src/pages/Landing.tsx @@ -684,7 +684,7 @@ export default function Landing() { ))} - @@ -726,7 +726,7 @@ export default function Landing() { ))} - @@ -759,7 +759,7 @@ export default function Landing() { ))} - @@ -783,7 +783,13 @@ export default function Landing() { style={{ transitionDelay: `${i * 100}ms` }} >