← Documentação

Apps mobile e desktop

Um app no Analisa, uma site key: o seu site, os seus apps iOS e Android e o seu app desktop (Electron, Tauri ou nativo) mandam para o mesmo lugar, e todo relatório pode ser filtrado por plataforma.

O que já existe hoje

  • Web: o SDK @racoelho/analytics (tag script ou npm). Os eventos dele vão marcados como platform: web automaticamente.
  • Qualquer app, já: o protocolo HTTP de ingestão abaixo. Um POST com JSON — funciona em Swift, Kotlin, Dart, React Native, Electron ou num backend.
  • React Native / Expo: o SDK @racoelho/analytics-react-native implementa este protocolo por você: telas (React Navigation e Expo Router), sessões, ciclo de vida do app, crashes, fila offline e reenvio. A instalação está em Instalação → React Native no painel do app.
  • Electron / Tauri: o SDK @racoelho/analytics-desktop faz o mesmo para apps desktop (Windows, macOS e Linux): telas, sessões, ciclo de vida do app, crashes e fila offline. A instalação está em Instalação → Electron/Tauri.
  • Android (Kotlin): o SDK io.useanalisa:analytics cobre telas (Jetpack Compose Navigation ou Activities), sessões, ciclo de vida do app, crashes e fila offline. A instalação está em Instalação → Android (Kotlin).
  • Flutter: o pacote analisa_analytics cobre iOS, Android, Windows, macOS e Linux em Dart: telas (NavigatorObserver), sessões, ciclo de vida do app, crashes e fila offline. A instalação está em Instalação → Flutter.
  • iOS / Swift (em breve): um SDK nativo em Swift que faz o mesmo. Até lá, use o protocolo HTTP (ou Flutter / React Native).

Ligue a ingestão de apps

  1. Abra Aplicações → seu app → Configurações no painel.
  2. Em Apps mobile e desktop, marque Receber eventos de apps (iOS, Android, desktop).
  3. Opcional: liste os bundle ids / package names (ex.: com.suaempresa.app). Com a lista preenchida, só entram requests com um deles (header X-Analisa-App-Id ou context.app.id).
  4. Mande os eventos com a site_key do app e sem header Origin. O tráfego web continua com a checagem de origens permitidas.

O que aparece no painel

  • Filtro de plataforma ao lado do período (Todas, Web, iOS, Android, Windows, macOS, Linux): vale para todos os relatórios e fica salvo enquanto você navega.
  • Card Plataformas no dashboard, com sessões e visitantes por plataforma, quando mais de uma plataforma tem dados.
  • Versões do app: usuários, sessões, erros e sessões sem crash por versão ao longo do tempo, e quantos usuários já estão na versão mais recente.
  • Páginas e telas: apps registram telas como page views com um caminho tipo rota (/product/detail), então Páginas, Fluxo de páginas, Funis e Metas funcionam para apps também.
  • Dispositivos: versão do sistema e modelo do aparelho para o tráfego de apps.
  • Erros: crashes e erros agrupados como na web, com plataforma e versão do app, filtráveis por versão.

Sobre a chave em apps e abuso

Uma site key dentro de um binário mobile ou desktop é pública: qualquer um consegue extraí-la. A ingestão de apps e a lista de app ids são mitigação de spam no mesmo nível da checagem de Origin na web — evitam reuso por engano e lixo casual, não um atacante determinado. Nunca coloque a secret key num app; ela é só para chamadas do servidor.


Protocolo de ingestão v1 (especificação)

O contrato completo que os SDKs implementam: endpoint, payload, campos de contexto, catálogo de eventos (telas, ciclo de vida, crashes, identify), identidade e sessões, lotes e reenvio.

A especificação técnica está em inglês, para quem escreve SDKs em qualquer linguagem.

This is the wire contract between any Analisa SDK (web, React Native/Expo, Electron/Tauri, Flutter, Swift, Kotlin, or your own HTTP client) and the ingestor. One app in the dashboard has one site key, and that same key receives events from the website and from the mobile and desktop apps. The dashboard then filters and groups everything by platform.

The key words MUST, SHOULD and MAY are used as in RFC 2119.

  • Status: v1, stable. New optional fields may be added; nothing here will change meaning.

1. Endpoint

POST {endpoint}/v1/i

{endpoint} defaults to https://function-bun-production-3339.up.railway.app, so the full default URL is https://function-bun-production-3339.up.railway.app/v1/i. SDKs MUST let the developer override the full URL (self-hosting, proxies).

There is a single endpoint for every event type. There is no separate identify, screen or batch endpoint.

2. Request

2.1 Headers

HeaderRequiredNotes
Content-Type: application/jsonyestext/plain is also accepted (the web SDK uses it to avoid a CORS preflight). Body MUST be UTF-8 JSON either way.
User-AgentrecommendedFree form, e.g. ShopApp/2.3.0 (iOS 17.4; iPhone15,2) analisa-swift/0.1.0. Parsed only as a fallback when context lacks a field.
X-Analisa-App-IdconditionalBundle id / package name. Required when the app restricts allowed_app_ids and the events do not carry context.app.id (see §4). Sending it always is fine.
Accept-LanguageoptionalUsed as locale fallback.
X-CountryoptionalISO 3166-1 alpha-2 guess from the device locale/region. The server prefers its own geo headers.
Authorization: Bearer <secret_key>server-side onlyNever put the secret key in a mobile or desktop binary. Bearer requests are for backends (see §4.3).
Originnever set it manuallyBrowsers set it. Native clients MUST NOT send one (see §4).

No cookies are used or set.

2.2 Body

json
{
  "site_key": "pk_live_abc123",
  "sent_at": 1767225600123,
  "context": { "...": "context shared by every event in the batch (§3)" },
  "batch": [ { "...": "event (§2.3)" } ]
}
FieldTypeRequiredNotes
site_keystringyes (unless Bearer)The app's public key from the dashboard.
sent_atinteger ms epochrecommendedDevice clock at the moment the request is sent (set it on every attempt, including retries). If it differs from the server clock by more than 60 s, the server shifts every event ts in the batch by the difference, so a device with a wrong clock still produces correct times.
contextobjectrecommendedBatch-level context (§3). Applies to every event; an event's own context overrides it key by key (top-level keys: an event context.app replaces the whole batch context.app).
batcharrayyes1 to 1000 events.

2.3 Event

json
{
  "id": "7d1f8a0e-4b5c-4c1e-9d0b-2f3a4b5c6d7e",
  "event": "page_view",
  "ts": 1767225599000,
  "anon_id": "c0a8012e-5f4b-4d4e-8f7a-1b2c3d4e5f60",
  "session_id": "0f9e8d7c-6b5a-4f3e-8d2c-1b0a9f8e7d6c",
  "user_id": "user_123",
  "path": "/product/detail",
  "url": "shopapp://product/42",
  "referrer": "",
  "props": { "screen_name": "ProductDetail", "product_id": "42" },
  "context": { "...": "optional per-event overrides (§3)" }
}
FieldTypeRequiredNotes
idstringyes for native SDKsClient-generated unique id, 8–128 chars of [A-Za-z0-9._:-] (UUID v4 recommended). It is the idempotency key: the same id sent twice for the same site key is stored once. Generate it when the event is created, persist it with the queued event and reuse it on every retry.
eventstringyesEvent name, max 128 chars. Reserved names in §5. Use snake_case.
tsinteger ms epochyesWhen the event happened on the device (not when it was sent). Numbers only (a numeric string is tolerated). Events more than 10 min in the future (after the sent_at correction) are stored with the server time.
anon_idstringyesInstall-level anonymous id (§6.1).
session_idstringyesSession id (§6.2).
user_idstring | nullwhen knownYour app's user id after identify (§6.3). Omit or null when anonymous.
pathstringfor page_viewRoute-like path. For apps, the screen path (§5.1). Query string and trailing slash are stripped.
urlstringoptionalFull URL (web) or deep link (apps).
referrerstringoptionalWeb referrer. Apps MAY send the install/deep-link source (e.g. https://google.com, play-store); leave empty otherwise.
utmobjectoptional{ source, medium, campaign, term, content } (strings). Apps MAY fill it from campaign deep links.
propsobjectoptionalEvent properties. Flat JSON values (string, number, boolean, null) recommended; nested values are stored but not filterable in the dashboard. Keep it under ~4 KB per event.
contextobjectoptionalPer-event context overrides (§3).

Legacy web fields (lang, tz, viewport, screen, ua_string) are still accepted; native SDKs use context instead.

2.4 Limits

LimitValueServer response when exceeded
Request body100 KB (MAX_PAYLOAD_BYTES)413 {"ok":false,"error":"payload_too_large"}
Events per request1000413 {"ok":false,"error":"batch_too_large","max":1000}

SDKs SHOULD send at most 50 events per request and split larger queues.

3. Context

context describes where the event came from. Every field is optional; send everything you can read cheaply. The server validates and normalizes it: unknown keys are dropped, enum values are lower-cased and checked, strings are trimmed and truncated, numbers out of range are dropped. A bad field never rejects the event.

json
{
  "platform": "ios",
  "app": { "version": "2.3.0", "build": "451", "id": "com.shop.app" },
  "os": { "name": "iOS", "version": "17.4.1" },
  "device": { "model": "iPhone15,2", "manufacturer": "Apple", "type": "phone" },
  "locale": "pt-BR",
  "tz": "America/Sao_Paulo",
  "screen": { "w": 393, "h": 852, "density": 3 },
  "sdk": { "name": "analisa-swift", "version": "0.1.0" },
  "framework": "swift"
}
FieldType / valuesMaxSource examples
platformweb | ios | android | windows | macos | linux | otherenumiPadOS → ios; Android TV → android; Electron/Tauri → the OS they run on. Aliases accepted: darwin/osx → macos, win32 → windows. Unknown values become other.
app.versionstring64CFBundleShortVersionString, versionName, app.getVersion(), package_info_plus.version
app.buildstring (numbers accepted)64CFBundleVersion, versionCode, buildNumber
app.idstring255Bundle id / package name / Electron appId. Used by allowed_app_ids (§4).
os.namestring64UIDevice.systemName, "Android", "Windows", "macOS", distro name on Linux
os.versionstring64"17.4.1", "14", "10.0.22631", "14.5"
device.modelstring128Machine identifier: utsname.machine (iPhone15,2), Build.MODEL (SM-S918B), hw.model (MacBookPro18,3)
device.manufacturerstring128Apple, Build.MANUFACTURER (samsung)
device.typephone | tablet | desktop | tv | otherenumiPad / Android smallestScreenWidthDp >= 600 → tablet; Electron/Tauri → desktop
localeBCP 47 string35pt-BR, en-US
tzIANA zone64TimeZone.current.identifier, ZoneId.systemDefault()
screen.w, screen.hnumber 0–100000Logical size in points/dp (not physical pixels), portrait or current orientation
screen.densitynumber 0–20UIScreen.scale, displayMetrics.density, devicePixelRatio
sdk.namestring64analisa-react-native, analisa-flutter, analisa-swift, analisa-kotlin, analisa-electron, analisa-tauri, @racoelho/analytics (web)
sdk.versionstring32SDK semver
frameworklower-case slug32web, react-native, expo, flutter, electron, tauri, swift, swiftui, kotlin, compose

How the server derives what it stores:

  • No context.platform: browser requests (allowed Origin) and server requests (Bearer) are web — this keeps every existing web integration working unchanged. Native requests (no Origin) without a platform are stored as other. Native SDKs MUST always send platform.
  • The web SDK sends {"platform":"web","sdk":{...},"framework":"web"} as batch context.
  • For native events the stored OS name is canonicalized to the same names the web UA parser uses (iOS, Android, Windows, macOS, Linux), so web and app traffic group together in the Devices report. The User-Agent header is only a fallback for missing fields.

4. Authentication and abuse control

ClientHow it authenticates
Browser (web SDK)site_key + the browser's Origin, checked against the app's allowed origins (empty list = any origin).
Mobile / desktop appsite_key + no Origin header. Accepted only when the app has "Receive events from apps" (allow_native) turned on in the dashboard settings. If the app also lists allowed app ids (allowed_app_ids), the request MUST carry an app id — X-Analisa-App-Id header and/or context.app.id (batch or event) — and every app id in the request must be in that list.
Desktop webview with a custom scheme (tauri://localhost, app://, file://, null) or Tauri's Windows WebView2 host (http(s)://tauri.localhost)Treated as a native request (rule above). Any other webview served from http(s):// is a web origin: add it to allowed origins, or send from the main process.
Your backendAuthorization: Bearer <secret_key> (no site_key needed). Never ship the secret key in an app.

Be honest about what this protects. A site key inside a mobile or desktop binary is public: anyone can extract it and the app id, and replay requests without an Origin. allow_native and allowed_app_ids are spam mitigation of the same strength as the Origin check on the web (which curl also bypasses): they stop drive-by junk and other apps reusing your key by accident, not a determined attacker. Keep allow_native off for apps that are web-only.

Error responses (JSON, ok: false):

StatuserrorMeaningSDK action
400invalid_payloadBody is not JSON or batch is missing/emptyDrop the batch (it will never succeed), log in debug
400site_key_requiredNo site_key and no BearerDrop, disable sending (misconfiguration)
401unauthorizedBad Bearer secretDrop, disable sending
402quota_exceeded / subscription_inactivePlan limitDrop the batch; pause sending for 1 h
403native_not_allowedNo Origin and allow_native is offDrop, disable sending for this process; log a clear message ("enable 'Receive events from apps' in the dashboard")
403app_id_required / app_id_not_allowedallowed_app_ids check failedDrop, disable sending; log
403origin_not_allowedWeb origin not allowedDrop
403site_inactiveApp paused/disabledDrop, disable sending
404site_not_foundUnknown site keyDrop, disable sending
413payload_too_large / batch_too_largeSee §2.4Split the batch in half and resend; a single event that is still too large is dropped
429, 5xx, network error, timeout—TransientKeep the batch and retry with backoff (§7)

Success: 200 {"ok":true,"received":N}. N can be lower than the batch size (duplicates removed); that is still success — remove the whole batch from the queue.

5. Event catalog

All names below are reserved. Anything else is a custom event (track(name, props)).

5.1 Screen views — page_view

Apps record screens as page_view so Pages ("Pages and screens"), Page flow, Funnels and Goals work the same for web and apps:

json
{ "id": "…", "event": "page_view", "ts": 1767225599000, "anon_id": "…", "session_id": "…",
  "path": "/product/detail", "props": { "screen_name": "ProductDetail" } }
  • path MUST be route-like: starts with /, lower-case, - between words, no ids (/product/detail, not /product/42). If the app has a router with paths (Expo Router, React Navigation linking, go_router, Compose Navigation routes), send the route pattern; otherwise derive it from the screen name: ProductDetail → /product-detail.
  • props.screen_name SHOULD carry the human screen name.
  • Put ids in props (product_id), never in path, to keep the Pages report aggregated.
  • Send one page_view per screen appearance. Do not resend when the same screen re-renders or when the app returns from background to the same screen.

Shortcut: {"event":"screen_view","screen_name":"ProductDetail"} (or props.screen / props.screen_name) is accepted and stored as a page_view with path derived as above when path is absent. New SDKs SHOULD send page_view directly.

5.2 Lifecycle (apps)

EventWhenprops
app_openCold start (process launch), and when the app comes to the foreground and a new session starts (§6.2)launch: "cold" | "warm"; first_launch: true only on the very first launch after install (otherwise omit or false)
app_foregroundEvery transition background → foregroundbackground_ms: time spent in background
app_backgroundEvery transition foreground → backgroundforeground_ms: time since the last foreground/open
app_updateFirst launch after app.version or app.build changed (send it before that launch's app_open)previous_version, previous_build

Order on a cold start after an update: app_update → app_open {launch:"cold"} → first page_view. On the first ever launch: app_open {launch:"cold", first_launch:true} (no app_update). Desktop SDKs map "background" to all windows hidden/minimized (or app deactivated on macOS) and MAY skip app_foreground/app_background when that is not observable.

5.3 Errors and crashes — error

Same shape as the web SDK, so the Errors report groups them:

json
{ "event": "error", "props": {
    "error_type": "NSInvalidArgumentException",
    "error_message": "unrecognized selector sent to instance",
    "error_stack": "0 CoreFoundation …\n1 libobjc.A.dylib …",
    "error_kind": "crash",
    "error_handled": false,
    "error_fingerprint": "a1b2c3d4"
} }
PropNotes
error_typeException class / signal (SIGSEGV, java.lang.NullPointerException, TypeError)
error_messageMessage, max ~1000 chars
error_stackStack trace, max ~8 KB; strip absolute file-system paths and user data
error_kindcrash (process died: uncaught exception or fatal signal), anr (Android not responding), unhandled (uncaught but app survived, e.g. JS/Dart error handler), handled (captureException)
error_handledtrue only for handled
error_fingerprintStable group id: hash of error_type + the first 3 in-app frames without line numbers/addresses, so the same bug groups across builds. Max 64 chars.

Crashes are usually sent on the next launch: persist the crash report to disk in the crash handler (do no network I/O there), and on the next launch enqueue it with the crash's own ts, session_id and context (the version that crashed), before app_open. The dashboard computes crash-free sessions per app version from error_kind = "crash".

5.4 Identify — identify

json
{ "id": "…", "event": "identify", "user_id": "user_123",
  "props": { "email": "ana@shop.com", "plan": "pro", "id_prop": "email" } }
  • user_id (top level) is required. props are traits; id_prop (optional) names the trait the dashboard uses as the person's label.
  • After identify, every following event MUST carry the same user_id (persist it across launches) until reset().
  • reset() (logout): clear user_id, start a new session and keep anon_id.
  • Do not send identify again for the same user_id within 5 minutes.

5.5 Custom events

Any other name: { "event": "add_to_cart", "props": { "product_id": "42", "price": 99.9 } }. The web SDK's helpers (purchase, signup, login, search, …) define recommended names and props; native SDKs SHOULD expose the same helpers with the same names.

6. Identity and sessions

6.1 anon_id

  • UUID v4, generated on first launch, persisted in app storage (UserDefaults, SharedPreferences / DataStore, localStorage in Electron/Tauri, a file in the app data dir).
  • Never derived from hardware ids, advertising ids (IDFA/GAID) or anything that survives reinstall. A reinstall produces a new anon_id — that is intended.
  • Sent on every event.

6.2 session_id

  • UUID v4. A session ends after 30 minutes without events.
  • Before stamping any event, the SDK checks now - last_event_at > 30 min (wall clock); if so (or if there is no session yet) it creates a new session_id. last_event_at is updated for every event and persisted together with session_id, so a process restart within 30 minutes continues the same session.
  • Background → foreground: if the app was in background for more than 30 minutes, the next event starts a new session, and the SDK emits app_open {launch:"warm"} as its first event. Otherwise it emits only app_foreground and the session continues.
  • reset() always starts a new session.
  • Lifecycle events count as activity (an app_background updates last_event_at).

6.3 user_id

Set by identify, persisted, cleared by reset() (§5.4).

SDKs MUST offer setConsent('denied') / optOut() that stops collection, clears the queue and the persisted user_id, and survives restarts. Respect platform tracking settings where they apply. Do not collect IDFA/GAID, IP-derived precise location, contacts or any personal data by default.

7. Delivery: queue, batching, retry

  • Persistent queue. Events are appended to an on-disk queue (file, SQLite, MMKV, localStorage…) so they survive app kills. Cap it at 1000 events; when full, drop the oldest. Drop events older than 7 days at send time.
  • Flush triggers. Flush when the queue reaches 20 events, every 10 s while the app is in the foreground, on app_background (iOS: inside beginBackgroundTask; Android: from ProcessLifecycleOwner ON_STOP, using WorkManager if the send cannot finish), and at launch for anything left from the previous run. Only one request in flight at a time.
  • Batch. Up to 50 events per request (§2.4). Set sent_at on every attempt.
  • Retry. On network error, timeout (use 10 s), 429 or 5xx: keep the batch, retry with exponential backoff min(2^attempt × 1 s, 5 min) with ±20 % jitter. Honor Retry-After when present. Reset the backoff after a success. Because every event has an id, retrying a batch that was in fact stored is harmless.
  • Do not retry other 4xx (§4 table). On 413, split the batch.
  • Offline. Do not attempt sends while the OS reports no connectivity; flush when it returns.
  • Never block the UI thread and never throw into the host app.

8. Privacy guarantees the server applies

  • The client IP is used only for country lookup and stored anonymized (last IPv4 octet / last 80 bits of IPv6 zeroed). Do not send IPs in props.
  • No fingerprinting: the server never combines IP/UA into an identifier.
  • Delete requests (GDPR) work by anon_id / user_id through the dashboard.

9. Example: plain HTTP

bash
curl -X POST https://function-bun-production-3339.up.railway.app/v1/i \
  -H 'Content-Type: application/json' \
  -H 'User-Agent: ShopApp/2.3.0 (Android 14; SM-S918B)' \
  -H 'X-Analisa-App-Id: com.shop.app' \
  -d '{
    "site_key": "pk_live_abc123",
    "sent_at": 1767225600123,
    "context": {
      "platform": "android",
      "app": { "version": "2.3.0", "build": "451", "id": "com.shop.app" },
      "os": { "name": "Android", "version": "14" },
      "device": { "model": "SM-S918B", "manufacturer": "samsung", "type": "phone" },
      "locale": "pt-BR", "tz": "America/Sao_Paulo",
      "screen": { "w": 384, "h": 854, "density": 2.8125 },
      "sdk": { "name": "curl", "version": "1" }, "framework": "kotlin"
    },
    "batch": [
      { "id": "b6f1c9a4-0d0e-4f57-9a3b-6a5f3f7f2b10", "event": "app_open", "ts": 1767225599000,
        "anon_id": "c0a8012e-5f4b-4d4e-8f7a-1b2c3d4e5f60", "session_id": "0f9e8d7c-6b5a-4f3e-8d2c-1b0a9f8e7d6c",
        "props": { "launch": "cold" } },
      { "id": "0b8e7c4f-2a6d-4c51-8f0b-9d3e2a1c5b77", "event": "page_view", "ts": 1767225599500,
        "anon_id": "c0a8012e-5f4b-4d4e-8f7a-1b2c3d4e5f60", "session_id": "0f9e8d7c-6b5a-4f3e-8d2c-1b0a9f8e7d6c",
        "path": "/home", "props": { "screen_name": "Home" } }
    ]
  }'
# → {"ok":true,"received":2}

10. Minimal contract for a new SDK (checklist)

  1. init(siteKey, { endpoint?, debug? }); never throws, never blocks the UI thread.
  2. Persist anon_id; manage session_id with the 30-minute rule and foreground handling (§6).
  3. Build context once per launch (platform, app, os, device, locale, tz, screen, sdk, framework) and send it as batch context; refresh locale/tz/screen on change.
  4. Every event: id (UUID), event, ts, anon_id, session_id, user_id when identified.
  5. API: track(name, props), screen(name, props?) → page_view (§5.1), `identify(userId, traits?, idProp?), reset(), captureException(error, props?), setConsent(...), flush()`.
  6. Automatic: app_open, app_update, app_foreground, app_background (§5.2) and crash capture (§5.3), each switchable off in the config.
  7. Persistent queue, batching, backoff and the response handling table (§4, §7).
  8. No Origin header, no secret key, no personal data by default.
  9. Test against a real ingestor: events appear in the dashboard under the right platform, versions show in "App versions", a forced crash appears in Errors with error_kind: "crash".
Analytics de apps mobile e desktop · Analisa