---
name: cyclopes
description: Add Cyclopes to this codebase and instrument it. Wires up error reporting, logs and user journeys for whatever stack this project uses, then reads the app to work out which user actions are worth tracking and proposes journeys to measure. Use when someone wants error monitoring, analytics, funnels, journeys, or asks why users are dropping off.
---

# Instrumenting a codebase with Cyclopes

Cyclopes is a hosted service. The developer signs up, creates a project, and
copies a DSN. Nothing is installed on a server of theirs, and there is nothing
to deploy.

Your job has two halves, and they are not equally risky.

**Wiring the SDK** is mechanical, reversible and safe. Do it without ceremony.

**Instrumenting the app** is neither. Every `track` call you add is a name that
somebody will build a dashboard on, and a name is expensive to change once data
exists under it. So you propose first, in one list, and write nothing until the
human has said yes.

---

## Phase 1: wire it up

### Get the DSN

Ask for it if you do not have it. It looks like
`https://<key>@cyclopes.localhost.co.zw/ingest/<project-id>` and comes from the
project's settings page. Never invent one, and never commit it: read it from
the environment the way the project already reads its other secrets.

If the project has no obvious secret-handling convention, use an environment
variable and add it to `.env.example`.

### Pick the SDK

Detect the stack rather than asking. In a monorepo, wire each app separately.

| Found | Package | Import |
| --- | --- | --- |
| `pubspec.yaml` with `flutter:` | `cyclopes_flutter` | `package:cyclopes_flutter/cyclopes_flutter.dart` |
| `pubspec.yaml`, no Flutter | `cyclopes` | `package:cyclopes/cyclopes.dart` |
| `package.json` | `@cyclopes/sdk` | `@cyclopes/sdk` |
| `go.mod` | `cyclopes.localhost.co.zw/go` | `cyclopes "cyclopes.localhost.co.zw/go"` |
| `pyproject.toml`, `requirements.txt` | `cyclopes` | `import cyclopes` |

All of them install from Cyclopes' own package servers, not from the public
registries. The exact install line per ecosystem is on the project's settings
page and in the docs; use those rather than guessing a registry URL.

### Wire the minimum, and stop

The minimum is genuinely small, and it already covers uncaught errors, session
identity and the flush before a process dies. Get that working and confirm data
arrives before you write a single `track` call.

```dart
// Flutter: this is the whole integration
await CyclopesFlutter.init(dsn: dsn, environment: 'production', release: appVersion);
runApp(CyclopesFlutter.wrap(const MyApp()));
// and on MaterialApp: navigatorObservers: CyclopesFlutter.navigatorObservers
```

```python
# Python / Django: add the middleware, set CYCLOPES = {"dsn": ...}
cyclopes.init(dsn=dsn, environment="production", release=version)
```

```go
// Go: the middleware scopes a session per request and reports panics
cyclopes.Init(cyclopes.Options{DSN: dsn, Environment: "production"})
defer cyclopes.Flush(2 * time.Second)
http.ListenAndServe(":8080", cyclopes.Middleware(mux))
```

```js
// JavaScript: handlers for both runtimes are installed for you
cyclopes.init({ dsn, environment: "production", release: version });
```

**On a server, sessions must be per request.** A server SDK that mints one
session for the process merges every user's journey into one blob. Go's
`Middleware` and Python's `with client.session(...)` exist for this. If you are
wiring a server framework by hand, scope the session in the request lifecycle
or the journeys data is worthless.

**Frameworks that already have an error hook**: chain, never replace. If
something is already assigned to the global error handler, call it after
reporting.

### Confirm it works before going further

Trigger one real error and check it appears in the dashboard. If nothing
arrives, turn on `debug` and read what the SDK prints; a bad DSN is reported
and does not throw, by design.

---

## Phase 2: read the app and propose

Now read the codebase. Do not write anything yet.

### What you are looking for

You are not cataloguing features. You are looking for the handful of moments a
founder would ask about on a Monday morning. Concretely, hunt for:

1. **The commitment points.** Checkout, subscribe, upgrade, publish, submit,
   book, send, invite, upload. Anything that costs the user money, effort or
   reputation. These are almost always the required steps of a journey.
2. **The authentication boundary.** Sign up, sign in, verify. This is where
   `identify` belongs, and without it every user is anonymous and no journey
   can be followed across sessions.
3. **The screens between them.** The route table, the navigator, the page
   components. These become `screen` events and the nodes of the flow map.
4. **The abandonment suspects.** Long forms, payment steps, permission prompts,
   anything with a retry or an error branch. These are what a journey exists to
   measure.

Ignore: debug routes, health checks, admin panels, anything behind a staff
flag, and every internal helper. Instrumenting those adds noise and hides the
map.

### The naming rules, which are not negotiable

Names are the axis everything groups by, so they must be stable and
low-cardinality.

- **Never interpolate an id.** `order_1234_paid` fragments one step into
  thousands and no journey can ever match it. The id goes in properties:
  `track("order_paid", {"order_id": id})`.
- **Actions read as past-tense facts**: `purchase_completed`, `invite_sent`.
  Not `clickPayButton`.
- **Screens are named after the screen, not the URL**: `ProductDetail`, not
  `/p/8412`.
- **Pick one convention and hold it.** `add_to_cart` and `addToCart` are two
  different steps to Cyclopes. Match whatever the codebase already uses.
- **Do not track everything.** A flow map of forty screens is a hairball; a
  flow map of the eight that matter is a map. If you are proposing more than
  about fifteen events, you have not finished deciding.

Properties carry the varying part. Keep them low-cardinality too where they
will be filtered on (`plan`, `method`, `source`), and never put credentials,
tokens, full names, emails or message bodies in them.

### What to hand back

One table, and nothing written to disk yet:

```
EVENTS
  type      name                  where                          properties
  identify  (on sign-in)          auth/session.py:41             id, plan
  screen    ProductDetail         routes/product.tsx             sku
  track     add_to_cart           cart/actions.ts:88             sku, price
  track     checkout_started      checkout/page.tsx:24
  track     payment_submitted     checkout/pay.ts:130            method
  track     purchase_completed    orders/create.py:57            value, currency

JOURNEYS
  Checkout
    entry     add_to_cart          the first real commitment
    required  checkout_started
    required  payment_submitted
    required  purchase_completed
    optional  apply_coupon         visibility without failing sessions
    window    30 minutes
```

Then say plainly what you left out and why. "I did not instrument the admin
routes or the twelve settings screens" is useful; silently omitting them is
not.

### Proposing journeys correctly

A journey is the set of steps that add up to something completed. Three rules
decide whether your proposal will actually measure anything:

- **Matching is unordered.** A session completes when it has hit every required
  step, in any order. Do not propose a strict funnel; real users wander, and
  order carries no information here.
- **Exactly one entry step, and it is the denominator.** A session counts
  towards the journey only if it hit the entry step. Pick the step meaning
  "this user is genuinely attempting this flow", so `add_to_cart` rather than
  `Home`. Without an entry step the journey is a draft and is not measured.
- **Optional steps are for visibility**, not for steps you are unsure about. If
  a step is genuinely required for the thing to be done, mark it required.

A completion window is the maximum time between the first and last required
step. Sessions that hit everything but exceed it are reported as timed out
rather than failed, so set it to the time the flow honestly takes, and use `0`
when the flow can legitimately span days.

---

## Phase 3: apply what was approved

Only what was approved, and nothing extra.

- Put the call where the fact becomes true, not where the button was pressed.
  `purchase_completed` belongs after the payment provider confirms, not in the
  click handler, or you will count attempts as successes.
- On mobile, tag screens rather than calling on every route. Flutter has
  annotations and a generator that writes the wiring; it is less code and it
  produces a manifest that tells the dashboard your screen names before anyone
  has opened the app.
- Emit the screen before the actions taken on it. Every SDK stamps the current
  screen onto each `track`, which is what lets the dashboard say what people do
  on a screen rather than only which order they saw them in. If an action can
  fire from a modal, a tab or a sheet that is not a screen of its own, set the
  current screen around it (`set_current_screen`, `setCurrentScreen`,
  `SetScreen`, `Cyclopes.currentScreen`): the event order cannot recover it.
- Never let instrumentation change behaviour. No new `await` in a hot path, no
  exception escaping a `track` call, no reordering of business logic to suit an
  event.
- Keep the diff reviewable. Instrumentation spread across forty files in one
  commit will not get read properly.

Then tell the human to create the journeys in the dashboard under
**Journeys → Journeys → new journey**, using the table you proposed. The
dashboard is the source of truth for what a journey is; the code only has to
agree with it.

---

## Phase 4: prove it

Instrumentation that was never exercised is a guess. Run the flow yourself if
you can drive the app, or ask the human to walk through it once, then check:

- the events arrive, with the names you proposed and not variants of them;
- `identify` fired, so the session has a user on it;
- the journey reports a completion rather than sitting at zero.

If a step shows as **never seen**, the name in the dashboard and the name in
the code disagree. If it shows as **filter never matches**, the name is right
and the property filter is wrong. Both are visible on the journey detail page,
which is faster than guessing.

---

## Things that go wrong

| Symptom | Cause |
| --- | --- |
| Every user is anonymous | `identify` was never called at the auth boundary |
| All journeys are one session | a server SDK minting one session per process |
| Journeys stop one step short | the tail was lost when the process or app died; wire the flush |
| A step matches nothing | name mismatch, or the type is wrong (`track` saved where the app sends `screen`) |
| The flow map is a hairball | too many events; instrument the eight that matter |
| Thousands of one-event steps | an id was interpolated into a name |
