Skip to content

Commit d339aee

Browse files
authored
backend analytics + various minor fixes (#1)
* chore: remove excalidraw-patch.js script and its reference from index.html * refactor: integrate PostHog client and remove unused components * feat: implement PostHog analytics for canvas events and user identification * refactor: enhance authentication flow and UI components, update analytics * fix: add custom styles for embeddable renderer * fix: rename to match component * chore: remove unused api-analytics dependency from requirements and main.py * fix: update media query breakpoint for AuthModal styles
1 parent 4413d2a commit d339aee

24 files changed

+178
-393
lines changed

src/backend/main.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,17 @@
66
from fastapi.responses import FileResponse
77
from fastapi.middleware.cors import CORSMiddleware
88
from fastapi.staticfiles import StaticFiles
9-
from fastapi.templating import Jinja2Templates
9+
from dotenv import load_dotenv
10+
import posthog
11+
12+
load_dotenv()
13+
14+
POSTHOG_API_KEY = os.environ.get("VITE_PUBLIC_POSTHOG_KEY")
15+
POSTHOG_HOST = os.environ.get("VITE_PUBLIC_POSTHOG_HOST")
16+
17+
if POSTHOG_API_KEY:
18+
posthog.project_api_key = POSTHOG_API_KEY
19+
posthog.host = POSTHOG_HOST
1020

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

36-
print("ASSETS_DIR", ASSETS_DIR)
3746
app.mount("/assets", StaticFiles(directory=ASSETS_DIR), name="assets")
3847

3948
@app.get("/")

src/backend/requirements.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,5 @@ asyncpg
66
python-dotenv
77
PyJWT
88
requests
9-
sqlalchemy
9+
sqlalchemy
10+
posthog

src/backend/routers/canvas.py

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import json
22
import jwt
33
from typing import Dict, Any
4-
from fastapi import APIRouter, HTTPException, Depends
4+
from fastapi import APIRouter, HTTPException, Depends, Request
55
from fastapi.responses import JSONResponse
66

77
from dependencies import SessionData, require_auth
88
from db import store_canvas_data, get_canvas_data
9+
import posthog
910

1011
canvas_router = APIRouter()
1112

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

23+
@canvas_router.get("/default")
24+
async def get_default_canvas(auth: SessionData = Depends(require_auth)):
25+
try:
26+
with open("default_canvas.json", "r") as f:
27+
canvas_data = json.load(f)
28+
return canvas_data
29+
except Exception as e:
30+
return JSONResponse(
31+
status_code=500,
32+
content={"error": f"Failed to load default canvas: {str(e)}"}
33+
)
34+
2235
@canvas_router.post("")
23-
async def save_canvas(data: Dict[str, Any], auth: SessionData = Depends(require_auth)):
36+
async def save_canvas(data: Dict[str, Any], auth: SessionData = Depends(require_auth), request: Request = None):
2437
access_token = auth.token_data.get("access_token")
2538
decoded = jwt.decode(access_token, options={"verify_signature": False})
2639
user_id = decoded["sub"]
2740
success = await store_canvas_data(user_id, data)
2841
if not success:
2942
raise HTTPException(status_code=500, detail="Failed to save canvas data")
43+
# PostHog analytics: capture canvas_saved event
44+
try:
45+
app_state = data.get("appState", {})
46+
width = app_state.get("width")
47+
height = app_state.get("height")
48+
zoom = app_state.get("zoom", {}).get("value")
49+
api_path = str(request.url.path) if request else None
50+
full_url = None
51+
if request:
52+
full_url = str(request.base_url).rstrip("/") + str(request.url.path)
53+
posthog.capture(
54+
distinct_id=user_id,
55+
event="canvas_saved",
56+
properties={
57+
"pad_width": width,
58+
"pad_height": height,
59+
"pad_zoom": zoom,
60+
"$current_url": full_url,
61+
}
62+
)
63+
except Exception as e:
64+
print(f"Error capturing canvas_saved event: {str(e)}")
65+
pass
3066
return {"status": "success"}
3167

3268
@canvas_router.get("")

src/backend/routers/user.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import jwt
22
from fastapi import APIRouter, Depends
3+
import posthog
34

45
from dependencies import SessionData, require_auth
56

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

1314
decoded = jwt.decode(access_token, options={"verify_signature": False})
15+
16+
# Identify user in PostHog (mirrors frontend identify)
17+
posthog.identify(
18+
distinct_id=decoded["sub"],
19+
properties={
20+
"email": decoded.get("email", ""),
21+
"username": decoded.get("preferred_username", ""),
22+
"name": decoded.get("name", ""),
23+
"given_name": decoded.get("given_name", ""),
24+
"family_name": decoded.get("family_name", ""),
25+
"email_verified": decoded.get("email_verified", False)
26+
}
27+
)
1428

1529
return {
1630
"id": decoded["sub"], # Unique user ID
@@ -20,4 +34,4 @@ async def get_user_info(auth: SessionData = Depends(require_auth)):
2034
"given_name": decoded.get("given_name", ""),
2135
"family_name": decoded.get("family_name", ""),
2236
"email_verified": decoded.get("email_verified", False)
23-
}
37+
}

src/frontend/excalidraw-patch.js

Lines changed: 0 additions & 35 deletions
This file was deleted.

src/frontend/index.html

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@
2121

2222
window.ExcalidrawLib = ExcalidrawLib;
2323
</script>
24-
<script type="module" src="excalidraw-patch.js"></script>
2524
<script type="module" src="index.tsx"></script>
2625
</body>
2726
</html>

src/frontend/index.tsx

Lines changed: 4 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import React, { StrictMode } from "react";
22
import { createRoot } from "react-dom/client";
3+
4+
import posthog from "./src/utils/posthog";
35
import { PostHogProvider } from 'posthog-js/react';
6+
47
import { QueryClientProvider } from '@tanstack/react-query';
58
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
69
import { queryClient } from './src/api/queryClient';
@@ -11,12 +14,8 @@ import "./src/styles/index.scss";
1114
import type * as TExcalidraw from "@excalidraw/excalidraw";
1215

1316
import App from "./src/App";
14-
import { AuthProvider } from "./src/auth/AuthContext";
1517
import AuthGate from "./src/AuthGate";
16-
import ErrorBoundary from "./src/ErrorBoundary";
1718

18-
// PostHog is automatically initialized in ./utils/posthog.ts
19-
import "./src/utils/posthog";
2019

2120
declare global {
2221
interface Window {
@@ -30,15 +29,7 @@ async function initApp() {
3029
const { Excalidraw } = window.ExcalidrawLib;
3130
root.render(
3231
<StrictMode>
33-
34-
<ErrorBoundary>
35-
<PostHogProvider
36-
apiKey={import.meta.env.VITE_PUBLIC_POSTHOG_KEY}
37-
options={{
38-
api_host: import.meta.env.VITE_PUBLIC_POSTHOG_HOST,
39-
}}
40-
>
41-
32+
<PostHogProvider client={posthog}>
4233
<QueryClientProvider client={queryClient}>
4334
<AuthGate>
4435
<App
@@ -51,7 +42,6 @@ async function initApp() {
5142
<ReactQueryDevtools initialIsOpen={false} />
5243
</QueryClientProvider>
5344
</PostHogProvider>
54-
</ErrorBoundary>
5545
</StrictMode>,
5646
);
5747
}

src/frontend/src/App.tsx

Lines changed: 13 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import { useSaveCanvas } from "./api/hooks";
88
import type * as TExcalidraw from "@excalidraw/excalidraw";
99
import type { NonDeletedExcalidrawElement } from "@excalidraw/excalidraw/element/types";
1010
import type { ExcalidrawImperativeAPI, AppState } from "@excalidraw/excalidraw/types";
11-
import AuthModal from "./auth/AuthModal";
1211
import { useAuthCheck } from "./api/hooks";
1312

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

29-
// Get authentication state from React Query
3028
const { data: isAuthenticated, isLoading: isAuthLoading } = useAuthCheck();
31-
32-
// Get user profile for analytics identification
3329
const { data: userProfile } = useUserProfile();
3430

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

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

71-
// Use React Query mutation for saving canvas
7265
const { mutate: saveCanvas } = useSaveCanvas({
7366
onSuccess: () => {
74-
console.debug("Canvas saved to database successfully");
75-
// Track canvas save event with PostHog
76-
capture('canvas_saved');
67+
console.debug("[pad.ws] Canvas saved to database successfully");
7768
},
7869
onError: (error) => {
79-
console.error("Failed to save canvas to database:", error);
80-
// Track canvas save failure
81-
capture('canvas_save_failed', {
82-
error: error instanceof Error ? error.message : 'Unknown error'
83-
});
70+
console.error("[pad.ws] Failed to save canvas to database:", error);
8471
}
8572
});
8673

8774
useEffect(() => {
8875
if (excalidrawAPI) {
8976
(window as any).excalidrawAPI = excalidrawAPI;
90-
// Track application loaded event
9177
capture('app_loaded');
9278
}
9379
return () => {
9480
(window as any).excalidrawAPI = null;
9581
};
9682
}, [excalidrawAPI]);
9783

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

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

10791
const canvasData = {
@@ -110,11 +94,9 @@ export default function App({
11094
files
11195
};
11296

113-
// Compare with last sent data (deep equality via JSON.stringify)
11497
const serialized = JSON.stringify(canvasData);
11598
if (serialized !== lastSentCanvasDataRef.current) {
11699
lastSentCanvasDataRef.current = serialized;
117-
// Use React Query mutation to save canvas
118100
saveCanvas(canvasData);
119101
}
120102
},
@@ -123,12 +105,18 @@ export default function App({
123105
[saveCanvas, isAuthenticated]
124106
);
125107

126-
// Identify user in PostHog when username is available
127108
useEffect(() => {
128-
if (userProfile?.username) {
129-
posthog.identify(userProfile.username);
109+
if (userProfile?.id) {
110+
posthog.identify(userProfile.id);
111+
if (posthog.people && typeof posthog.people.set === "function") {
112+
const {
113+
id, // do not include in properties
114+
...personProps
115+
} = userProfile;
116+
posthog.people.set(personProps);
117+
}
130118
}
131-
}, [userProfile?.username]);
119+
}, [userProfile]);
132120

133121
return (
134122
<>
@@ -141,7 +129,6 @@ export default function App({
141129
{children}
142130
</ExcalidrawWrapper>
143131

144-
{/* AuthModal is now handled by AuthGate */}
145132
</>
146133
);
147134
}

0 commit comments

Comments
 (0)