Skip to content

Add analytics dashboard: DAU, viral metrics, chat ratings, floating bar usage#6404

Merged
kodjima33 merged 2 commits into
mainfrom
worktree-floating-bar-feedback
Apr 7, 2026
Merged

Add analytics dashboard: DAU, viral metrics, chat ratings, floating bar usage#6404
kodjima33 merged 2 commits into
mainfrom
worktree-floating-bar-feedback

Conversation

@kodjima33
Copy link
Copy Markdown
Collaborator

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

kodjima33 and others added 2 commits April 7, 2026 17:33
…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>
@kodjima33 kodjima33 merged commit 32bf23d into main Apr 7, 2026
3 checks passed
@kodjima33 kodjima33 deleted the worktree-floating-bar-feedback branch April 7, 2026 21:33
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Apr 7, 2026

Greptile Summary

This 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.

  • Stale token after 1 hour: the ID token is fetched once at mount and cached in React state; after 1 hour the cached token expires and all SWR fetches silently fail with 401 — a periodic getIdToken(true) refresh is required.
  • Negative text_queries: derived by subtracting PTT-ended events from total query events across two independent PostHog streams; on any day where the voice event count exceeds the total, the value goes negative and corrupts the stacked bar chart.

Confidence Score: 3/5

Two 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

Filename Overview
web/admin/app/(protected)/dashboard/analytics/page.tsx Token refresh on expiry is missing; cached token becomes stale after 1 hour, breaking all auth-gated SWR fetches.
web/admin/app/api/omi/stats/floating-bar-usage/route.ts text_queries can go negative when voice event count exceeds total queries; dead overallAvgPerUser variable present.
web/admin/app/api/omi/stats/viral-metrics/route.ts Eight parallel PostHog queries with correct growth-accounting math; module-level cache is per-instance only.
web/admin/app/api/omi/stats/dau-trends/route.ts Clean HogQL daily-DAU query with safe days clamping, proper auth gating, and complete date-series backfill.
web/admin/app/api/omi/stats/message-ratings/route.ts Clean single-query implementation; ratio correctly clamped; proper auth and error handling throughout.

Sequence Diagram

sequenceDiagram
    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
Loading

Reviews (1): Last reviewed commit: "Update analytics dashboard with DAU, vir..." | Re-trigger Greptile

Comment on lines +223 to +227
useEffect(() => {
if (user) {
user.getIdToken().then(setToken).catch(() => setToken(null));
}
}, [user]);
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.

P1 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:

Suggested change
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]);

Comment on lines +101 to +113
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,
};
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.

P1 text_queries can go negative

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:

Suggested change
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;

Comment on lines +117 to +124
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;
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.

P2 Dead variable overallAvgPerUser

overallAvgPerUser is computed on line 122 but is never referenced in the result object returned in the response. The response on line 133 uses its own separate inline expression instead. The dead variable can be removed to avoid confusion.

Comment on lines +6 to +8
// 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;
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.

P2 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.

Glucksberg pushed a commit to Glucksberg/omi-local that referenced this pull request Apr 28, 2026
…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)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant