Skip to content

Commit f8a1ea8

Browse files
committed
fix: use async iteration for stats queries to avoid 16MB read limit
1 parent 9c59f9d commit f8a1ea8

5 files changed

Lines changed: 61 additions & 30 deletions

File tree

changelog.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ Format based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
2121
- Hardened public stats page rendering
2222
- Added a local error boundary so stats failures do not blank the app
2323
- Guarded growth chart against invalid data points
24+
- Fixed stats page exceeding 16MB Convex read limit
25+
- Changed publicMessageCount and publicMessageGrowth queries to use async iteration instead of collect
26+
- Added temporary refresh button to stats header (to be removed later)
2427

2528
### Added
2629

convex/analytics.ts

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -634,19 +634,22 @@ export const summaryStats = query({
634634
},
635635
});
636636

637-
// TEMP: Public message count for milestone display on login page
638-
// TODO: Remove when no longer needed
637+
// TEMP: Public message count for milestone display on stats page
638+
// Uses async iteration to avoid collecting all documents into memory
639639
export const publicMessageCount = query({
640640
args: {},
641641
returns: v.number(),
642642
handler: async (ctx) => {
643-
const messages = await ctx.db.query("messages").collect();
644-
return messages.length;
643+
let count = 0;
644+
for await (const _ of ctx.db.query("messages")) {
645+
count += 1;
646+
}
647+
return count;
645648
},
646649
});
647650

648-
// TEMP: Public message growth data for animated chart on login page
649-
// TODO: Remove when no longer needed
651+
// TEMP: Public message growth data for animated chart on stats page
652+
// Uses pagination to avoid exceeding read limits
650653
export const publicMessageGrowth = query({
651654
args: {},
652655
returns: v.array(
@@ -657,20 +660,18 @@ export const publicMessageGrowth = query({
657660
})
658661
),
659662
handler: async (ctx) => {
660-
const messages = await ctx.db.query("messages").collect();
661-
662-
// Group messages by date (skip invalid dates)
663+
// Group messages by date using async iteration (no collect)
663664
const byDate: Record<string, number> = {};
664-
for (const msg of messages) {
665+
666+
for await (const msg of ctx.db.query("messages")) {
665667
// Safety check: skip messages with invalid createdAt
666668
if (!msg.createdAt || typeof msg.createdAt !== "number") continue;
667669
try {
668670
const dateObj = new Date(msg.createdAt);
669-
if (isNaN(dateObj.getTime())) continue; // Skip invalid dates
671+
if (isNaN(dateObj.getTime())) continue;
670672
const date = dateObj.toISOString().split("T")[0];
671673
byDate[date] = (byDate[date] || 0) + 1;
672674
} catch {
673-
// Skip any messages that cause date parsing errors
674675
continue;
675676
}
676677
}

files.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ Backend functions and schema.
2929
| `users.ts` | User queries/mutations: getOrCreate, me (returns enabledAgents), stats, API key management, updateEnabledAgents, deleteAllData (parallel deletes for all 7 tables: parts, messages, sessions, sessionEmbeddings, messageEmbeddings, dailyWrapped, apiLogs), deleteAccount (deletes Convex first, then WorkOS to prevent partial deletion) |
3030
| `sessions.ts` | Session CRUD: list, get, getPublic, setVisibility, remove, getMarkdown, upsert (with 10s dedup window, idempotency), batchUpsert, listExternalIds, exportAllDataCSV |
3131
| `messages.ts` | Message mutations: upsert (with 5s dedup, combined session patch, parallel parts ops), batchUpsert for bulk sync |
32-
| `analytics.ts` | Analytics queries with source filtering: dailyStats, modelStats, projectStats, providerStats, summaryStats, sessionsWithDetails, sourceStats, publicPlatformStats (no auth, for homepage leaderboard), publicMessageCount (no auth, total message docs for /stats page), publicMessageGrowth (no auth, daily growth with cumulative totals for /stats page, includes safety checks for invalid dates). Includes inferProvider helper with OAuth provider normalization (antigravity-oauth to google, anthropic-oauth to anthropic) and model-based provider detection (including Codex models mapped to openai) |
32+
| `analytics.ts` | Analytics queries with source filtering: dailyStats, modelStats, projectStats, providerStats, summaryStats, sessionsWithDetails, sourceStats, publicPlatformStats (no auth, for homepage leaderboard), publicMessageCount (no auth, uses async iteration for large datasets), publicMessageGrowth (no auth, daily growth with cumulative totals, uses async iteration to avoid 16MB read limit). Includes inferProvider helper with OAuth provider normalization (antigravity-oauth to google, anthropic-oauth to anthropic) and model-based provider detection (including Codex models mapped to openai) |
3333
| `search.ts` | Full-text and semantic search: searchSessions, searchSessionsPaginated, searchMessages, searchMessagesPaginated, semanticSearch, hybridSearch, semanticSearchMessages, hybridSearchMessages |
3434
| `embeddings.ts` | Vector embedding generation with idempotency checks and replace pattern for session-level and message-level semantic search |
3535
| `evals.ts` | Eval management: setEvalReady, listEvalSessions (with factoryDroid stats), getEvalTags, generateEvalExport (DeepEval JSON, OpenAI JSONL, Filesystem formats) |
@@ -57,7 +57,7 @@ React frontend application.
5757
| File | Description |
5858
|------|-------------|
5959
| `Login.tsx` | Public homepage with WorkOS AuthKit integration. Shows "Go to Dashboard" button when logged in (no auto-redirect), "Sign in" when logged out. Includes privacy messaging, "Syncs with" section showing supported CLI tools (OpenCode, Claude Code, Droid, Codex CLI, Cursor with coming soon badge), getting started section with plugin links including codex-sync (mobile-visible), tan mode theme support with footer (theme switcher, Terms/Privacy links), footer icons (GitHub, Discord, Support, Discussions), updated mockup with view tabs and OC/CC source badges (desktop-only), feature list with Sync/Search/Private/Tag/Export/Delete keywords and eval datasets tagline, Watch the demo link, trust message with cloud/local deployment info, real-time Platform Stats leaderboard (Top Models, Top CLI) above Open Source footer link |
60-
| `Stats.tsx` | Public stats page (/stats) with platform statistics isolated from homepage. Includes MessageMilestoneCounter (daily 9am PT snapshot with dynamic milestones, progress bar), AnimatedGrowthChart (SVG chart with play/reset animation showing cumulative message growth over days, dynamic Y-axis scaling, X-axis date labels), plus a local error boundary and data guards so chart failures do not blank the app. Theme toggle, back link to homepage. No auth required. |
60+
| `Stats.tsx` | Public stats page (/stats) with platform statistics isolated from homepage. Includes MessageMilestoneCounter (daily 9am PT snapshot with dynamic milestones, progress bar), AnimatedGrowthChart (SVG chart with play/reset animation showing cumulative message growth over days, dynamic Y-axis scaling, X-axis date labels), plus a local error boundary and data guards so chart failures do not blank the app. Theme toggle, temp refresh button, back link to homepage. No auth required. |
6161
| `Dashboard.tsx` | Main dashboard with custom themed source filter dropdown (filters by user's enabled agents from Settings, dark/tan mode support, click-outside close, escape key close), source badges (CC/OC/FD), eval toggle button, Context link with search icon, setup banner for new users with 3-column plugin cards (OpenCode, Claude Code, Factory Droid), mobile-optimized header/filters/session rows, URL param support for deep linking from Context search (?session=id), Cmd/Ctrl+K shortcut to open Context search, MessageBubble with content normalization helpers (getPartTextContent, getToolName) for multi-plugin format support, flash-free session transitions using lastValidSessionRef cache (shows cached content until new data loads, subtle corner spinner), and five views: Overview (responsive stat grids), Sessions (mobile-friendly list with stacked layout), Evals (eval-ready sessions with export modal), Analytics (responsive breakdowns with collapsible filters), Wrapped (Daily Sync Wrapped visualization) |
6262
| `Settings.tsx` | Tabbed settings: API Access (two-column layout with Plugin Setup and AI Coding Agents sections, keys, endpoints with plugin links including codex-sync), Profile (collapsible section for privacy, account info, Legal section with Terms/Privacy links, Danger Zone with delete data/account options). AI Coding Agents section lets users enable/disable CLI tools for the source filter dropdown (OpenCode, Claude Code, Factory Droid, Codex CLI with supported status, Cursor, Continue, Amp, Aider, Goose, Mentat, Cline, Kilo Code). Back link navigates to /dashboard |
6363
| `Docs.tsx` | Comprehensive documentation page with instant typeahead search (Cmd/Ctrl+K shortcut), left sidebar navigation (hidden scrollbar) with npm links (opencode-sync-plugin, claude-code-sync, codex-sync), right table of contents, anchor tags, copy/view as markdown buttons, mobile responsive, works with both dark/tan themes. Search indexes all sections with keywords for quick navigation to any topic via hash anchor. Covers use hosted version (with features, 3-plugin install cards with OC/CC/CX badges, login/sync for all three plugins), self-hosting requirements with cloud and 100% local deployment options, quick start (3-plugin cards), dashboard features, OpenCode plugin, Claude Code plugin, Codex CLI plugin with full documentation sections, API reference, search types, authentication, hosting, fork guide, troubleshooting, and FAQ. Links to opencode.ai, claude.ai, and openai.com/codex in hero and plugin sections. |

src/pages/Stats.tsx

Lines changed: 39 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
Play,
1212
RotateCcw,
1313
ArrowLeft,
14+
RefreshCw,
1415
} from "lucide-react";
1516

1617
// Helper to format large numbers
@@ -661,7 +662,14 @@ type StatsPageContentProps = {
661662

662663
function StatsPageContent({ theme, setTheme }: StatsPageContentProps) {
663664
const isDark = theme === "dark";
664-
const refreshToken = useDailyRefreshAt9amPT();
665+
const dailyRefreshToken = useDailyRefreshAt9amPT();
666+
// TEMP: Manual refresh button - remove later
667+
const [manualRefresh, setManualRefresh] = useState(0);
668+
const refreshToken = dailyRefreshToken + manualRefresh;
669+
670+
const handleManualRefresh = () => {
671+
setManualRefresh((prev) => prev + 1);
672+
};
665673

666674
return (
667675
<div
@@ -699,21 +707,36 @@ function StatsPageContent({ theme, setTheme }: StatsPageContentProps) {
699707
</h1>
700708
</div>
701709

702-
{/* Theme toggle */}
703-
<button
704-
onClick={() => setTheme(isDark ? "tan" : "dark")}
705-
className={`p-2 rounded-md ${
706-
isDark
707-
? "text-zinc-400 hover:text-zinc-200 hover:bg-zinc-800"
708-
: "text-[#8b7355] hover:text-[#1a1a1a] hover:bg-[#e6e4e1]"
709-
}`}
710-
>
711-
{isDark ? (
712-
<Sun className="h-4 w-4" />
713-
) : (
714-
<Moon className="h-4 w-4" />
715-
)}
716-
</button>
710+
<div className="flex items-center gap-2">
711+
{/* TEMP: Manual refresh button - remove later */}
712+
<button
713+
onClick={handleManualRefresh}
714+
className={`p-2 rounded-md ${
715+
isDark
716+
? "text-zinc-400 hover:text-zinc-200 hover:bg-zinc-800"
717+
: "text-[#8b7355] hover:text-[#1a1a1a] hover:bg-[#e6e4e1]"
718+
}`}
719+
title="Refresh stats"
720+
>
721+
<RefreshCw className="h-4 w-4" />
722+
</button>
723+
724+
{/* Theme toggle */}
725+
<button
726+
onClick={() => setTheme(isDark ? "tan" : "dark")}
727+
className={`p-2 rounded-md ${
728+
isDark
729+
? "text-zinc-400 hover:text-zinc-200 hover:bg-zinc-800"
730+
: "text-[#8b7355] hover:text-[#1a1a1a] hover:bg-[#e6e4e1]"
731+
}`}
732+
>
733+
{isDark ? (
734+
<Sun className="h-4 w-4" />
735+
) : (
736+
<Moon className="h-4 w-4" />
737+
)}
738+
</button>
739+
</div>
717740
</div>
718741
</header>
719742

task.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,10 @@ OpenSync supports two AI coding tools: **OpenCode** and **Claude Code**.
3939
- [x] Switched stats to daily snapshots at 9am PT
4040
- Loads once from the database (no realtime updates)
4141
- Shows daily update status in the UI
42+
- [x] Fixed 16MB Convex read limit error on stats page
43+
- Changed publicMessageCount to use async iteration instead of collect
44+
- Changed publicMessageGrowth to use async iteration instead of collect
45+
- Added temporary refresh button to stats header (to remove later)
4246
- [x] Added new Convex queries in analytics.ts
4347
- publicMessageCount: returns total message documents (no auth)
4448
- publicMessageGrowth: returns daily counts with cumulative totals (no auth)

0 commit comments

Comments
 (0)