# Android SDK

Official Android client SDK - Kotlin, minSdk 21, zero third-party runtime dependencies. The API mirrors the Mixpanel Android SDK, so migrating is mostly a find-and-replace of MixpanelAPI to Cohorly.

## Install

Distributed via JitPack from the public `cohorly-io/cohorly-android`
repository:

```kotlin
// settings.gradle.kts
dependencyResolutionManagement {
    repositories {
        google(); mavenCentral()
        maven("https://jitpack.io")
    }
}

// app/build.gradle.kts
dependencies {
    implementation("com.github.cohorly-io.cohorly-android:cohorly-android:v0.1.0")
}
```

The host app must declare the `INTERNET` permission.

## Initialize

Call `Cohorly.getInstance` once, e.g. in `Application.onCreate()`. One
instance per project token.

```kotlin
import com.cohorly.android.Cohorly

val cohorly = Cohorly.getInstance(context, "YOUR_PROJECT_TOKEN")

cohorly.track("Plan Selected", mapOf("plan" to "pro"))

cohorly.identify("user-123")           // links the anonymous id via /alias
cohorly.people.set("plan", "pro")      // profile update via /engage

cohorly.registerSuperProperties(mapOf("ab_test" to "variant_b"))

cohorly.flush() // also auto-flushes every 5s / 20 events
```

## API

| Member                                                                           | Notes                                                                                                                |
| -------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- |
| `Cohorly.getInstance(context, token, apiHost[, config])`                         | Singleton per token. `CohorlyConfig` accepts `flushIntervalMs`, `batchSize`, `maxQueueSize`, `trackAutomaticEvents`. |
| `track(name[, JSONObject \| Map])`                                               | Queue an event (persisted to SharedPreferences).                                                                     |
| `identify(id)` / `reset()`                                                       | Set the distinct id (sends /alias while anonymous) / new anonymous id on logout.                                     |
| `registerSuperProperties[Once] / unregisterSuperProperty / clearSuperProperties` | Super properties merged into every event.                                                                            |
| `people.set / setOnce / increment / unset / deleteUser`                          | Profile updates sent to /engage.                                                                                     |
| `optOutTracking() / optInTracking()`                                             | GDPR opt-out; drops queued events while opted out.                                                                   |
| `flush()`                                                                        | Send now, bypassing any retry backoff window.                                                                        |
| `getFeatureFlag(key[, fallback])` / `isFeatureEnabled(key[, fallback])`          | Read a flag's variant/enabled value; never hits the network.                                                         |
| `getFeatureFlagDetails(key[, fallback])`                                         | The value plus `source` (`network` / `persistence` / `fallback`) and cache metadata.                                 |
| `reloadFeatureFlags()` / `onFeatureFlags(listener)`                              | Re-evaluate flags on the worker thread / subscribe to updates.                                                       |

## Feature flags

Flags are evaluated on `getInstance` (opt out with
`CohorlyConfig(loadFeatureFlags = false)`), on `identify()` and on `reset()`,
always on the worker thread, and persisted to storage scoped by distinct id;
reads never hit the network.

`CohorlyConfig.flagPersistence` (default
`FlagPersistenceMode.persistenceUntilNetworkSuccess`) controls how reads resolve
against the persisted cache:

| Policy                           | Behavior                                                                                                                                                                                                                                                                                               |
| -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `networkOnly`                    | Never reads nor writes the cache on the serving path (identity changes still remove it - that is hygiene, not serving). Flags come only from network responses this session; a failed evaluation serves the fallback.                                                                                  |
| `networkFirst`                   | Prefers the network, but reads are synchronous, so a valid cache (inside the TTL) is served immediately like `persistenceUntilNetworkSuccess`. Strict about freshness: a cache past the TTL is dropped, never served.                                                                                  |
| `persistenceUntilNetworkSuccess` | Serves a valid cache immediately, so startup doesn't flicker; the first network success takes over and re-persists. A failed evaluation keeps the cache serving. Trades freshness for offline availability: an expired cache still serves while inside `CohorlyCore.FLAG_CACHE_MAX_STALE_MS` (7 days). |

The cache carries a `stored_at` timestamp and expires after
`CohorlyConfig.flagCacheTtlMs` (default **24 hours** - the Mixpanel reference
default shared by every Cohorly SDK). A legacy cache with no timestamp cannot
be dated, so it is served indefinitely rather than expired - the
graceful-upgrade path preserving the pre-TTL behavior - until the next
successful reload rewrites it in the stamped format. `identify()` and
`reset()` clear the previous identity's flags before fetching the new
identity's, so values never leak across users - `onFeatureFlags` listeners
are notified of the clear with an empty map, then again when the new
identity's flags land.

```kotlin
val variant = cohorly.getFeatureFlag("checkout-copy", fallback = "control")
val details = cohorly.getFeatureFlagDetails("checkout-copy")
// details.source: FlagSource.network | .persistence | .fallback
// details.enabled / details.payload; a fallback result reports
// enabled = truthiness of the fallback and payload = null
// details.persistedAt / details.ttlMs when source == FlagSource.persistence
```

A read that resolves to the fallback reports no exposure. Reads of known
flags emit `$feature_flag_called` once per (key, value) per identity, stamped
with `$feature_flag_source` (`"network"` or `"persistence"`) plus
`$feature_flag_persisted_at` and `$feature_flag_ttl_ms` for persistence reads;
the same value served first from persistence and then from the network is
reported once.

## Auto-captured properties

Every event includes `$lib` = `"android"`, `$lib_version`, `$os`,
`$os_version`, `$manufacturer`, `$brand`, `$model`, `$screen_dpi`,
`$screen_height`, `$screen_width`, `$app_version_string`,
`$app_build_number`, `$carrier`, `$has_nfc`, `$has_telephone`,
`$bluetooth_version` and `$google_play_services`. `$wifi` and
`$bluetooth_enabled` are only collected when the app holds the
corresponding permission. Geo properties are derived server-side from the
client IP.

## Automatic events

Opt-in via `trackAutomaticEvents = true` (default off, matching the iOS
and React Native SDKs): `$ae_first_open` (once ever), `$ae_updated` (on
version change) and `$ae_session` with `$ae_session_length` when the app
backgrounds after a session of 10s or more.

The offline queue survives process death (SharedPreferences, capped at
1000 events, oldest dropped) and follows the shared Cohorly retry
contract: exponential backoff on 429/5xx/network errors honoring
`Retry-After`, batch halving on 413, permanent drop on 400.
