# Logsky integration guide

This is the complete contract for sending analytics to Logsky from any product — Android, iOS, web site, TV app, desktop app, bot. It is written so that a developer or an AI assistant can implement a client from this file alone. Everything the dashboard shows is derived from the events described here; send them the way they are described and every card lights up.

- Dashboard: https://app.logsky.org (project, environments, tokens under **Settings**)
- Ingest: `https://eu.logsky.org` (Europe) and `https://ir.logsky.org` (Iran). Both accept the same requests. Send to the region the user's other API traffic goes to; a user who switches region keeps the same device id, so nothing breaks either way.
- Web SDK (also TV and desktop web views): https://logsky.org/sdk/logsky.js
- This file online: https://logsky.org/docs/integration.md

## 1. Set up a project

1. **One project per product and platform**: `flixdrama` (Android), `flixdrama-web`, `flixdrama-tv`, `flixdrama-desktop`, `turkflix`, … The project slug is what the dashboard groups by; mixing platforms in one project works but makes every number a blend.
2. **Environments** inside a project: `production` always; `legacy` for the old build line, `beta`/`staging` as needed. An environment is a filter in the dashboard.
3. **Tokens**: Settings → project → environment → *New token*. Shown once. A token fixes the project and environment; the client never sends them. One token per environment is enough for both regions. Revoking a token makes every client that uses it receive **401** from then on.
4. Put the token in the build (Android: `telemetry.properties`, web: `LogskyConfig`, others: build config). Never in a public repo.

## 2. Wire protocol

```
POST https://<region>.logsky.org/api/ingest
X-Ingest-Token: <token>              (or ?token=<token> when headers are impossible, e.g. sendBeacon)
Content-Type: application/json
Content-Encoding: gzip               (optional but recommended above a few KB)

{ "context": { … }, "events": [ … ] }
```

| Response | Meaning | Client action |
|---|---|---|
| **204** | stored | drop the batch |
| **400** | malformed body / bad gzip | drop the batch (it will never succeed) |
| **401** | unknown or revoked token | drop; stop retrying this batch |
| **413** | > 1 MB compressed or > 500 events | split the batch |
| **503** / **429** / network error | server busy or restarting | keep the batch, retry with back-off (5 s → 5 min) |

Limits: 1 MB per request (compressed), 500 events per batch, 32 KB per stack trace, events older than 7 days are dropped, timestamps in the future are clamped to now.

`OPTIONS` preflight is answered with `Access-Control-Allow-Origin: *`, so browsers can post directly.

### Dry run

Append `?dry_run=1` to validate without storing. The answer lists every event with `ok` or a `reason`, plus the project/environment the token resolves to and what the server would record for `platform`, `country`, `screen`:

```
curl -s -X POST 'https://eu.logsky.org/api/ingest?dry_run=1' -H 'X-Ingest-Token: …' -H 'Content-Type: application/json' -d @batch.json
```

Use it while building a client; it is the fastest way to see what is wrong.

## 3. Identity

| Field | Rule |
|---|---|
| `device_id` | **Exactly 64 hex characters** (32 random bytes). Generated once per install and stored (Android SharedPreferences, web localStorage, desktop config file). Never derived from hardware ids or the user. This is what "devices", "installs" and retention count. |
| `session_id` | Any string ≤ 64 chars (16 random bytes hex is fine). New session on app start and after **30 minutes without activity**; the same id across background/foreground within those 30 minutes. "Sessions", "avg session", "time in app" are built from it. |
| `user_id` | Positive integer of the signed-in account, omitted or `null` when anonymous. Powers "signed-in users" and the user lookup page. Never an email or a name. |

A session is measured server-side as first event → last event, capped at 6 hours. Send at least an event at start and one at background so the length is real (see §6).

## 4. Context (once per batch)

```json
"context": {
  "app_version": "7.5.3",
  "platform": "android",      // android | ios | web | tv | desktop | bot | other (a-z and _ only, ≤16)
  "os_version": "Android 14 (API 34)",
  "device_model": "Samsung SM-A525F",
  "locale": "fa-IR",
  "country": "IR"             // optional ISO-3166 alpha-2; when empty the server uses the request IP
}
```

`project` and `app_flavor` (the environment) are ignored if sent — the token decides them.

## 5. Event

```json
{
  "event_name": "player_started",     // required, ≤128, snake_case
  "event_category": "event",          // event | lifecycle | breadcrumb | error | crash | engagement (default event)
  "severity": "info",                 // debug | info | warning | error | fatal (default info)
  "session_id": "…", "device_id": "…", "user_id": 627651,
  "timestamp_ms": 1791372691817,      // client clock, ms since epoch (0 = server time)
  "screen": "player",                 // the screen the user was on; set on EVERY event
  "network_type": "wifi",             // wifi | cellular | ethernet | unknown
  "properties": { … },                // any JSON object; scalars, nested objects, arrays
  "error_class": "", "error_message": "", "stack_trace": null   // errors and crashes only
}
```

**`screen` on every event** is what makes the Screens page and the "what screen are people on now" realtime list work. Use stable route-like names: `main`, `post/{id}`, `player`, `settings`. Replace ids with `{id}`; the web SDK does this for numeric path segments.

Two property keys are promoted to columns when present: `duration_ms` (how long something took) and `bytes`.

## 6. Event vocabulary the dashboard understands

Any event name is accepted and appears on the Events page. These names drive specific cards; use them verbatim, with these properties.

### Lifecycle (sessions, startup)

| event_name | category | properties | drives |
|---|---|---|---|
| `app_started` | lifecycle | `cold: true/false` | session start |
| `cold_start_completed` | lifecycle | `total_ms` (process start → first screen interactive), `splash_ms` | **Cold start median / p90** |
| `app_foregrounded` / `app_backgrounded` | lifecycle | – | session boundaries, "time in app" |
| `session_start` / `session_end` | lifecycle | `referrer` (web) | same, for web |
| `screen_view` | breadcrumb | `title` | screens (web); native apps instead stamp `screen` on every event |
| `app_exit` | crash | `reason`: `CRASH`, `ANR`, `SIGNALED`, `LOW_MEMORY`, … (Android ApplicationExitInfo), `description`, `importance` | **crashes** (reason CRASH) and **ANRs** |

### Errors and crashes

| event_name | category | fields | drives |
|---|---|---|---|
| `uncaught_exception` | crash | `error_class`, `error_message`, `stack_trace`; severity `fatal` | **Crash groups** (grouped by class + first three stack frames), crash-free devices |
| `unhandled_rejection` (web) | error | same | errors |
| `api_error` | error | properties `path` (template, e.g. `/app/post/feed/{n}`), `method`, `status_code` (0 = no response), `duration_ms`, `error_class` (exception name or `http_500`), `expected: true` for failures that are normal (wrong password, 404 on optional data) | **API health**: failures, timeouts, per endpoint. Expected ones are excluded. |

### Playback (video/audio products)

| event_name | properties | drives |
|---|---|---|
| `player_started` | `post_id`, `episode_id`, `quality`, `is_local`, `source`, **`ttfb_ms`** (play pressed → first frame) | **Plays**, plays per device, **start time median/p90** |
| `player_buffering` | `position_ms`, **`duration_ms`** (length of the stall) | **buffering per play**, waiting per play |
| `player_error` | `post_id`, `episode_id`, `quality`, `is_local`, `error_code`, `error_class`, `error_message`, `position_ms` | **play error rate**, error devices |
| `player_completed` | `duration_ms` (watched), **`watched_pct`** | completed episodes, average watched % |
| `subtitle_resolved` | `offered` (count), **`attached`** (bool) | subtitles attached rate |
| `download_enqueued` / `download_completed` / `download_failed` | `post_id`, `episode_id`, `quality`, `bytes`, `error_class`, `error_message`, `retries` | downloads, **download failure rate** |

### Engagement (any product)

| event_name | drives |
|---|---|
| `tweet_feed_open`, `tweet_like`, `tweet_save`, `tweet_open_show`, `tweet_lens` | the Tweets tile (feed opens, actions, devices) |
| anything else you track (`search`, `signup_completed`, `purchase_started`, `purchase_completed`, …) | Events page: volume, devices, per-day series. Name them `object_verb`, keep the set small and stable. |

Suggested for new products (not yet their own card, but queryable): `signup_completed`, `login_completed`, `purchase_completed { plan, amount, currency }`, `search { results }`, `share { target }`, `notification_opened { kind }`.

## 7. Privacy rules (enforced server-side, mirror them client-side)

- These property keys are **deleted**: `email, phone, phone_number, name, first_name, last_name, full_name, address, lat, lng, latitude, longitude, post_title, episode_title, search_query, comment_text, review_text, message`.
- Any property whose key starts with `url` or ends with `_url` is reduced to `scheme://host/path` (query strings removed).
- The client IP is used only to derive a two-letter country and is not stored.
- Do not send tokens, cookies, auth headers, free text typed by users, or anything that identifies a person beyond the numeric `user_id`.

## 8. Client behaviour (what a good SDK does)

1. **Queue on disk / storage**, not only in memory: events survive a crash and offline periods. Cap the queue (e.g. 2 000 events, oldest dropped).
2. **Batch**: flush every 5–30 s or at 20–100 events, whichever first; always flush on background/pagehide (web: `sendBeacon` with `?token=`).
3. **gzip** batches above a few KB (`Content-Encoding: gzip`).
4. **Retry** only on 503/429/network, with exponential back-off; **drop** on 400/401/413 (after splitting a 413).
5. **Clock**: `timestamp_ms` from the device clock at the moment of the event, not at send time.
6. **Screen**: keep a "current screen" variable and stamp it on every event.
7. **Session**: new id on start and after 30 min idle; send `app_backgrounded` / `session_end` so sessions close.
8. **Crashes**: install a global handler that writes `uncaught_exception` to the queue synchronously and flushes on next start (Android: `Thread.setDefaultUncaughtExceptionHandler`; web: `error` + `unhandledrejection`; Electron: both processes).
9. **Startup**: measure `cold_start_completed.total_ms` from process start to the first interactive screen.
10. **Network calls**: wrap your HTTP client once and emit `api_error` for every failed call with the templated path; mark expected failures `expected: true`.
11. **Never block the UI** on telemetry; never crash because of it (catch everything inside the SDK).
12. Verify with `?dry_run=1`, then look at Realtime in the dashboard: your device should appear within ~10 s.

## 9. Platform recipes

**Web site, TV web app, Electron / desktop web view** — include the SDK and configure it; it does items 1–8 above:

```html
<script src="https://logsky.org/sdk/logsky.js" defer></script>
<script>window.LogskyConfig = { token: '…', endpoint: 'https://ir.logsky.org', app_version: '3.2.0', platform: 'web' }</script>
<script>
  // after sign-in:
  Logsky.setUser(627651)
  // product events:
  Logsky.track('player_started', { post_id: 12, episode_id: 34, quality: '720p', ttfb_ms: 900 })
  Logsky.track('api_error', { path: '/api/post/{id}', method: 'GET', status_code: 0, duration_ms: 10000, error_class: 'TimeoutError' }, 'error')
</script>
```
`platform` is `web`, `tv` or `desktop`. In an SPA that does not use the History API call `Logsky.screen('post/{id}')` on each route change.

**Android (Kotlin)** — copy the `feature/telemetry` package from the FlixDrama app (`EventLogger`, `EventBuffer`, `BatchUploader`, `SessionTracker`, `CrashReporter`, `AppExitReporter`, `ColdStartTracker`, `BreadcrumbManager`, `DeviceContext`); set the two BuildConfig fields `TELEMETRY_INGEST_URL_TEMPLATE = "https://%s.logsky.org/api/ingest"` and the token per flavour. It already implements everything in §6 and §8.

**iOS / Flutter / React Native / Electron main process / bots** — a small HTTP client following §2–§8 (about 150 lines). Use the vocabulary in §6 so the same cards work.

## 10. Checklist for an AI implementing a client

- [ ] device_id: 64 hex, persisted; session_id: regenerated after 30 min idle; user_id numeric or absent
- [ ] context: app_version, platform, os_version, device_model, locale
- [ ] every event carries `screen`
- [ ] `cold_start_completed.total_ms`, `app_backgrounded`, `uncaught_exception`, `api_error { path, status_code, expected }` implemented
- [ ] playback events with `ttfb_ms`, buffering `duration_ms`, `player_error`, `player_completed.watched_pct` (if the product plays media)
- [ ] batching, gzip, retry/drop rules, flush on background, disk queue
- [ ] no PII in properties; URLs stripped
- [ ] `?dry_run=1` returns `rejected: 0`; the device shows up in Realtime

## 11. Reading the data programmatically

Sign in once (`POST /auth/login` on `app.logsky.org` with `{email, password}`, cookie `lsk_session`), then:

`GET /api/v1/stats/<name>?project=&env=a,b&from=YYYY-MM-DD&to=YYYY-MM-DD&region=eu|ir`

names: `overview`, `realtime`, `product`, `hourly`, `versions`, `retention_summary`, `retention`, `breakdown&by=app_version|os_version|device_model|country|platform`, `screens`, `events[&name=]`, `crashes[&fingerprint=]`, `api_health`, `user&user_id=|device_id=`, `session&session_id=`. Answers are JSON, cached (10 s realtime, 3 min for ranges touching today, 6 h for history). `region` is per request; the dashboard asks both and merges in the browser.
