Docs
Get your DSN
Create a project, then copy its DSN from the project's settings page. It carries the host and the project id, so it is the only thing an SDK needs:
https://<key>@cyclopes.localhost.co.zw/ingest/<project-id>
The key is write-only: it can send events and nothing else, so shipping it inside a mobile app is fine. Read it from your environment rather than committing it, the same way you handle your other configuration.
Let your agent do it
If you use Claude Code or another coding agent, you do not have to do any of this by hand. Drop this skill into your repo and ask it to instrument the project:
$ mkdir -p .claude/skills/cyclopes
$ curl -o .claude/skills/cyclopes/SKILL.md https://cyclopes.localhost.co.zw/docs/skill/
It detects your stack, installs the right SDK and wires the minimum. Then it reads the app to work out which moments actually matter, the checkout, the sign-up, the upload, and comes back with one list of events and journeys for you to approve before it writes anything.
- It proposes before it edits, because a step name is expensive to change once data exists under it.
- It applies the naming rules below, so the names it picks stay usable.
- It knows an unordered journey needs exactly one entry step, which is the part people get wrong by hand.
Reading it first is worthwhile even if you instrument by hand: view the skill.
Servers
Journeys
Errors tell you what broke. Journeys tell you what people were trying to do. Three calls produce the whole activity stream:
identify({"id": user.id}) # who this is
screen("ProductDetail", {...}) # where they are
track("add_to_cart", {...}) # what they did
A journey is the set of steps that add up to something completed. You define one in the dashboard, not in code, so you can change what "done" means without shipping a release.
- Matching is unordered. A session completes when it has hit every required step, in any order. Real users wander, and insisting on a sequence would mark genuine completions as failures.
- Exactly one entry step, and it is the denominator. Pick the step meaning "this user is genuinely attempting this", so add_to_cart rather than Home.
- You get a completion rate, per-step attainment, and the blocker: the required step most often missing. That last one is the number to act on.
Naming things
Names are the axis everything groups by, so they have to be stable and low-cardinality. This is the one thing worth getting right first, because renaming a step after data exists under it is painful.
track("order_paid", {"order_id": order.id}) # one step, a property
track(f"order_{order.id}_paid") # one step per order
- Screens are named after the screen, not the URL: ProductDetail, not /p/8412.
- Actions read as past-tense facts: purchase_completed, invite_sent.
- Pick one convention and hold it. add_to_cart and addToCart are two different steps.
- Do not track everything. A flow map of forty screens is a hairball; a map of the eight that matter is a map.
Coming from Sentry
Keep your existing sentry-sdk and change one line, the DSN. Cyclopes speaks Sentry's ingest protocol, so an unmodified Sentry SDK works as it is.
sentry_sdk.init(dsn="<your-sentry-format-dsn>")
Copy the Sentry-format DSN from your project's settings page. Exceptions, stack frames, breadcrumbs, tags and contexts map straight across. Journeys are the exception: activity has no equivalent in Sentry's protocol, so that part needs a Cyclopes SDK.
Retention
Activity is kept for 90 days. A journey you defined last year still exists, but it can only be measured over the window that still has data behind it.
Stuck on any of this? Support, or write to cyclopes@localhost.co.zw.