Add analytics dashboard: DAU, viral metrics, chat ratings, floating bar usage#6404
Conversation
…e API endpoints Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ating bar usage charts Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Greptile SummaryThis PR adds four PostHog-backed API routes (DAU trends, viral metrics, chat ratings, floating bar usage) and wires the admin analytics page to use Firebase ID token auth for all API calls. The visualization and data-aggregation logic is largely sound, but two defects need attention before merging.
Confidence Score: 3/5Two P1 defects — stale token expiry after 1 hour and possible negative text_queries values — should be fixed before merging. The stale-token bug will silently break the entire dashboard for any admin session open longer than 1 hour, and the text_queries negativity will corrupt floating-bar chart data. Both are straightforward one-line or few-line fixes but represent real, present defects on the changed code path. page.tsx (token refresh interval missing), floating-bar-usage/route.ts (text_queries floor missing) Important Files Changed
Sequence DiagramsequenceDiagram
participant Browser as Analytics Page
participant Firebase as Firebase SDK
participant API as Next.js API Routes
participant PH as PostHog HogQL
Browser->>Firebase: getIdToken() on mount
Firebase-->>Browser: ID Token (valid 1h)
Browser->>API: GET /api/omi/stats/* Bearer token
API->>API: verifyAdmin() → adminData collection check
API->>PH: HogQL query (DAU / viral / ratings / usage)
PH-->>API: results[]
API->>API: transform + module-level cache (30min, per-instance)
API-->>Browser: JSON response
Browser->>Browser: SWR renders Recharts charts
Note over Browser,Firebase: ⚠️ After 1h token expires;<br/>useEffect won't re-run since<br/>user reference is unchanged
Reviews (1): Last reviewed commit: "Update analytics dashboard with DAU, vir..." | Re-trigger Greptile |
| useEffect(() => { | ||
| if (user) { | ||
| user.getIdToken().then(setToken).catch(() => setToken(null)); | ||
| } | ||
| }, [user]); |
There was a problem hiding this comment.
Stale token after 1-hour expiry
The Firebase ID token is fetched once in a useEffect that only re-runs when user changes. Firebase tokens expire after 1 hour; after that the cached token state becomes invalid and every subsequent SWR fetch receives a 401 — no recovery occurs because user hasn't changed and the effect won't re-run. A periodic refresh keeps the stored token valid for long-lived admin sessions:
| useEffect(() => { | |
| if (user) { | |
| user.getIdToken().then(setToken).catch(() => setToken(null)); | |
| } | |
| }, [user]); | |
| useEffect(() => { | |
| if (user) { | |
| user.getIdToken().then(setToken).catch(() => setToken(null)); | |
| const id = setInterval(() => { | |
| user.getIdToken(true).then(setToken).catch(() => setToken(null)); | |
| }, 55 * 60 * 1000); | |
| return () => clearInterval(id); | |
| } | |
| }, [user]); |
| const daily = totalRows.map(([date, totalQueries, uniqueUsers]) => { | ||
| const d = date.slice(0, 10); | ||
| const voice = voiceByDate[d] || 0; | ||
| const text = totalQueries - voice; | ||
| const avgPerUser = uniqueUsers > 0 ? Math.round((totalQueries / uniqueUsers) * 10) / 10 : 0; | ||
| return { | ||
| date: d, | ||
| total_queries: totalQueries, | ||
| text_queries: text, | ||
| voice_queries: voice, | ||
| unique_users: uniqueUsers, | ||
| avg_per_user: avgPerUser, | ||
| }; |
There was a problem hiding this comment.
text = totalQueries - voice derives text queries by subtracting PTT-ended events from total query events. These two values come from independent PostHog event streams (floating_bar_query_sent vs floating_bar_ptt_ended with had_transcript=true). If the voice count for a given day exceeds totalQueries — possible due to event timing skew or replay — text_queries is negative, corrupting the stacked bar chart and the totalText summary. Clamp to zero:
| const daily = totalRows.map(([date, totalQueries, uniqueUsers]) => { | |
| const d = date.slice(0, 10); | |
| const voice = voiceByDate[d] || 0; | |
| const text = totalQueries - voice; | |
| const avgPerUser = uniqueUsers > 0 ? Math.round((totalQueries / uniqueUsers) * 10) / 10 : 0; | |
| return { | |
| date: d, | |
| total_queries: totalQueries, | |
| text_queries: text, | |
| voice_queries: voice, | |
| unique_users: uniqueUsers, | |
| avg_per_user: avgPerUser, | |
| }; | |
| const voice = Math.min(voiceByDate[d] || 0, totalQueries); | |
| const text = totalQueries - voice; |
| const totalAllQueries = daily.reduce((s, d) => s + d.total_queries, 0); | ||
| const totalAllVoice = daily.reduce((s, d) => s + d.voice_queries, 0); | ||
| const totalAllText = daily.reduce((s, d) => s + d.text_queries, 0); | ||
| const totalAllUsers = daily.reduce((s, d) => s + d.unique_users, 0); | ||
| const activeDays = daily.filter((d) => d.total_queries > 0).length; | ||
| const overallAvgPerUser = activeDays > 0 && totalAllUsers > 0 | ||
| ? Math.round((totalAllQueries / (totalAllUsers / activeDays)) * 10) / 10 | ||
| : 0; |
There was a problem hiding this comment.
| // Module-level cache (30 min TTL) | ||
| let cache: { data: { date: string; dau: number }[]; days: number; timestamp: number } | null = null; | ||
| const CACHE_TTL = 30 * 60 * 1000; |
There was a problem hiding this comment.
Module-level cache is per-serverless-instance
The module-level cache variable is reset on every cold start, so in a multi-instance or auto-scaled deployment (e.g., Vercel) each new instance will re-fetch from PostHog. The comment "Module-level cache (30 min TTL)" implies shared caching — consider adding a note that this is in-process only. The same pattern appears in viral-metrics, message-ratings, and floating-bar-usage.
…ar usage (BasedHardware#6404) ## Summary - Added 4 new API endpoints to web/admin: dau-trends, viral-metrics, message-ratings, floating-bar-usage - Updated analytics page with full chart suite from omi-admin: growth accounting, stickiness, power user curve, activation rate, chat response ratings, floating bar voice/text usage, avg queries per user - All endpoints use verifyAdmin auth and PostHog HogQL queries - Fixed auth: analytics page now uses Firebase ID token for API calls ## Test plan - [ ] Navigate to admin.omi.me/dashboard/analytics - [ ] Verify MRR, subscriptions, conversations load - [ ] Scroll to macOS Growth Metrics section — verify DAU, ratings, usage charts - [ ] Verify retention cohort heatmap renders 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Summary
Test plan
🤖 Generated with Claude Code