glueful / thallo-analytics
Product-analytics fact store for Thallo: consumes lifecycle events, owns its own facts, as a removable capability pack.
Requires
- php: ^8.3
- glueful/extension-contracts: *
- glueful/framework: ^1.65.0
- glueful/thallo-contracts: v1.0.0-beta.22
Requires (Dev)
None
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-09-12 23:38:40 UTC
README
A self-contained product-analytics fact store for Thallo. It consumes content, collection, and auth lifecycle events, owns its own privacy-minimized facts (raw facts
- daily rollups + distinct-actor presence), and exposes a gated admin read API — packaged as a
removable capability pack that depends only on the framework and
glueful/thallo-contracts.
It answers "what's happening in the content/data" and "who's using it," and is deliberately distinct
from two things that already capture data: the framework's metrics capability (raw HTTP traffic —
ops, not product) and audit_logs (immutable per-action forensics). Analytics stores aggregated
trends; audit and analytics are independent consumers of the same pure events.
What it provides
- Three tables (hybrid model — raw facts are canonical, rollups serve fast reads):
Table Role analytics_factsAppend-only raw event rows (source of truth); pruned after the retention window. Raw actor_idlives only here.analytics_daily(day, event, subject) → count, UPSERT-incremented per fact; a__total__sentinel row is the per-event daily total, low-cardinality subjects (collections, content types) also get a breakdown row. Kept forever.analytics_active_actors(day, metric, actor_type, actor_id_hash)— a salted HMAC of the actor id, never the raw value. "Active users / day" = distinct rows. Kept forever, privacy-minimized. AnalyticsRecorder— the single, synchronous, best-effort write chokepoint (never throws into the request). Writes the fact, atomically increments the daily rollups (ON CONFLICT), and records a distinct active user (humans only —adminnormalized touser, api-keys/system excluded).- Ingestion — the pack subscribes framework auth events (
login/logout/login_failed) under a strict token/PII allow-list (never reads token accessors; failed logins are count-only). An App-side bridge listener maps the events the pack can't depend on —thallo-collectionsCollection*/CollectionRow*and contentEntry*events — into the recorder (the audit-listener pattern). - Read API —
GET /v1/admin/analytics/series(zero-filled daily time-series for a metric, optionally by subject),GET /v1/admin/analytics/summary(KPI totals + distinct active users over a range), andGET /v1/admin/analytics/breakdown(top subjects for one event over a range), behindauth+content_permission:analytics.read. - Retention —
./thallo analytics:prunedeletes rawanalytics_factspastanalytics.retention_days(default 90); the rollups and the distinct-actor table are never pruned.
The capability
The provider registers a single capability in boot():
new Capability('thallo.analytics', label: 'Analytics', description: '…');
- Enabled by default. Disable it by setting
'thallo.analytics' => falseinconfig/thallo.php'scapabilitiesswitchboard. - Gated end-to-end. When disabled, the read API routes are never registered (
404) and all ingestion stops — the auth listeners and the App bridge only subscribe when the capability is enabled, so no facts are recorded. Migrations run on install (not enable), so disabling preserves the tables. - Permission. The pack declares
analytics.read; the host app grants it toadministratorin its own dependent migration.
Privacy
The forever-kept analytics_active_actors table holds no identity — only a per-instance one-way
HMAC (actor_id_hash = hmac_sha256(actor_id, ANALYTICS_HASH_KEY | APP_KEY)), which preserves
uniqueness for counting without being reversible to a user. Raw actor_id exists only in
analytics_facts and is removed at the retention prune. Auth facts never carry token material, and
failed logins record no attempted username.
Boundary
Depends on glueful/thallo-contracts and glueful/framework — and never on glueful/thallo (the
application), the audit extension, or glueful/thallo-collections. The collection/content event bridge
lives App-side (app/Analytics/) precisely so the pack stays dependency-pure; the repo's
composer boundaries check enforces this (no App\ references in src/).
Install
The pack is bundled by default in the Thallo create-project template. To add it to an existing app (it lives as a path package in this monorepo):
composer require glueful/thallo-analytics./thallo extensions:enable thallo-analytics(writes the provider into theconfig/extensions.phpallow-list and recompiles the extension cache)./thallo migrate:runto create the tables.
Optionally set ANALYTICS_HASH_KEY (falls back to APP_KEY) and ANALYTICS_RETENTION_DAYS.
Remove
./thallo extensions:disable thallo-analytics, then composer remove glueful/thallo-analytics. The CMS
core boots unchanged. The thallo.analytics capability disappears from GET /v1/admin/capabilities,
so the read API is gone and ingestion stops. The analytics tables remain on disk (drop them manually
if you want the data gone).
Out of scope
The admin-SPA analytics dashboard (charts over this data) is a separate concern — this pack ships
the backend fact store + read API. HTTP/ops metrics stay with the framework metrics capability.