# Events

The full event vocabulary: automatic, fixed manual, and custom events, plus the context stamped on every one.

Other guides: [quickstart.md](quickstart.md) · [revenuecat.md](revenuecat.md) · [agent-api.md](agent-api.md) · [apps.md](apps.md) · [apple-ads.md](apple-ads.md)

## Automatic — the SDK sends these; never call them yourself

| Event | When | Dashboard use |
|---|---|---|
| `first_open` | Once per install, at first `configure` | Installs headline; the install cohort every funnel stage is measured against; pricing-tier splits; install bars |
| `app_open` | Every new session: launch, or foreground after 30+ min in background | "Online now", activity chart, app-opens in the agent API |

Session length is measured from the `session_id` the SDK stamps on every event: the time between a session's consecutive events, ignoring any gap over 5 minutes. That last part matters for apps that log from a notification handler — the SDK only rotates `session_id` on a foreground transition, so without the cap one id can read as a 19-hour session. `first_open` and `install_attribution` fire before the id exists and sit outside every session.

| `install_attribution` | SDK captures the AdServices token at first open; the server resolves it against Apple's attribution API. Props: `resolved` + Apple's payload (`attribution`, `campaignId`, ...) | Acquisition card: Apple Ads vs organic, per-campaign split, ads-attributed revenue |

### Events that happen somewhere else

A Watch app has no SDK of its own — it relays to the phone over WatchConnectivity, and the phone stamps its own hardware model, which files the action under iPhone. Mark the relay point and the Devices card files it under Apple Watch instead:

```swift
ActivationPal.trackFromWatch("workout_started")          // shorthand
ActivationPal.track("set_logged", ["reps": 12], from: .watch)
```

`Origin` is `.phone`, `.watch`, `.widget`, `.vision` or `.mac`; it lands in `props.origin` and outranks the hardware model. A device that drives both its phone and its watch is counted under both, so that column can sum past the unique-device total.

## Fixed manual — helpers on `ActivationPal`

Names and prop keys are fixed; the funnel and paywall cards match on them exactly. Never reuse them for other meanings.

| Event | Call | Dashboard use |
|---|---|---|
| `onboarding_step` | `onboardingStep(_ index: Int, id: String, answer: String? = nil)` | One funnel row per step, ordered by `index`; `answer` values split under question steps |
| `onboarding_completed` | `onboardingCompleted()` | Onboarded headline; funnel "completed" stage |
| `paywall_shown` | `paywallShown(_ placement: String)` | Placements card (all users) + funnel paywall stage and the Paywall conv. headline (install cohort only) |
| `paywall_plan_selected` | `paywallPlanSelected(_ plan: String)` | Funnel stage, per-plan split |
| `paywall_purchased` | `paywallPurchased(_ plan: String)` | Funnel stage, per-plan split; the Conversion and Paywall conv. headlines; median `first_open` → first purchase |
| `paywall_dismissed` | `paywallDismissed()` | Event feed and per-user timelines |

Every funnel stage counts unique devices from the range's install cohort (`first_open` inside the range) — no stage exceeds 100% of installed. `paywall_shown` is also counted un-cohorted in the placements card.

Purchases, trials, renewals, and churn arrive server-side as `rc_*` events — [revenuecat.md](revenuecat.md).

## Custom

```swift
ActivationPal.track("workout_logged", ["type": "run", "duration_min": 42, "pro": true])
```

- Zero registration: any new name appears in the Events card (count + unique devices), queryable via [`get_events`](agent-api.md).
- Names: snake_case, past tense — `workout_logged`, `export_tapped`. Keep the set small and stable; a renamed event is a new event.
- Prop values: `String`, `Bool`, `Int`, `Double` only — anything else is silently dropped. Prefer low-cardinality strings; props are for slicing, not payloads.

## Context on every event

SDK-stamped: `app_version`, `build`, `os_version`, `device_model`, `locale`, `app_language` (the localization the app actually resolved to), `timezone`, `storefront` (App Store country; resolves shortly after launch), `installed_at`, `sdk_version`, `env` (`debug` / `testflight` / `release`), stable per-install `device_id`, per-session `session_id`. Server-added from the request IP: `country`, `city`. These power the device, geo, and language breakdowns.

## Delivery

Buffer in memory, persist to disk (survives force-quit), flush every 10 s / at 20 buffered events / on background; failed flushes retry. Never crashes the app, never blocks the main thread.
