App Install & Activity Telemetry
The apps extension closes the marketplace funnel: browsing and downloading are already public, but until installs and launches were reported there was no way to tell whether a listed app was actually adopted, still running, or drifting across versions. This telemetry is owned entirely by the extension (ext_apps_install_event); it never touches the APay core schema.
It records four signals:
| Layer | Event | Source |
|---|---|---|
| Funnel top | download | Recorded server-side when a verified package is handed out by the package endpoint |
| Adoption | install | Reported once per device by the installer/client on first successful install |
| Activity | launch | Reported by an installed client when it runs |
| Lifecycle | update, uninstall | Reported by the installer/client |
Public Ingest (POST /api/public/plugins/apps/telemetry)
Fire-and-forget endpoint for the Shopark installer and installed clients. It is intentionally forgiving: rate limiting and validation problems return a JSON body, never a 5xx, so a telemetry hiccup can never block a client start or an install.
Request body:
| Field | Type | Required | Notes |
|---|---|---|---|
event | string | yes | One of install, launch, update, uninstall |
uniqueName | string | yes | The listing's unique_name (lowercase kebab-case) |
installId | string | yes | 8–128 url-safe characters (A-Za-z0-9_-); a fresh id per install so reinstalls count separately |
version | string | no | Installed version, max 64 characters |
os | string | no | Max 32 characters |
arch | string | no | Max 32 characters |
curl -X POST https://yourdomain.com/api/public/plugins/apps/telemetry \
-H 'content-type: application/json' \
-d '{"event":"install","uniqueName":"stripe","installId":"device-2f9a1c04","version":"1.2.0","os":"macos","arch":"arm64"}'
{ "code": 0, "msg": "ok", "data": { "recorded": true } }
The endpoint only accepts events for a uniqueName that exists in the catalog (unique_name or slug); unknown apps return code: 404.
Server-side download counter
POST /api/public/plugins/apps/package records a download event once a listing resolves to a verified, non-withdrawn release, just before the signed mirrors are returned. The write is best-effort: a failure is logged and never changes the download response.
Admin aggregation (GET /api/admin/plugins/apps/catalog/telemetry)
Backed by the same catalog capability as the Apps page. Query parameter days (1–365, default 30) sets the reporting window.
{
"windowDays": 30,
"generatedAt": "2026-01-20T12:00:00.000Z",
"totals": { "installsAllTime": 1840, "installs": 212, "uninstalls": 9, "downloads": 640, "active7d": 1180, "active30d": 1602 },
"apps": [{ "uniqueName": "stripe", "name": "Stripe Connector", "version": "1.3.0", "installs": 210, "active": 1590, "downloads": 620, "uninstalls": 9 }],
"daily": [{ "date": "2026-01-20", "installs": 8, "active": 1150, "downloads": 27 }],
"versions": [{ "uniqueName": "stripe", "version": "1.3.0", "installs": 196 }]
}
Counts are deduplicated by installId: installs counts distinct devices that installed in the window, active counts distinct devices that launched or updated (launch/update) inside the selected days window, daily buckets events by the configured site timezone (settings.timezone) across SQLite/PostgreSQL/MySQL dialects, and versions reflects each device's latest lifecycle state (install/update while excluding devices whose latest lifecycle event is uninstall). The server also enriches apps with name and version from ext_apps_catalog.
Deduplication and retention
- An
installis recorded once per(uniqueName, installId)for the lifetime of that id; reinstalling mints a newinstallIdand counts again. - Repeated
launch/update/uninstallwithin a one-hour window collapse to a single row, so a stuck client cannot flood the table. downloadevents are never deduplicated.- Raw events are pruned forward-only after 180 days; the guard runs
node scripts/check-app-install-telemetry.mjs.