[@layout]: ../docs.layout.md

# 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

```sh
bun run pmx plugins add analytics
bun run pmx serve
```

The package activates through `package.json`:

```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](./cloudflare.guide#add-analytics-to-static-pages)
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:

```sh
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](../extend/plugins.reference#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:

```sh
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](https://developers.cloudflare.com/durable-objects/api/sqlite-storage-api/)
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

```json
{
  "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](../extend/events.reference) 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:

```yaml
---
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

```sh
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

```sh
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.
