# iOS SDK

CohorlySwift is a standalone Swift Package for Cohorly, supporting iOS 15+ and macOS 12+. It uses URLSession for transport, batches events, and persists its queue to UserDefaults by default.

## Install

`CohorlySwift` is distributed as a Swift Package - add it from the Cohorly
package repository.

### Xcode

File > Add Package Dependencies..., enter the repo URL, pick a version
rule, and add the **Cohorly** library product to your app target.

### Package.swift

```swift
dependencies: [
    .package(url: "https://github.com/Gitarcitano/cohorly-swift", from: "0.1.0"),
],
targets: [
    .target(
        name: "YourApp",
        dependencies: [.product(name: "Cohorly", package: "cohorly-swift")]
    ),
]
```

## Initialize

Call `Cohorly.initialize` once near app launch. It configures and returns
the shared instance (`Cohorly.shared`). `token` is optional - omit it if
you don't use per-project tokens.

```swift
import Cohorly

let cohorly = Cohorly.initialize(token: "YOUR_PROJECT_TOKEN")

cohorly.register(["app_version": "1.2.0"])
cohorly.track("Screen Viewed", properties: ["screen": "Home"])

cohorly.identify("user_123")
cohorly.people.set(["plan": "pro"])

// Manually flush (also happens automatically every 5s or every 20 events).
await cohorly.flush()
```

## API

| Member                                                          | Notes                                                                                                                                                                         |
| --------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `Cohorly.initialize(apiHost:token:...)`                         | Configures and returns `Cohorly.shared`. Also accepts `transport`, `storage`, `flushInterval`, `flushAt`.                                                                     |
| `track(_:properties:)`                                          | Queue an event.                                                                                                                                                               |
| `identify(_:)`                                                  | Set the distinct id. On the first call after being anonymous, also posts `/alias` linking the previous anonymous id (best effort, not retried). No-op if the id is unchanged. |
| `reset()`                                                       | Clears identity, super properties, and un-flushed events. Call this on logout - see [Identity](/reference/data-model#identity).                                               |
| `register(_:)` / `unregister(_:)`                               | Super properties merged into every event.                                                                                                                                     |
| `people.set / setOnce / increment / unset / deleteUser`         | Profile updates sent to /engage.                                                                                                                                              |
| `flush() async`                                                 | Manually flush; resolves once any auto-triggered flush completes.                                                                                                             |
| `currentDistinctId: String`                                     | Current distinct id.                                                                                                                                                          |
| `getFeatureFlag(_:fallback:)` / `isFeatureEnabled(_:fallback:)` | Read a flag's variant/enabled value; never hits the network.                                                                                                                  |
| `getFeatureFlagDetails(_:fallback:)`                            | The value plus `source` (`network` / `persistence` / `fallback`) and cache metadata.                                                                                          |
| `reloadFeatureFlags() async` / `onFeatureFlags(_:)`             | Re-evaluate flags / subscribe to updates.                                                                                                                                     |

## Feature flags

Flags are evaluated on `initialize` (opt out with `loadFeatureFlags: false`),
on `identify()` and on `reset()`, and cached in storage scoped by distinct id;
reads never hit the network.

`flagPersistence` (default `.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 `Cohorly.flagCacheMaxStale` (7 days). |

The cache carries a `stored_at` timestamp and expires after `flagCacheTTL`
(default **24 hours** - `Cohorly.defaultFlagCacheTTL`, 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.

```swift
let variant = cohorly.getFeatureFlag("checkout-copy", fallback: "control")
let details = cohorly.getFeatureFlagDetails("checkout-copy")
// details.source: .network | .persistence | .fallback
// details.enabled / details.payload; a fallback result reports
// enabled = truthiness of the fallback and payload = nil
// details.persistedAt / details.ttlMs when source == .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` = `"ios"`
- `$os`, `$os_version`, `$model`
- `$screen_width` / `$screen_height` (via UIKit where available - skipped
  on macOS/watchOS)

Events and profile updates are queued in memory and persisted to storage
(UserDefaults by default) so nothing is lost across relaunch. All mutable
state is confined to a private serial `DispatchQueue`, so the client is
safe to call from any thread.

## Crashes

Native crash reporting is an optional product of the same package,
`CohorlyCrash`. It adds a dependency on
[KSCrash](https://github.com/kstenerud/KSCrash) (its `Recording` module) and
records signals, Mach exceptions, uncaught `NSException`s and C++ exceptions.
The `Cohorly` product never links KSCrash, so apps that do not add
`CohorlyCrash` pay nothing for it.

A crash kills the process before anything can be sent, so the report is written
to disk at crash time and sent on the **next launch** as a `$exception` event
with level `fatal`. The upload goes through a background `URLSession`, which
finishes even after the app dies, so a crash loop at startup still reports.
MetricKit crash, hang and CPU diagnostics are sent too, with level `error` or
`warning`. Each report keeps the `$insert_id` generated at crash time, so a
resend is deduplicated by the server.

Add the `CohorlyCrash` product to your target, then install it right after
`initialize`:

```swift
import Cohorly
import CohorlyCrash

func application(_ application: UIApplication,
                 didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
    let cohorly = Cohorly.initialize(token: "YOUR_PROJECT_TOKEN")
    CohorlyCrash.install(cohorly: cohorly)   // right after initialize
    return true
}

// Optional: lets iOS wake the app when a crash upload finishes.
func application(_ application: UIApplication, handleEventsForBackgroundURLSession identifier: String,
                 completionHandler: @escaping () -> Void) {
    if !CohorlyCrash.handleEventsForBackgroundURLSession(identifier, completionHandler: completionHandler) {
        completionHandler()
    }
}
```

- **Crash-time values.** A crash carries the release, identity and device of the
  launch that crashed, never those of the launch that sends it. `install`
  follows `identify()` and `reset()` on its own.
- **Release.** It defaults to
  `<CFBundleIdentifier>@<CFBundleShortVersionString>+<CFBundleVersion>`;
  `install(cohorly:release:dist:)` overrides it.
- **Symbolication.** Upload each build's dSYMs. They are matched by the
  binary's debug id (UUID), so upload the dSYM of that exact build, for
  example `cohorly debug-files upload --release com.example.shop@1.4.0+27 path/to/Shop.app.dSYM`.
  Without them the in-app frames stay raw and the error issue is shown as
  unsymbolicated. dSYM processing runs on macOS only, see the [CLI](/cli).
- **One crash reporter per process.** Never install a second one alongside it,
  for example turn off another SDK's native crash handling.
- **Debugger.** Under the Xcode debugger KSCrash's Mach handler is off, so
  crashes arrive through the signal path instead of the field behavior.
- **Report limits.** KSCrash keeps a bounded number of reports (the SDK raises
  it to 10) and drops the oldest, so a crash loop that outruns sending can
  lose the oldest crashes. Payloads are capped at 20 threads and 100 frames per
  stack.

The result is grouped, symbolicated and triaged like every other error, see
[Error tracking](/error-tracking). The repository includes `Examples/CrashDemo`,
a sample app that crashes on demand.
