cyclopes_
sign in sign up

Docs / Apps

Swift

No package server in the way: SwiftPM resolves straight from a git URL, so a tag is a release. One line already buys you a flush before iOS suspends you.

What you need first

A project's DSN. Create a project, then copy it from that project's settings page: it carries the host and the project id, so it is the only thing this SDK needs to be pointed at.

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.

Install

// Package.swift
.package(url: "https://cyclopes.localhost.co.zw/sdk-swift.git", from: "0.4.0")

There is no archive to serve and no version for this server to report: with SwiftPM the git tag is the version.

Wire it up

import Cyclopes

try? Cyclopes.start(Options(dsn: "https://<key>@cyclopes.localhost.co.zw/ingest/<project-id>"))

try? on purpose: an empty DSN returns nil and disables the SDK, which is how a debug build opts out. Only a malformed DSN throws, and that is a configuration mistake worth seeing at startup.

That one line already gives you a flush when the app is backgrounded, which is the single most valuable thing here. iOS suspends a backgrounded app within seconds and anything still buffered dies with it. It is why journeys otherwise stop one step short of wherever the user actually left.

Journeys

Cyclopes.identify(["id": user.id, "plan": user.plan])
Cyclopes.screen("Cart", ["items": cart.count])
Cyclopes.track("checkout_started", ["value": cart.total])
Cyclopes.warn("slow query", ["ms": elapsed])
  • identify applies to everything after it, not just its own row. (project, user_id) is one of only two join keys Cyclopes has.
  • screen sets where the user is, and every track after it says so. SwiftUI has no navigator to observe, so for a sheet, a tab or a step in a flow, set Cyclopes.currentScreen directly. That is how most screens get reported in a SwiftUI app.

Names must be stable and low-cardinality: track("order_paid", ["id": id]), never track("order_\(id)_paid").

Errors, and what Swift cannot tell us

Swift is the awkward case, and it is worth being straight about it. In every other language an exception carries the stack it was thrown from. A Swift Error is a value: no stack, no throw site, no cause chain, and try propagation leaves nothing behind.

So the trace is the stack where you called captureError, not where the error was created:

do {
    try checkout()
} catch {
    Cyclopes.captureError(error)   // call it in the catch that noticed
}

In practice those are the same place and the answer is right. Where they are not, the frames say who noticed rather than who caused it, which is the honest thing for an SDK to report rather than inventing a trace it does not have.

  • Symbols are mangled. Demangling needs a private runtime call. A mangled name still groups correctly, because grouping only needs it to be stable.
  • A cause chain only exists if you build one. NSUnderlyingErrorKey is followed, as is an underlyingError property on your own error type.

in_app

Swift frames carry a binary name rather than a path, so the heuristic excludes dylibs, the dynamic loader and the platform frameworks it knows. That list can only know what somebody thought of, so say which binary is yours:

options.inAppModules = ["MyApp"]

An explicit list is an answer rather than a guess: nothing else gets a vote.

Sessions

A session means one continuous sitting and rotates after 30 idle minutes. Rotation always flushes first: an envelope stamps its session id when it is built, so rotating with items still buffered would relabel them.

On a phone there is one user, so one session is right and there is nothing to scope. Use Cyclopes.setSession if your backend already has a session id you want the two halves to share.

Redaction

options.beforeSend = { event in
    var copy = event
    copy["user"] = nil
    return copy          // or nil to drop the event entirely
}

Sending it yourself

options.sender = { url, body in myQueue.enqueue(url, body) }

For an app that already batches its own network work. It is also the seam the conformance suite records through.

Guarantees

  • Never throws into your app past start.
  • Never blocks it. Sends go out on a background queue behind a bounded buffer; a full buffer drops rather than waits.
  • Drops rather than retries on 400, 401, 403, 413 and 429. Only transport errors and 5xx are retried, once.
  • Flushes on background and on terminate.