Analytics

Analytics records public content use and selected activity. Its CLI reports persisted observations and refreshes automatically in an interactive terminal. Counts retain their browser, server, or Action origin.

Enable collection

bun run pmx plugins add analytics
bun run pmx serve

The package activates through package.json:

{
  "pathmx": {
    "plugins": {
      "@pathmx/analytics": {}
    }
  }
}

Defaults collect public page views and definition-selected Events, keep Actor attribution off, and retain observations indefinitely. Development, authoring, inspection, and export itself do not collect. Collection requires a live production SQL host. Plain static exports receive no collector client; a Cloudflare publication with an Analytics server includes the ordinary collector and a durable same-origin receiver.

Plugin databases live in db/ beside paths/, including db/analytics.sqlite. Ignore db/ in Git and include it in backups. To use persistent storage outside a checkout, give both the serving process and administrative commands the same path:

PATHMX_DB_DIR=/srv/course-db bun run pmx serve
PATHMX_DB_DIR=/srv/course-db bun run pmx analytics status

Relative values resolve against the project root. Programmatic Bun hosts can supply stateRoot, which takes precedence over PATHMX_DB_DIR; portable hosts supply their resolved database location. Databases stay outside Sources and disposable output. See SQL storage.

Report from a static Cloudflare site

Enable the Analytics Plugin and select servers: ["analytics"] in the Cloudflare Plugin. The HTML remains static. The generated Worker receives observations and stores them in a SQLite-backed Durable Object, separate from build artifacts. No Bun server or second Analytics implementation is needed.

Set PATHMX_INSPECT_TOKEN in the deployed Worker's secrets and in the local reporting process. Keep the value outside Sources, Git, and build output. Use the same CLI report and filter syntax against the deployed origin:

bun run pmx analytics report --remote https://guide.example.com --since 1d --by path --once
bun run pmx analytics report --remote https://guide.example.com --interval hour --watch --format jsonl
bun run pmx analytics export --remote https://guide.example.com > observations.jsonl

Remote reports use deployed Event definitions and include retained coverage, persisted health, publication identity, and a Cloudflare recovery bookmark. Reporting and export require the secret; there is no anonymously readable statistics route. status, events, tail, and prune remain local-host commands.

Exports use bounded 500-row pages with a fixed upper sequence and time range. New observations cannot enter an in-progress export. A publication change or retention deletion that changes the snapshot makes the CLI fail rather than silently accepting an incomplete backup. Keep the output only when the command exits successfully. JSONL exports are portable observation records, not SQLite files; they do not back up the host's migration ledger. Cloudflare's SQLite point-in-time recovery provides full database recovery through its owning Durable Object. Retain the reported bookmark and the deployed artifacts when preparing a recovery.

The Cloudflare receiver uses the same admission, policy, Event validation, and UUID deduplication as the Bun host. It bounds each request to 20 observations / 32 KiB and in-flight persistence to 1,000 observations / 4 MiB, with a limit of 100 collection requests per second per Plugin object. It persists the small batch before returning 202; storage failure returns 503. Browser delivery is still best effort, with no offline queue or retries. A stale publication returns 409 so that old clients cannot silently collect against changed policy or Source metadata. Positive retention uses durable alarms and deletes at most 500 rows per minute; unlimited retention schedules no maintenance alarm. Changed health counters, including rate-limit rejections, are coalesced into a checkpoint after one second while the receiver is awake. Reports checkpoint current counters before reading them. These operational counters are best effort: an abrupt restart or storage failure can lose changes since the last successful checkpoint; committed observations remain durable. Invalid report queries return 400; storage failures return 503.

Choose a collection policy

{
  "pathmx": {
    "plugins": {
      "@pathmx/analytics": {
        "retainDays": 90,
        "actors": true,
        "events": {
          "action.completed": true,
          "page.view": false
        }
      }
    }
  }
}

events overrides each registered definition's default. false disables collection; true permits counting and the definition's declared scalar properties. Unknown Event names are configuration errors. Enable only Events supplied by your installed Plugins. Event definitions own their safe properties.

retainDays is a positive integer or null. Omitted/null keeps data indefinitely; zero is invalid. Positive retention deletes expired rows in bounded background work. A Source can opt out of page views and observations scoped to it:

---
analytics: false
---

Repository policy cannot bypass Source exclusion or public admission.

actors: true attributes collected Action Events using the trusted Action Actor. Browser and plugin-only server observations remain anonymous. The setting enables neither additional Event names nor private page views. Turning it off affects future rows; existing attributed history remains until pruned. Actor IDs are the canonical paths recorded at collection time. Moving or deleting an Actor Source does not rewrite that history or join old and new identities.

Understand page views

A public top-level page counts once after initial display or a completed navigation, including reload and Back/Forward. Hash changes, query-only changes on the same canonical path, aborted navigation, live patches, embedded Sources, and iframe display add no page views. Background prerendering waits for activation.

The client generates an ephemeral UUID for delivery deduplication. It collects no visitor or session identifier. Explicit browser observations use the same optional observe() API and remain subject to Source admission. Browser signals can be blocked or forged; redirect visits can include bots. Counts measure stored observations and do not establish unique people, personal funnels, or attendance.

Report and follow activity

bun run pmx analytics status
bun run pmx analytics events
bun run pmx analytics report --since 7d
bun run pmx analytics report --event page.view --by path --since 30d
bun run pmx analytics report --event action.completed --by action --interval day
bun run pmx analytics tail --limit 20
bun run pmx analytics report --once --format json
bun run pmx analytics report --watch --format jsonl

status shows the database, policy, retained coverage, size, and last persisted health counters. Those counters describe the last stored host report; a separate CLI cannot infer current queue state from the file. events includes enabled and disabled definitions, available dimensions, and zero-count definitions.

report defaults to the last seven rolling days and 20 groups, separated by Event and producer kind. --by selects a declared property or the canonical path or source context. --where dimension=value adds an exact filter; repeat it for multiple filters. --interval hour|day|week adds UTC buckets, with weeks beginning Monday. --limit accepts 1–1,000 results.

--since accepts durations such as 7d or ISO dates/times. --until is exclusive; an explicit end fixes the range. Date-only values mean midnight UTC. JSON reports include the effective range, retained coverage, metric, dimensions, and rows. Historical paths remain reportable after a Source is deleted or retargeted.

Interactive reports refresh automatically; piped reports default to a finite snapshot. --once explicitly selects a snapshot. --watch --format jsonl streams one timestamped report per refresh. tail follows committed storage sequence even when piped, unless --once is supplied. Refreshes show persisted rows.

Inspect or remove Actor history

bun run pmx analytics tail --actor /users/ada.user --since 30d --once
bun run pmx analytics report --actor /users/ada.user --since 30d --once
bun run pmx analytics export --actor /users/ada.user --format jsonl > ada-events.jsonl
bun run pmx analytics prune --actor /users/ada.user
bun run pmx analytics prune --actor /users/ada.user --yes

Actor filters match the exact historical ID, even if its Source no longer exists. A timeline covers only selected Action Events collected while attribution was enabled. Anonymous observations cannot be attributed retroactively.

export streams a finite JSONL snapshot with a fixed storage high-water mark and read transaction. prune previews the matching row count; --yes performs bounded deletes. To prune by age, use --before 2026-06-01. An Actor-only prune covers all matching attributed rows. Neither pruning nor request handling runs VACUUM.

These commands operate on the current environment's database. Use existing private host administration to run them on a deployed site, or inspect a restored backup. There is no public statistics endpoint.

Delivery and recovery

Collection is best effort. The browser buffers at most 20 observations for 100 ms, with batches limited to 32 KiB. It uses keepalive requests, without offline storage or retries. The Bun host admits at most 100 collection requests per second per process and queues at most 1,000 observations / 4 MiB. It writes at most 50 rows per transaction.

The Bun collector returns 202 for accepted work, 400 for invalid batches, 403 for an invalid origin, 429 for rate/queue limits, and 503 when unavailable. Acceptance is not a durable-write acknowledgment. Overflow drops new work; SQL errors and shutdown abandonment are logged and included in health where persistence is available. Domain Actions and navigation remain independent of collector success.

The normal-load target is durable queryability within one second. Network delay, blocking, and disconnection can prevent delivery. Shutdown allows up to two seconds for pending flush; an executing database call may finish later.

Back up SQLite with its owning process stopped or use a SQLite-safe snapshot. Preserve any active WAL alongside the database. Stop database connections before restoring files, retain the original backup, and verify both records and policy after restoration. Removing .pathmx/ does not remove db/ records.