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
4 changes: 4 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -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
17 changes: 17 additions & 0 deletions backend/app/analytics.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
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
)
Comment on lines +5 to +9
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the referenced file and surrounding context
sed -n '1,120p' backend/app/analytics.py | nl -ba

# Search for other Posthog initialization / debug flags
rg -n "Posthog\\(|debug\\s*=\\s*True|POSTHOG" backend -S || true

# Show settings related to Posthog if present
rg -n "POSTHOG_(API_KEY|HOST)" -S . || true
rg -n "class Settings|settings\\." backend -S || true

Repository: shubhamxdd/pyq-appl

Length of output: 106


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the referenced file with line numbers (nl not available)
awk '{printf "%6d\t%s\n", NR, $0}' backend/app/analytics.py | sed -n '1,160p'

# Search for other Posthog initialization / debug flags
rg -n "Posthog\\(" backend -S || true
rg -n "debug\\s*=\\s*True|debug\\s*=\\s*false|POSTHOG" backend -S || true

# Search for settings definitions / env flags around POSTHOG
rg -n "POSTHOG_(API_KEY|HOST|DEBUG)|posthog" backend -S -i || true
rg -n "class Settings|BaseSettings|settings\\s*=" backend -S || true

Repository: shubhamxdd/pyq-appl

Length of output: 2374


🏁 Script executed:

#!/bin/bash
set -euo pipefail

awk '{printf "%6d\t%s\n", NR, $0}' backend/app/config.py | sed -n '1,140p'

Repository: shubhamxdd/pyq-appl

Length of output: 1449


🏁 Script executed:

#!/bin/bash
set -euo pipefail

awk '{printf "%6d\t%s\n", NR, $0}' backend/app/config.py | sed -n '1,140p'

Repository: shubhamxdd/pyq-appl

Length of output: 1449


🌐 Web query:

PostHog Python Posthog debug parameter what does debug=True do logs payloads

💡 Result:

In the PostHog Python SDK, setting debug=True enables verbose logging, which helps troubleshoot issues with event capturing, feature flags, or survey rendering [1]. When enabled, the SDK changes the logging level of the posthog logger to DEBUG [2]. This causes the library to output internal operational logs to the standard logging output [1][2]. Specifically, enabling debug=True performs the following actions: - It configures the logger by calling logging.basicConfig() [2]. - It sets the log level for the posthog logger to logging.DEBUG (instead of the default logging.WARNING) [2]. - This allows you to see detailed information about the SDK's internal processes [1], which can include information regarding the processing and sending of payloads, helping you verify if events or configurations are being handled as expected [1]. You can enable this mode during client initialization or by setting the attribute directly on the client instance: python # During initialization from posthog import Posthog posthog = Posthog('<ph_project_token>', debug=True) # Or by setting the attribute posthog.debug = True If you are not seeing expected data appear in your PostHog instance, this mode is the recommended way to inspect the SDK's activity and identify why events might not be reaching the server [1].

Citations:


Disable PostHog debug mode by default.

debug=True in backend/app/analytics.py turns on the PostHog SDK’s verbose logging (sets the posthog logger to DEBUG), which can cause sensitive event/payload processing details to end up in server logs. Default debug to False and gate it behind an explicit settings/env flag (the current backend/app/config.py has POSTHOG_API_KEY/POSTHOG_HOST only, with no debug toggle).

ph_client = Posthog(
    settings.POSTHOG_API_KEY,
    host=settings.POSTHOG_HOST,
    debug=True
)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/app/analytics.py` around lines 5 - 9, Change the Posthog client
instantiation so it does not enable verbose SDK logging by default: set the
Posthog debug argument to False and wire it to a new settings flag (e.g.
settings.POSTHOG_DEBUG) so debug can only be enabled via explicit configuration;
update the settings/config module to read POSTHOG_DEBUG from env (defaulting to
False) and then construct ph_client with Posthog(settings.POSTHOG_API_KEY,
host=settings.POSTHOG_HOST, debug=settings.POSTHOG_DEBUG) so sensitive payloads
are not logged unless deliberately turned on.


# 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]}***")
print(f"✅ [POSTHOG] Initialized")
5 changes: 5 additions & 0 deletions backend/app/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down
1 change: 1 addition & 0 deletions backend/app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from fastapi.middleware.cors import CORSMiddleware
from .routers import auth, resources, solver, papers
from .config import settings
from .analytics import ph_client

app = FastAPI(title="PYQ Solver API")

Expand Down
7 changes: 7 additions & 0 deletions backend/app/routers/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"])
Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand Down Expand Up @@ -110,13 +115,15 @@ 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
await db.commit()
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)
Expand Down
20 changes: 20 additions & 0 deletions backend/app/routers/papers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__)

Expand Down Expand Up @@ -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)}")
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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}
13 changes: 13 additions & 0 deletions backend/app/routers/resources.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
})

Comment on lines +98 to +104
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show the relevant portion of the file with line numbers
sed -n '70,140p' backend/app/routers/resources.py | nl -ba -w3 -s': '

# Also find where the upload/try/cleanup block is defined in this file
rg -n "try:|except:|finally:|cleanup|rollback|delete|remove|capture\\(" backend/app/routers/resources.py

# Show the exact ph_client usage context (a bit wider)
rg -n "ph_client\\.capture" -n backend/app/routers/resources.py

Repository: shubhamxdd/pyq-appl

Length of output: 106


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== resources.py (lines ~1-220) with line numbers) ==="
# Use cat -n instead of nl (nl is missing in the sandbox)
sed -n '1,220p' backend/app/routers/resources.py | cat -n -w3 -s': '

echo "=== Search for upload/try/cleanup/rollback and capture usage ==="
rg -n "ph_client\\.capture|try:|except:|finally:|cleanup|rollback|delete|remove|filesystem|storage|upload" backend/app/routers/resources.py

Repository: shubhamxdd/pyq-appl

Length of output: 202


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== resources.py: lines 60-140 (with line numbers) ==="
sed -n '60,140p' backend/app/routers/resources.py | cat -n

echo "=== Exact ph_client.capture call contexts ==="
rg -n "ph_client\.capture" backend/app/routers/resources.py

echo "=== Surrounding try/except/finally around capture (context) ==="
# Print a window around the capture line(s)
CAPTURE_LINE="$(rg -n "ph_client\.capture" backend/app/routers/resources.py | head -n1 | cut -d: -f1 || true)"
if [ -n "${CAPTURE_LINE}" ]; then
  START=$((CAPTURE_LINE-30))
  END=$((CAPTURE_LINE+30))
  echo "CAPTURE_LINE=${CAPTURE_LINE}; showing ${START}-${END}"
  sed -n "${START},${END}p" backend/app/routers/resources.py | cat -n
fi

Repository: shubhamxdd/pyq-appl

Length of output: 6809


🏁 Script executed:

#!/bin/bash
set -euo pipefail
echo "=== resources.py: lines 90-170 (capture + surrounding try/except) ==="
sed -n '90,170p' backend/app/routers/resources.py | cat -n

echo "=== resources.py: find storage cleanup/delete/remove calls in this area ==="
rg -n "delete|remove|cleanup|rollback|finally" backend/app/routers/resources.py | head -n 80

Repository: shubhamxdd/pyq-appl

Length of output: 4139


Move ph_client.capture inside the transactional/cleanup block (best-effort)

ph_client.capture("resource_upload_started", ...) runs before the outer try/except that cleans up storage via storage_service.delete_file(object_name). If analytics throws after storage_service.upload_file(...) succeeds, the function will exit without that cleanup, leaving orphaned files in storage. Wrap the capture in the guarded section (or add a local try/except around it) and make it best-effort.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/app/routers/resources.py` around lines 98 - 104,
ph_client.capture("resource_upload_started", ...) currently runs before the
outer try/except that performs cleanup via
storage_service.delete_file(object_name), so if analytics fails after
storage_service.upload_file(...) succeeds you can leave orphaned files; move the
ph_client.capture call into the guarded transactional/cleanup section (the same
try/except that calls storage_service.delete_file) or wrap the capture in its
own local try/except that catches and logs errors but does not re-raise, making
the analytics call best-effort while ensuring
storage_service.delete_file(object_name) always runs.

try:
# Create DB record (not committed yet)
new_resource = Resource(
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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
9 changes: 9 additions & 0 deletions backend/app/routers/solver.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"])
Expand All @@ -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])
Expand Down Expand Up @@ -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},
Expand Down
34 changes: 34 additions & 0 deletions backend/app/workers/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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'):
Expand All @@ -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:
Expand Down Expand Up @@ -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}")
Expand Down Expand Up @@ -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:
Expand All @@ -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
Expand Down
1 change: 1 addition & 0 deletions backend/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,4 @@ python-dotenv
pypdfium2
pillow
email-validator
posthog
12 changes: 10 additions & 2 deletions context/progress-tracker.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@

## Completed

- **Sprint 7: Phase 4 (Analytics & Growth)**:
- **PostHog Integration**: Full-stack analytics pipeline implemented across React and FastAPI.
- **Granular Event Tracking**: Instrumented high-value actions including resource uploads, AI questions, paper generations, and PDF exports.
- **Conversion Funnel instrumentation**: Added behavioral tracking to Landing Page sections, FAQ interactions, and Pricing buttons.
- **Revenue Insight**: Instrumented `UpgradeModal` to track intent vs. hesitation on paywall hits.
- **Robust Backend Implementation**: Resolved circular imports and implemented a dedicated `analytics.py` module with debug mode and string-ID formatting.
- **Sprint 7: Phase 3 (Marketing & Identity)**:
- **PrepAI Landing Page**: Fully implemented as a high-converting single-page layout at `/`.
- Replicated professional Hero section and "Meet the two superpowers" from HTML template.
Expand All @@ -23,6 +29,8 @@
- **LLM Resilience**: Implemented exponential backoff retries (2s, 4s, 8s) in `OpenRouterClient` for all API calls.
- **Production Deployment**: VPS updated and live with unified Docker setup behind system Nginx reverse proxy.
- **Sprint 7: Phase 1 (UI/UX & Onboarding)**:
- **Solver & Generator Mobile UX**: Added Rename and Delete buttons to sidebar items and chat/paper headers on mobile for easier session management.
- **Inline Header Renaming**: Implemented a seamless renaming flow directly in both Solver and Generator headers.
- Implemented `PublicRoute` to redirect authenticated users away from login/register.
- Added dismissible `WelcomeBanner` on the Dashboard for new user guidance.
- Created `UpgradeModal` and integrated it into Solver, Generator, and Resources for quota limit handling.
Expand Down Expand Up @@ -53,6 +61,6 @@

## Session Notes

- Sprint 7: Marketing & Identity phase complete. Landing page is live and fully polished.
- Sprint 7: Analytics & Growth phase complete. Full-stack PostHog integration is live with granular behavioral tracking.
- App architecture is now clearly separated between public marketing and private dashboard.
- Future focus shifted towards core AI performance (RAG) and monetization.
- Future focus shifted towards core AI performance (RAG) and monetization (Razorpay).
2 changes: 2 additions & 0 deletions frontend/.env.example
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
VITE_API_URL=http://127.0.0.1:8001/api
VITE_POSTHOG_KEY=
VITE_POSTHOG_HOST=https://us.i.posthog.com
Loading