---
title: "App Install & Activity Telemetry"
canonical_url: "https://ainode.run/docs/app-install-telemetry"
type: "doc"
locale: "en"
description: "Public ingest and admin aggregation for marketplace app downloads, installs, activation, and version mix."
---

# App Install & Activity Telemetry
> Public ingest and admin aggregation for marketplace app downloads, installs, activation, and version mix.

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 |

```bash
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"}'
```

```json
{ "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.

```json
{
  "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`.
