A generic, configurable, embedded analytics component for Convex — any event, any dimension, any domain — with rollup-on-write so reads stay O(1).
import { AnalyticsClient } from "@vllnt/convex-analytics";
import { components } from "./_generated/api";
const analytics = new AnalyticsClient(components.analytics, {
dimensions: ["plan", "country"], // prop keys to roll up on
});
await analytics.track(ctx, "signup", { subjectRef: userId, props: { plan: "pro", country: "FR" } });
const byPlan = await analytics.top(ctx, "signup", "plan"); // [{ value: "pro", count: 1 }]- Generic events — free-string event name, opaque
subjectRef/sessionRef, host-typedprops. - Host-declared dimensions — you pass the prop keys to roll up on; nothing is hardcoded.
- Rollup-on-write — counts are incremented as events land, so
metric/top/timeseriesread in O(1) (backed by@convex-dev/aggregate+@convex-dev/sharded-counter). - Rich verb set —
metric,top,timeseries,uniques,funnel,retention,distribution, plus paginated rawlist. - Configurable —
scope,dimensions,granularities,retentionDays,sampleRate,propsValidator; sensible defaults, zero config required. - Per-session rate limiting + dedupe + sampling built into
track. - Opt-in web preset —
@vllnt/convex-analytics/webadds web dimensions + UA/geo helpers when you want them. - Optional React hooks —
@vllnt/convex-analytics/reactfor reactive aggregate reads. - Zero hardcoded domain — no
v.any(), no baked-in web fields; the host owns meaning and auth.
The core is domain-neutral: it knows nothing about web, mobile, or any vertical.
Web analytics — pageview dimensions (path, referrer, device, browser, os,
country, UTM) plus parseUserAgent / geoFromHeaders / trackPageview helpers —
lives in the opt-in @vllnt/convex-analytics/web preset. You turn it on by passing
webDimensions to the client's dimensions config; otherwise the core ships no web
fields. See docs/web-preset.md.
npm install @vllnt/convex-analytics// convex/convex.config.ts
import { defineApp } from "convex/server";
import analytics from "@vllnt/convex-analytics/convex.config";
const app = defineApp();
app.use(analytics);
export default app;// convex/analytics.ts — track + read, server-side
import { AnalyticsClient } from "@vllnt/convex-analytics";
import { components } from "./_generated/api";
type MyProps = { plan?: string; country?: string };
const analytics = new AnalyticsClient<MyProps>(components.analytics, {
dimensions: ["plan", "country"],
granularities: ["day"],
});
await analytics.track(ctx, "signup", { subjectRef: userId, props: { plan: "pro" } });
const total = await analytics.metric(ctx, "signup");Passed to the AnalyticsClient constructor. All optional — the defaults work with zero config.
| Option | Type | Default | Purpose |
|---|---|---|---|
scope |
string |
"default" |
Multi-tenant partition; isolates one tenant's data. |
dimensions |
string[] |
[] |
Prop keys to roll up on (drives rollup-on-write). Empty = count by event name only. |
granularities |
("minute" | "hour" | "day")[] |
["day"] |
Rollup bucket sizes (minute for short live windows). |
retentionDays |
number |
90 |
Raw-event TTL in days (rollups are kept forever). Applied via configure. |
sampleRate |
number |
1 |
Fraction 0..1 of events to keep at ingest. |
sessionIdleMs |
number |
1800000 |
Idle timeout before a session is closed. Applied via configure. |
propsValidator |
Convex validator | typed scalar record | Optional host validator narrowing props at the boundary. |
scope, dimensions, granularities, and sampleRate apply per call from the client.
retentionDays, sampleRate, and sessionIdleMs are persisted for the prune/close-sessions
crons by calling analytics.configure(ctx, { ... }) once.
| Verb | Kind | Purpose |
|---|---|---|
track(ctx, name, opts) |
mutation | Ingest an event; rollup-on-write + raw event + counter. Returns "tracked" | "dropped" | "duplicate". |
metric(ctx, name, opts) |
query | Total count over a range, optionally filtered by a dimension value. |
top(ctx, name, dimension, opts) |
query | Top values of a dimension (breakdown). |
timeseries(ctx, name, opts) |
query | Bucketed counts over a range. |
uniques(ctx, opts) |
query | DAU / WAU / MAU from subjects. |
funnel(ctx, steps, opts) |
query | Ordered step conversion, keyed by subjectRef. |
retention(ctx, opts) |
query | Cohort return rates by first-seen period. |
distribution(ctx, name, measure, opts) |
query | Histogram of a numeric measure over declared buckets + overflow (with count / sum). |
list(ctx, name, paginationOpts, opts) |
query | Paginated raw events, newest first. |
configure(ctx, opts) |
mutation | Persist cron-relevant config (retention / sampling / idle). |
Full reference: docs/client-sdk.md. REST surface: docs/api-reference.md.
The Convex-native, authoritative in-app aggregate layer — predefined-dimension rollups served
reactively in O(1). It complements a product-analytics warehouse; it doesn't replace one.
Declare the dimensions and measures you'll query and the rollups answer instantly; ad-hoc or
retroactive questions over non-declared dimensions, or beyond the raw-event retention window,
are an export concern. metric / top / timeseries read pre-aggregated rollups (kept forever);
funnel / retention / distribution read raw events in range (index-backed, bounded, TTL-pruned).
import { AnalyticsClient } from "@vllnt/convex-analytics";
import { webDimensions, trackPageview } from "@vllnt/convex-analytics/web";
const analytics = new AnalyticsClient(components.analytics, { dimensions: webDimensions });
await trackPageview(analytics, ctx, {
path: "/pricing",
ua: request.headers.get("user-agent") ?? undefined,
headers: request.headers,
});webDimensions opts the rollups into path / referrer / device / browser / os /
country / UTM; parseUserAgent and geoFromHeaders build those props from a request.
See docs/web-preset.md.
import { useMetric, useTop } from "@vllnt/convex-analytics/react";
import { api } from "../convex/_generated/api";
const signups = useMetric(api.analytics.metric, { name: "signup" });
const byPlan = useTop(api.analytics.top, { name: "signup", dimension: "plan" });Thin reactive wrappers over the host's re-exported query refs (metric, top, timeseries,
uniques). react + convex are optional peer deps; backend-only consumers pull no React.
claude mcp add convex-analytics-mcp \
--env CONVEX_URL=https://your-deployment.convex.cloud \
--env ANALYTICS_API_KEY=your-key7 tools: track, get_metric, get_top, get_timeseries, get_uniques, detect_anomalies,
query_analytics (NL router). See docs/mcp-tools.md.
- Auth-agnostic mount — the host gates access and passes opaque
subjectRef/sessionRefin. - Tables are sandboxed; the host reaches them only through the exported functions.
- REST endpoints require an
x-api-keyheader, compared timing-safe.
See docs/architecture.md.
pnpm testimport analyticsTest from "@vllnt/convex-analytics/test";
import { convexTest } from "convex-test";
const t = convexTest(hostSchema, hostModules);
analyticsTest.register(t);Schema · Client SDK · REST API ·
Web Preset · Multi-Product ·
Architecture · MCP Tools. For AI agents:
llms.txt · llms-full.txt.
See CONTRIBUTING.md.
Built by bntvllnt · bntvllnt.com · X @bntvllnt
Part of the @vllnt Convex component fleet — vllnt.com
If this is useful, sponsor the work.
MIT — see LICENSE.