Embedded Convex analytics with rollup-on-write so metric, top, and timeseries reads stay O(1) — no external SaaS required.
npm install @vllnt/convex-analytics@vllnt/convex-analytics stores events and exposes analytical queries inside your Convex backend. Event names, subject references, properties, and dimensions are supplied by your application rather than imposed by a web-only schema.
Track typed event properties and choose the dimensions to aggregate. Query metrics, breakdowns, timeseries, funnels, retention, or raw events. Rollups support aggregate reads; an optional web preset adds web-specific helpers and an optional React entry supports reactive displays.
The host defines event meaning, identity, consent, and access rules. Sampling, retention, and selected dimensions affect interpretation and coverage. This component does not automatically instrument every client or establish causation from a funnel; validate the events your product sends.
Maintained by [bntvllnt](https://bntvllnt.com) at [VLLNT](https://vllnt.com). Find the author on [GitHub](https://github.com/bntvllnt) and [X](https://x.com/bntvllnt).
Install @vllnt/convex-analytics, mount it in convex.config.ts with app.use(analytics), and instantiate AnalyticsClient with the prop keys you want to roll up on. Calling analytics.track() increments rollup counters at write time, so analytics.metric(), analytics.top(), and analytics.timeseries() return pre-aggregated results in O(1) without scanning raw events.
AnalyticsClient exposes analytics.funnel(ctx, steps, opts) for ordered step conversion keyed by subjectRef and analytics.retention(ctx, opts) for cohort return rates by first-seen period. Both read from TTL-pruned raw events backed by indexes, so they stay bounded and do not require a separate data warehouse.
Import webDimensions and trackPageview from @vllnt/convex-analytics/web and pass webDimensions to the AnalyticsClient dimensions config. trackPageview builds path, referrer, device, browser, os, country, and UTM props from request headers using the bundled parseUserAgent and geoFromHeaders helpers, then calls track() on the core client.
@vllnt/convex-analytics ships an MCP server with seven tools including track, get_metric, get_top, get_timeseries, get_uniques, detect_anomalies, and query_analytics. Register it with Claude Code using claude mcp add, passing your CONVEX_URL and an ANALYTICS_API_KEY, and the agent can read and write analytics without direct database access.
@vllnt/convex-analytics uses a rollup-on-write strategy: every call to analytics.track() immediately increments counters per dimension value and time bucket, backed by @convex-dev/aggregate and @convex-dev/sharded-counter. The metric(), top(), and timeseries() queries read those pre-aggregated counters, so their cost is O(1) regardless of how many raw events have been ingested.
@vllnt/convex-analytics provides metric (total count over a range), top (dimension breakdowns), timeseries (bucketed counts), uniques (DAU/WAU/MAU), funnel (ordered step conversion by subjectRef), retention (cohort return rates), distribution (histogram of a numeric measure), and list (paginated raw events). metric, top, and timeseries read pre-aggregated rollups kept forever; funnel, retention, and distribution read TTL-pruned raw events via indexes.
The core of @vllnt/convex-analytics is domain-neutral and has no hardcoded web fields. You declare your own prop keys as dimensions, pass an optional propsValidator to type them at the boundary, and the component rolls up on whatever keys you specify. Web-specific dimensions and helpers are isolated in the opt-in @vllnt/convex-analytics/web preset.
@vllnt/convex-analytics is auth-agnostic: the host application gates access and passes opaque subjectRef and sessionRef values into track(). Tables are sandboxed so the host reaches them only through exported functions. Multi-tenancy is handled with the scope config option, which partitions one tenant's data from another without any additional schema work.
Raw events are pruned after a configurable retentionDays period (default 90 days), enforced by a cron configured via analytics.configure(). Rollup counters for metric(), top(), and timeseries() are kept indefinitely and are not affected by raw event pruning. Queries like funnel() and retention() that rely on raw events are bounded by the retention window.