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
13 changes: 11 additions & 2 deletions src/backend/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,17 @@
from fastapi.responses import FileResponse
from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
from dotenv import load_dotenv
import posthog

load_dotenv()

POSTHOG_API_KEY = os.environ.get("VITE_PUBLIC_POSTHOG_KEY")
POSTHOG_HOST = os.environ.get("VITE_PUBLIC_POSTHOG_HOST")

if POSTHOG_API_KEY:
posthog.project_api_key = POSTHOG_API_KEY
posthog.host = POSTHOG_HOST

from db import init_db
from config import STATIC_DIR, ASSETS_DIR
Expand All @@ -33,7 +43,6 @@ async def lifespan(_: FastAPI):
allow_headers=["*"],
)

print("ASSETS_DIR", ASSETS_DIR)
app.mount("/assets", StaticFiles(directory=ASSETS_DIR), name="assets")

@app.get("/")
Expand Down
3 changes: 2 additions & 1 deletion src/backend/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ asyncpg
python-dotenv
PyJWT
requests
sqlalchemy
sqlalchemy
posthog
40 changes: 38 additions & 2 deletions src/backend/routers/canvas.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import json
import jwt
from typing import Dict, Any
from fastapi import APIRouter, HTTPException, Depends
from fastapi import APIRouter, HTTPException, Depends, Request
from fastapi.responses import JSONResponse

from dependencies import SessionData, require_auth
from db import store_canvas_data, get_canvas_data
import posthog

canvas_router = APIRouter()

Expand All @@ -19,14 +20,49 @@ def get_default_canvas_data():
detail=f"Failed to load default canvas: {str(e)}"
)

@canvas_router.get("/default")
async def get_default_canvas(auth: SessionData = Depends(require_auth)):
try:
with open("default_canvas.json", "r") as f:
canvas_data = json.load(f)
return canvas_data
except Exception as e:
return JSONResponse(
status_code=500,
content={"error": f"Failed to load default canvas: {str(e)}"}
)

@canvas_router.post("")
async def save_canvas(data: Dict[str, Any], auth: SessionData = Depends(require_auth)):
async def save_canvas(data: Dict[str, Any], auth: SessionData = Depends(require_auth), request: Request = None):
access_token = auth.token_data.get("access_token")
decoded = jwt.decode(access_token, options={"verify_signature": False})
user_id = decoded["sub"]
success = await store_canvas_data(user_id, data)
if not success:
raise HTTPException(status_code=500, detail="Failed to save canvas data")
# PostHog analytics: capture canvas_saved event
try:
app_state = data.get("appState", {})
width = app_state.get("width")
height = app_state.get("height")
zoom = app_state.get("zoom", {}).get("value")
api_path = str(request.url.path) if request else None
full_url = None
if request:
full_url = str(request.base_url).rstrip("/") + str(request.url.path)
posthog.capture(
distinct_id=user_id,
event="canvas_saved",
properties={
"pad_width": width,
"pad_height": height,
"pad_zoom": zoom,
"$current_url": full_url,
}
)
except Exception as e:
print(f"Error capturing canvas_saved event: {str(e)}")
pass
return {"status": "success"}

@canvas_router.get("")
Expand Down
16 changes: 15 additions & 1 deletion src/backend/routers/user.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import jwt
from fastapi import APIRouter, Depends
import posthog

from dependencies import SessionData, require_auth

Expand All @@ -11,6 +12,19 @@ async def get_user_info(auth: SessionData = Depends(require_auth)):
access_token = token_data.get("access_token")

decoded = jwt.decode(access_token, options={"verify_signature": False})

# Identify user in PostHog (mirrors frontend identify)
posthog.identify(
distinct_id=decoded["sub"],
properties={
"email": decoded.get("email", ""),
"username": decoded.get("preferred_username", ""),
"name": decoded.get("name", ""),
"given_name": decoded.get("given_name", ""),
"family_name": decoded.get("family_name", ""),
"email_verified": decoded.get("email_verified", False)
}
)

return {
"id": decoded["sub"], # Unique user ID
Expand All @@ -20,4 +34,4 @@ async def get_user_info(auth: SessionData = Depends(require_auth)):
"given_name": decoded.get("given_name", ""),
"family_name": decoded.get("family_name", ""),
"email_verified": decoded.get("email_verified", False)
}
}
35 changes: 0 additions & 35 deletions src/frontend/excalidraw-patch.js

This file was deleted.

1 change: 0 additions & 1 deletion src/frontend/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@

window.ExcalidrawLib = ExcalidrawLib;
</script>
<script type="module" src="excalidraw-patch.js"></script>
<script type="module" src="index.tsx"></script>
</body>
</html>
18 changes: 4 additions & 14 deletions src/frontend/index.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import React, { StrictMode } from "react";
import { createRoot } from "react-dom/client";

import posthog from "./src/utils/posthog";
import { PostHogProvider } from 'posthog-js/react';

import { QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import { queryClient } from './src/api/queryClient';
Expand All @@ -11,12 +14,8 @@ import "./src/styles/index.scss";
import type * as TExcalidraw from "@excalidraw/excalidraw";

import App from "./src/App";
import { AuthProvider } from "./src/auth/AuthContext";
import AuthGate from "./src/AuthGate";
import ErrorBoundary from "./src/ErrorBoundary";

// PostHog is automatically initialized in ./utils/posthog.ts
import "./src/utils/posthog";

declare global {
interface Window {
Expand All @@ -30,15 +29,7 @@ async function initApp() {
const { Excalidraw } = window.ExcalidrawLib;
root.render(
<StrictMode>

<ErrorBoundary>
<PostHogProvider
apiKey={import.meta.env.VITE_PUBLIC_POSTHOG_KEY}
options={{
api_host: import.meta.env.VITE_PUBLIC_POSTHOG_HOST,
}}
>

<PostHogProvider client={posthog}>
<QueryClientProvider client={queryClient}>
<AuthGate>
<App
Expand All @@ -51,7 +42,6 @@ async function initApp() {
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
</PostHogProvider>
</ErrorBoundary>
</StrictMode>,
);
}
Expand Down
39 changes: 13 additions & 26 deletions src/frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import { useSaveCanvas } from "./api/hooks";
import type * as TExcalidraw from "@excalidraw/excalidraw";
import type { NonDeletedExcalidrawElement } from "@excalidraw/excalidraw/element/types";
import type { ExcalidrawImperativeAPI, AppState } from "@excalidraw/excalidraw/types";
import AuthModal from "./auth/AuthModal";
import { useAuthCheck } from "./api/hooks";

export interface AppProps {
Expand All @@ -26,10 +25,7 @@ export default function App({
}: AppProps) {
const { useHandleLibrary, MainMenu } = excalidrawLib;

// Get authentication state from React Query
const { data: isAuthenticated, isLoading: isAuthLoading } = useAuthCheck();

// Get user profile for analytics identification
const { data: userProfile } = useUserProfile();

// Only enable canvas queries if authenticated and not loading
Expand All @@ -44,12 +40,10 @@ export default function App({
useCustom(excalidrawAPI, customArgs);
useHandleLibrary({ excalidrawAPI });

// On login and canvas data load, update the scene
// Helper to ensure collaborators is a Map
function normalizeCanvasData(data: any) {
if (!data) return data;
const appState = { ...data.appState };
// Remove width and height so they get recomputed when loading from DB
appState.width = undefined;
if ("width" in appState) {
delete appState.width;
}
Expand All @@ -68,40 +62,30 @@ export default function App({
}
}, [excalidrawAPI, canvasData]);

// Use React Query mutation for saving canvas
const { mutate: saveCanvas } = useSaveCanvas({
onSuccess: () => {
console.debug("Canvas saved to database successfully");
// Track canvas save event with PostHog
capture('canvas_saved');
console.debug("[pad.ws] Canvas saved to database successfully");
},
onError: (error) => {
console.error("Failed to save canvas to database:", error);
// Track canvas save failure
capture('canvas_save_failed', {
error: error instanceof Error ? error.message : 'Unknown error'
});
console.error("[pad.ws] Failed to save canvas to database:", error);
}
});

useEffect(() => {
if (excalidrawAPI) {
(window as any).excalidrawAPI = excalidrawAPI;
// Track application loaded event
capture('app_loaded');
}
return () => {
(window as any).excalidrawAPI = null;
};
}, [excalidrawAPI]);

// Ref to store the last sent canvas data for change detection
const lastSentCanvasDataRef = useRef<string>("");

const debouncedLogChange = useCallback(
debounce(
(elements: NonDeletedExcalidrawElement[], state: AppState, files: any) => {
// Only save if authenticated
if (!isAuthenticated) return;

const canvasData = {
Expand All @@ -110,11 +94,9 @@ export default function App({
files
};

// Compare with last sent data (deep equality via JSON.stringify)
const serialized = JSON.stringify(canvasData);
if (serialized !== lastSentCanvasDataRef.current) {
lastSentCanvasDataRef.current = serialized;
// Use React Query mutation to save canvas
saveCanvas(canvasData);
}
},
Expand All @@ -123,12 +105,18 @@ export default function App({
[saveCanvas, isAuthenticated]
);

// Identify user in PostHog when username is available
useEffect(() => {
if (userProfile?.username) {
posthog.identify(userProfile.username);
if (userProfile?.id) {
posthog.identify(userProfile.id);
if (posthog.people && typeof posthog.people.set === "function") {
const {
id, // do not include in properties
...personProps
} = userProfile;
posthog.people.set(personProps);
}
}
}, [userProfile?.username]);
}, [userProfile]);

return (
<>
Expand All @@ -141,7 +129,6 @@ export default function App({
{children}
</ExcalidrawWrapper>

{/* AuthModal is now handled by AuthGate */}
</>
);
}
Loading