Stats
Query daily and hourly time-series rollups and trigger backfills from the terminal.
Time-Series Rollups are pre-aggregated daily and hourly count tables for the four event-shaped data sources — events / metric_events / funnel_events / questionnaire_responses. They power the subtle sparkline charts on dashboard cards and arbitrary date-range trend queries that survive raw-event retention pruning.
Kinds: events, users, sessions, metric_completions, funnel_completions, questionnaire_responses. Grains: daily, hourly. Buckets are UTC; the in-progress bucket is excluded by default so a partial day or hour can't render as a misleading dip. Reads always hit the project-rollup row (one row per (project, is_dev, bucket)) unless a single-app filter is supplied — never a SUM across apps at query time.
Two background jobs maintain the tables — stats_aggregate_hourly at :05 UTC and stats_aggregate_daily at 00:30 UTC, each re-aggregating the trailing 3 buckets to absorb late-arriving events from offline SDKs. Historical backfills (e.g. first-time rollout or repair after a gap) are an operator action — run pnpm backfill in the monorepo against the production DB. It re-aggregates the trailing 365 days using DELETE+INSERT … SELECT … GROUP BY in a single transaction per bucket range, so re-running over the same window is idempotent and stale rows never survive. unique_users and unique_sessions are per-bucket distincts and non-additive across buckets — sparkline value lines plot the additive count columns.
Query a series
owlmetry stats <kind> <grain> (--project-id <id> | --team-id <id>) \
[--app-id <id>] [--days <n> | --hours <n>] [--from <iso> --to <iso>] \
[--data-mode <mode>] [--slug <slug>] [--include-current]<kind> is one of events, users, sessions, metric_completions, funnel_completions, questionnaire_responses. <grain> is daily or hourly. The default rendering is a bar-graph table showing each bucket's value alongside a trend bar; pass --format json for the structured StatsBucketedResponse.
| Flag | Required | Description |
|---|---|---|
--project-id <id> | One of | Project UUID. Mutually exclusive with --team-id |
--team-id <id> | One of | Team UUID — aggregates across every project in the team |
--app-id <id> | No | Narrow to one app within the chosen scope |
--days <n> | No | Trailing UTC days for grain=daily (default 30, max 365). Ignored when --from/--to are set |
--hours <n> | No | Trailing UTC hours for grain=hourly (default 24, max 2160). Ignored when --from/--to are set |
--from <iso> | No | Explicit start (YYYY-MM-DD for daily, ISO 8601 for hourly). Pair with --to |
--to <iso> | No | Explicit end (inclusive at the bucket level) |
--data-mode <mode> | No | production / development / all. Defaults to production |
--slug <slug> | No | Narrow metric_completions / funnel_completions to one definition |
--include-current | No | Include the in-progress bucket (defaults to excluding it) |
events, users, and sessions all read the same events_{daily,hourly} tables — the kind selects which column the value series follows. metric_completions filters to phase='complete'. funnel_completions filters to each funnel's terminal step (resolved from funnel_definitions at the route layer).
Backfill a date range
Backfilling the rollup tables is an operator action with no public CLI/API surface — it runs server-side as a one-shot script directly against the database. From the monorepo on the production VPS:
pnpm backfillRe-aggregates the trailing 365 days for every project in a single pass. Idempotent: re-running over the same window replaces existing rollup rows with freshly computed values, so it's safe to invoke as often as needed. Aggregation tables are explicitly excluded from retention pruning, so historical buckets stay queryable indefinitely.
