Skip to content

Commit 7018da1

Browse files
committed
add optional analytics using umami
1 parent efbc08b commit 7018da1

9 files changed

Lines changed: 147 additions & 9 deletions

File tree

README.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,8 @@ Born from the need to master a new split keyboard layout while practicing real c
3737
- Real-time WPM, accuracy, and completion tracking
3838
- Per-session statistics with detailed breakdowns
3939
- Prompt-specific history to track improvement over time
40-
- All data stored locally in IndexedDB—nothing leaves your device
40+
- All personal data stored locally in IndexedDB—nothing leaves your device
41+
- Optional analytics (self-hosted, no personal data) - disabled by default
4142

4243
**Built for Performance**
4344
- Rust WASM engine for blazing-fast diff calculation and scoring
@@ -118,7 +119,8 @@ pnpm build:web
118119
## 🔒 Privacy First
119120

120121
- **100% local** — All typing data, sessions, and progress stay on your device
121-
- **No analytics** — No tracking, cookies, or external requests
122+
- **Optional analytics** — Self-hosted analytics can be enabled to track aggregate usage (visits, runs started/completed)
123+
- **No personal data** — No cookies, no user identification, no external tracking services
122124
- **Offline-ready** — Works without an internet connection once loaded
123125
- **Open source** — Audit the code yourself
124126

apps/web/index.html

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,16 @@
88
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
99
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
1010
<link rel="apple-touch-icon" href="/favicon.svg" />
11+
<script>
12+
// Analytics will be loaded dynamically based on environment variables
13+
if ( typeof __ANALYTICS_ENABLED__ !== 'undefined' && __ANALYTICS_ENABLED__ ) {
14+
const script = document.createElement( 'script' );
15+
script.async = true;
16+
script.src = __ANALYTICS_URL__;
17+
script.setAttribute( 'data-website-id', __ANALYTICS_WEBSITE_ID__ );
18+
document.head.appendChild( script );
19+
}
20+
</script>
1121
</head>
1222

1323
<body class="bg-slate-950 text-slate-100">

apps/web/src/features/session/hooks/usePersistCompletedSession.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,10 @@ export function usePersistCompletedSession({
7272
completedAt: finishedAtEpoch,
7373
attempts: attemptsSnapshot,
7474
});
75+
76+
if (typeof window !== "undefined" && window.umami) {
77+
window.umami.track("run_completed");
78+
}
7579
try {
7680
const event = new CustomEvent("devkeys:session-saved", {
7781
detail: {

apps/web/src/features/session/state/sessionStore.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,10 @@ export const useSessionStore = create<SessionState>((set, get) => ({
8989
const nextStatus = reachedEnd ? "completed" : "active";
9090
const resolvedStatus = isEmpty ? "idle" : becameActive ? "active" : nextStatus;
9191

92+
if (becameActive && typeof window !== "undefined" && window.umami) {
93+
window.umami.track("run_started");
94+
}
95+
9296
set({
9397
typed: trimmed,
9498
attempts,
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
interface Window {
2+
umami?: {
3+
track: (eventName: string, eventData?: Record<string, string | number>) => void;
4+
};
5+
}

apps/web/vite.config.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,10 @@ import react from "@vitejs/plugin-react";
33

44
export default defineConfig({
55
plugins: [react()],
6+
define: {
7+
// Make environment variables available at build time
8+
__ANALYTICS_URL__: JSON.stringify(process.env.VITE_ANALYTICS_URL || "https://analytics.website/script.js"),
9+
__ANALYTICS_WEBSITE_ID__: JSON.stringify(process.env.VITE_ANALYTICS_WEBSITE_ID || "YOUR_WEBSITE_ID"),
10+
__ANALYTICS_ENABLED__: JSON.stringify(process.env.VITE_ANALYTICS_ENABLED === "true"),
11+
},
612
});

docs/01_Overview_Principles.md

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@
1414
**1. Code-native content**
1515
Every prompt is real, idiomatic code—not random words or generic sentences. Practice algorithms, syntax patterns, and framework idioms that you'll actually use in your work.
1616

17-
**2. True privacy**
18-
All data stays on your device. Sessions, metrics, and custom prompts live in IndexedDB. No tracking, no analytics, no external API calls. Your typing data is yours alone.
17+
**2. Privacy-first design**
18+
All personal data stays on your device. Sessions, metrics, and custom prompts live in IndexedDB. Optional anonymous analytics track only aggregate usage patterns (no personal data). Your typing data is yours alone.
1919

2020
**3. Editor-like experience**
2121
The UI borrows from modern IDEs: file-based prompt organization, language switching, syntax highlighting with VS Code themes, and Tab-aware indentation that respects code structure.
@@ -53,11 +53,12 @@ Generic typing trainers use news articles or literature. DevKeys uses code that
5353

5454
### 3. Own your data
5555

56-
Privacy isn't a feature—it's the foundation. Sessions, progress, and custom prompts never leave your device. No servers, no sync, no cloud dependency.
56+
Privacy isn't a feature—it's the foundation. Sessions, progress, and custom prompts never leave your device. Optional anonymous analytics are self-hosted and track only aggregate usage.
5757

5858
**In practice:**
5959
- IndexedDB for local persistence
60-
- No network requests after initial load
60+
- No personal data sent to external services
61+
- Anonymous analytics (if enabled) track only: page views, runs started, runs completed
6162
- Browser devtools provide full data access and export capability
6263
- Clear localStorage/IndexedDB to reset completely
6364

@@ -94,13 +95,14 @@ The app works with the TypeScript evaluator out of the box. When the Rust WASM b
9495

9596
### 7. Offline matters
9697

97-
Once the app loads, it should work indefinitely without network access. Prompts ship with the bundle. Metrics stay local. The only external dependency is the initial HTML/CSS/JS delivery.
98+
Once the app loads, it should work indefinitely without network access. Prompts ship with the bundle. Metrics stay local. Optional analytics are self-hosted and don't affect core functionality.
9899

99100
**In practice:**
100101
- All prompt packs bundled at build time
101102
- No CDN dependencies for fonts or icons
102103
- IndexedDB persists across sessions
103104
- Static deployment (works on any file server)
105+
- Analytics (if enabled) are self-hosted and optional
104106

105107
---
106108

@@ -180,7 +182,7 @@ DevKeys fails if:
180182
1. It requires cloud sync or accounts
181183
2. Keystroke latency becomes noticeable
182184
3. The bundle size grows beyond 500KB (gzipped)
183-
4. Privacy model changes to support growth
185+
4. Privacy model changes to collect personal data
184186
5. Core features require JavaScript frameworks beyond React
185187

186188
---
@@ -205,7 +207,7 @@ These are explicitly **not** goals for DevKeys:
205207
| Feature | DevKeys | Monkeytype | Typing.io | Keybr |
206208
|---------|---------|-----------|-----------|-------|
207209
| Code-first content | ✅ Yes | ❌ No | ✅ Yes | ❌ No |
208-
| Privacy-first | ✅ Local only | ⚠️ Optional account | ❌ Account required | ⚠️ Analytics |
210+
| Privacy-first | ✅ Local + optional anonymous | ⚠️ Optional account | ❌ Account required | ⚠️ Analytics |
209211
| Custom prompts | ✅ Yes | ✅ Yes | ❌ No | ❌ No |
210212
| Syntax highlighting | ✅ Yes | ❌ No | ✅ Yes | ❌ No |
211213
| Open source | ✅ MIT | ✅ GPL-3.0 | ❌ No | ❌ No |

env.example

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# DevKeys Environment Variables
2+
3+
# =============================================================================
4+
# Analytics Configuration (Optional)
5+
# =============================================================================
6+
7+
# Enable/disable analytics (default: false)
8+
# Set to 'true' to enable analytics, 'false' to disable
9+
VITE_ANALYTICS_ENABLED=false
10+
11+
# Analytics script URL (update with your actual analytics domain)
12+
VITE_ANALYTICS_URL=https://website/script.js
13+
14+
# Analytics website ID (get this from your Umami dashboard)
15+
VITE_ANALYTICS_WEBSITE_ID=your-website-id-here
16+

umami-setup.md

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
# Optional Analytics Setup
2+
3+
## Overview
4+
5+
Analytics are **optional** and disabled by default. This guide shows how to enable self-hosted Umami analytics to track:
6+
7+
1. Website visits (automatic pageviews)
8+
2. Run starts (custom event)
9+
3. Run completions (custom event)
10+
11+
Umami is open source, privacy-first (no cookies), and you control all data on your own infrastructure.
12+
13+
**Note**: The production example at `devkeys.app` has analytics enabled to demonstrate the feature. Your self-hosted instance will have analytics disabled by default.
14+
15+
## Deployment Setup
16+
17+
### 1. Deploy Umami
18+
19+
Using your preferred container orchestration platform (Dokploy, Coolify, Docker Compose, etc.):
20+
21+
1. **Create New Project****Docker Compose**
22+
2. Name it "umami-analytics"
23+
3. Paste this docker-compose configuration:
24+
25+
```yaml
26+
services:
27+
umami:
28+
image: ghcr.io/umami-software/umami:postgresql-latest
29+
environment:
30+
DATABASE_URL: postgresql://umami:umami@db:5432/umami
31+
DATABASE_TYPE: postgresql
32+
APP_SECRET: ${APP_SECRET}
33+
depends_on:
34+
db:
35+
condition: service_healthy
36+
restart: always
37+
healthcheck:
38+
test: ["CMD-SHELL", "curl http://localhost:3000/api/heartbeat"]
39+
interval: 5s
40+
timeout: 5s
41+
retries: 5
42+
43+
db:
44+
image: postgres:15-alpine
45+
environment:
46+
POSTGRES_DB: umami
47+
POSTGRES_USER: umami
48+
POSTGRES_PASSWORD: umami
49+
volumes:
50+
- umami-db-data:/var/lib/postgresql/data
51+
restart: always
52+
healthcheck:
53+
test: ["CMD-SHELL", "pg_isready -U umami -d umami"]
54+
interval: 5s
55+
timeout: 5s
56+
retries: 5
57+
58+
volumes:
59+
umami-db-data:
60+
```
61+
62+
4. **Set Environment Variables**:
63+
- Copy `env.example` to `.env` and update the values
64+
- `UMAMI_APP_SECRET` = generate random 32-char string
65+
- `VITE_ANALYTICS_URL` = your analytics domain (e.g., `https://analytics.devkeys.app/script.js`)
66+
- `VITE_ANALYTICS_WEBSITE_ID` = your website ID from Umami dashboard
67+
68+
5. **Configure Domain**: Set up subdomain like `analytics.devkeys.app`
69+
6. **Enable HTTPS**: Most platforms auto-generate SSL via Let's Encrypt
70+
7. **Deploy** and wait for health checks to pass
71+
72+
8. **Initial Setup**:
73+
- Visit `https://analytics.devkeys.app`
74+
- Default login: `admin` / `umami`
75+
- Change password immediately
76+
- Go to Settings → Websites → Add Website
77+
- Enter your main site domain: `devkeys.app`
78+
- Copy the **Website ID** from the tracking code
79+
80+
### 2. Deploy Main App
81+
82+
1. **Create New Project** → **Git Repository**
83+
2. Connect your GitHub repo
84+
3. Platform will auto-detect Vite/React
85+
4. **Build Settings**:
86+
- Build Command: `pnpm install && pnpm build:wasm && pnpm build:web`
87+
- Publish Directory: `apps/web/dist`
88+
5. Configure domain: `devkeys.app`
89+
6. Deploy

0 commit comments

Comments
 (0)