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:

LayerEventSource
Funnel topdownloadRecorded server-side when a verified package is handed out by the package endpoint
AdoptioninstallReported once per device by the installer/client on first successful install
ActivitylaunchReported by an installed client when it runs
Lifecycleupdate, uninstallReported 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:

FieldTypeRequiredNotes
eventstringyesOne of install, launch, update, uninstall
uniqueNamestringyesThe listing's unique_name (lowercase kebab-case)
installIdstringyes8–128 url-safe characters (A-Za-z0-9_-); a fresh id per install so reinstalls count separately
versionstringnoInstalled version, max 64 characters
osstringnoMax 32 characters
archstringnoMax 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 install is recorded once per (uniqueName, installId) for the lifetime of that id; reinstalling mints a new installId and counts again.
  • Repeated launch/update/uninstall within a one-hour window collapse to a single row, so a stuck client cannot flood the table.
  • download events are never deduplicated.
  • Raw events are pruned forward-only after 180 days; the guard runs node scripts/check-app-install-telemetry.mjs.