Error reporting — `see()`
The Swift SDK ships the full see() structured-error surface. Every handled exception documents its product consequence, not just its stack. Reports are…
/docs/ — also served raw at https://shipeasy-ai.github.io/sdk-swift/pages/error-reporting.md.The Swift SDK ships the full see() structured-error surface. Every handled
exception documents its product consequence, not just its stack. Reports are
fire-and-forget POSTs; see() never blocks or throws into your code, and every
event is tagged with the side "client".
The chain
see(error).causesThe(subject).to(outcome, extras:) — to(_:extras:) is the
terminal: it builds the wire event and fire-and-forgets the report. Nothing
sends without it.
The package-level see(_:) reports against the client built by
configureClient(...):
do {
try chargeCard(order)
} catch {
see(error)
.causesThe("checkout")
.to("use cached prices", extras: ["order_id": order.id])
}Where extras go in the chain
causesThe(_:) and to(_:) are two halves of one sentence and must stay
adjacent, so fold the extras into the terminal (they merge like a final
.extras(...), later wins on a shared key):
// PREFERRED — the consequence reads as one sentence:
see(error).causesThe("checkout").to("use cached prices", extras: ["order_id": order.id])to(_:) returns Void, so extras cannot trail the terminal in Swift. And never
split the sentence with extras(_:) — the standalone setter still exists, but
reach for it only when you genuinely cannot pass the context inline:
// WON'T COMPILE — to(_:) returns Void:
// see(error).causesThe("checkout").to("use cached prices").extras(["order_id": order.id])
// WRONG — extras wedged between the subject and the outcome. You read
// "checkout … order_id … use cached prices" and lose the consequence.
// see(error).causesThe("checkout").extras(["order_id": order.id]).to("use cached prices")If see() is called before configureClient(...) has run, the error is dropped
(with a note to stderr).
Non-exception problems — seeViolation
Report a problem that isn't an Error. The name is a stable fingerprint key
— put variable data in .extras(), never in the name:
seeViolation("large query")
.causesThe("search results")
.to("be trimmed", extras: ["row_count": rows.count])Expected control flow — reports nothing
controlFlowException(error).because("...") marks an exception as expected and
reports nothing — use it to document a deliberate catch so it isn't mistaken
for an unhandled error. extras on the tail is stored for local debugging only,
never transmitted:
do {
try parse(token)
} catch {
controlFlowException(error)
.because("an expired token is normal — we re-issue below")
.extras(["path": "/refresh"])
// …re-issue
}Private attributes
Keys listed in privateAttributes on configureClient(...) are stripped from
.extras() before the report leaves the device (as they are from track()
payloads). See advanced.
Limits & spam guard
Reports are bounded per process: identical events within a 30s window collapse to
one send, and there's a hard cap on total sends per process. Messages, stacks,
subjects, and extras are truncated; extras are capped at 20 keys and limited to
String / finite-number / Bool values. Swift has no per-throw stack, so the stack
is captured best-effort at report time (it points at the see() call site).
see() is idempotent — calling .to(...) twice on the same chain sends once.
Kill switches
A kill switch reports whether a panic/disable switch is engaged. It returns a Bool — true means the switch is on (the thing it guards is killed). The value…
Testing
Tests run hermetically — no network, no UserDefaults — by constructing a ShipeasyClient directly with two injected dependencies: