# You already have analytics. You still don't know where onboarding breaks.

You have an onboarding flow and you have a general-purpose analytics SDK. Between them you can
tell roughly how many people finish, as long as somebody remembered to fire the right event on
the right screen, and nobody has renamed one since. Observe is that, without the maintenance. You
name your onboarding screens once, in order, and every impression, exit, dwell time and paywall
view lands against that list. Drop-off is reported per screen, not per event name someone invented
in a hurry.

Your onboarding stays exactly as you built it. Observe adds one dependency and reads it.

## What comes back

- Sessions and drop-off at every screen, in your declared order.
- Median dwell per screen, timed by the SDK, not by you.
- Completion rate, paywall reach rate, and trial start rate.
- The same reads across app versions, so a release is a cohort.
- A trust verdict on the pipeline itself. An app with no events reads `no_data`, and `no_data`
  is never rendered as green.

That last one is the difference from an event bus. A general analytics SDK will keep showing you a
funnel after the events stop arriving, and the funnel will look like a drop in conversion.

## The whole integration

Three calls. Everything else on the surface is optional.

```swift
import WeirObserve

public enum Weir {
    public static func observe(
        appId: String, writeToken: String, endpoint: URL,
        flow: String = "onboarding",
        screens: [String],
        userId: String? = nil,
        flushInterval: TimeInterval = 30,
        sessionTimeout: TimeInterval = 1800     // 30 min backgrounded → abandon
    )
    public static func screen(_ name: String, index: Int? = nil,
                              properties: [String: JSONValue] = [:])
    public static func completed(properties: [String: JSONValue] = [:])
    public static func paywallShown(_ paywallId: String? = nil,
                                    properties: [String: JSONValue] = [:])
    public static func purchaseIntent(product: String,
                                      properties: [String: JSONValue] = [:])
    public static func purchaseResult(product: String, outcome: PurchaseOutcome,
                                      error: String? = nil)
    public static func track(_ name: String, _ properties: [String: JSONValue] = [:])
    public static func setUserId(_ id: String?)
    public static func flush()
}
```

Call `observe` once at launch, `screen` once per screen, `completed` where onboarding ends.
Android and React Native expose the same shape. The step-by-step version is the
[Observe quickstart](/docs/observe/).

The screen order is declared once and never passed per call. A hand-typed index is a second
source of truth that drifts from the real order, and the funnel is ordered by that index. A screen
name that is not in the declared array is recorded as undeclared and turns the app amber. It is
never absorbed into the funnel silently.

## Two claims, and how to check them

Both are internal verification runs on a Mac mini, against native Release builds. They are not
customer results. Weir is a controlled private alpha — see [Evidence status](/docs/evidence/) for
the full boundary.

### About fifteen minutes to integrate

The bar was fifteen agent minutes. Three consecutive cold runs came in at 86, 56 and 67 seconds.
Each run gave a fresh agent nothing but the public documentation and a sample app with no Weir
code in it. The run sandbox denies this repository, so an agent that consults the source fails the
run outright. Each agent added the calls itself, produced an iOS `Release` build, drove five
onboarding screens under Maestro, and rendered a funnel in the dashboard.

Retained at `demo/runs/rfc015-observe/rfc015-observe-20260901-010540Z-89978/` and its two
siblings under `demo/runs/rfc015-observe/`. Each summary ends `RFC-015 COLD OBSERVE PASS 9/9`.

### The numbers are correct, and the check is not ours to grade

A Release build was driven through 30 scripted device sessions: 12 completions, 10 abandons on
screen 3, 5 on screen 4, 3 reaching the paywall. The runner computed ground truth. The dashboard
was then read in a real browser and compared against it.

| Read | Ground truth | Rendered |
|---|---|---|
| Sessions per step | 30, 30, 30, 20, 15 | 30, 30, 30, 20, 15 |
| Drop-off per step | 0, 0, 0, 0.333, 0.25 | 0, 0, 0, 0.333, 0.25 |
| Completion rate | 0.4 | 40.0% |
| Paywall reach | 0.5 | 50.0% |
| Trial start | 0.1 | 10.0% |

Device dwell medians landed within 33 ms of the scripted sleeps, against a tolerance of 150 ms.
Retained at `demo/runs/rfc015-observe/rfc015-observe-dashboard-funnel-20260903-052012Z/`, with
the source device run at `docs/verification/rfc015-observe-device-funnel-2026-09-01.txt`.

Three failure modes were induced rather than asserted:

- **A misspelled screen name.** A build emitted `experiance` while declaring `experience`. One
  impression in 20 was flagged undeclared, trust went amber, and the dashboard labelled the step
  `Undeclared` instead of reordering the funnel.
  (`demo/runs/rfc015-observe/rfc015-observe-device-undeclared-20260903-050500Z/`)
- **A kill mid-flow and a 609-second network blackhole.** One session survived across two launches
  with contiguous sequence numbers and no duplicates. On recovery the queue drained to zero with
  no eviction: 18 events enqueued, 18 uploaded.
  (`demo/runs/rfc015-observe/rfc015-observe-loss-dedupe-20260903-045133Z/`)
- **An app with no data at all.** A registered Observe app with an empty events table reported
  `no_data` on the home read, the Overview, and the funnel page. It never reported green.
  (`demo/runs/rfc015-observe/rfc015-observe-configless-20260903-045500Z/`)

## What Observe does not do

- **It does not rewrite your onboarding.** No flow spec, no served config, no screens of ours in
  your app. `WeirObserve` depends on `WeirCore` only and never links the UI target.
- **It does not open a WebView.** There is no web runtime anywhere in the integration.
- **It does not fetch configuration or ship anything over the air.** Serving is a separate opt-in
  product, and adopting Observe does not adopt it.
- **It does not run experiments.** No variant assignment on an Observe app until you ask for it.
- **It does not replace your analytics.** Observe measures one flow. Keep your event bus for
  everything after onboarding.
- **It does not claim revenue.** `purchase_result` is a funnel marker, never a financial source
  of truth. `userId` is an opaque string you supply; the SDK invents no identifiers.

## Next

- [Observe quickstart](/docs/observe/) — the iOS, Android, and React Native integration, step by
  step.
- [Evidence status](/docs/evidence/) — what Weir does and does not claim today.
- [Monitoring](/docs/monitoring/) — the funnel and health reads an Observe integration watches.
