Error reporting (`see()`)
see (shipeasy error) is the structured error reporter. Every handled exception documents its product consequence, not just its stack. It works in vanilla JS…
/docs/ — also served raw at https://shipeasy-ai.github.io/sdk-ts/pages/error-reporting.md.see (shipeasy error) is the structured error reporter. Every handled
exception documents its product consequence, not just its stack. It works
in vanilla JS on both sides — the whole grammar hangs off one import:
import { see } from "@shipeasy/sdk/client"; // or "@shipeasy/sdk/server"Report a handled exception — see(e)
try {
await submitOrder(order);
} catch (e) {
see(e).causes_the("checkout").to("use cached prices").extras({ order_id: order.id });
}The chain dispatches on the next microtask — no .send(). It ships
immediately (sendBeacon in the browser, fire-and-forget fetch on the
server), spam-guarded by a 30s dedup window and a per-session cap.
.causes_the sets the subject; .to(outcome) is the terminal.
Where extras go in the chain
.causes_the(subject) and .to(outcome) are two halves of one sentence and
must stay adjacent. Because the chain dispatches on the next microtask,
TypeScript can take extras after the terminal — that is the preferred form
here:
// PREFERRED — the consequence reads as one sentence, extras hang off the end:
see(e).causes_the("checkout").to("use cached prices").extras({ order_id: order.id });
// Also fine — extras folded into the terminal inline:
see(e).causes_the("checkout").to("use cached prices", { order_id: order.id });Both produce the same report. Never split the sentence with extras:
// WRONG — extras wedged between the subject and the outcome. You read
// "checkout … order_id … use cached prices" and lose the consequence.
see(e).causes_the("checkout").extras({ order_id: order.id }).to("use cached prices");Trailing extras are a TypeScript affordance. In the other Shipeasy SDKs .to
is a hard terminal, so use the inline .to(outcome, extras) form (or
addExtras below) there.
Attach context from anywhere — addExtras()
To attach context without threading it into the catch block, buffer it earlier
with addExtras. Every see() report that fires later in the same scope
merges it in:
import { see, addExtras, clearExtras, runWithExtras } from "@shipeasy/sdk/server";
// server: wrap the request so addExtras is isolated per request (AsyncLocalStorage)
runWithExtras(async () => {
addExtras({ order_id: order.id, tenant: tenant.slug }); // from any layer, early
// ...later, deep in a service...
try {
await charge(order);
} catch (e) {
see(e).causes_the("checkout").to("use cached prices");
// report carries order_id + tenant automatically
}
});- Server — the buffer is backed by
AsyncLocalStorage, so concurrent requests never bleed into each other. Wrap each request inrunWithExtras(fn)(or wire theseeExtrasContextALS into your framework's request hook) to get a per-request scope. Outside such a scopeaddExtraswrites a module-level fallback buffer — callclearExtras()when the unit of work ends (job/script). - Browser — a single module-level buffer (one user per page). Import
addExtras/clearExtrasfrom@shipeasy/sdk/client; callclearExtras()on a client-side route change if you don't want the context to persist.
A chained .extras / .to extra of the same key overrides an ambient one
(chain merges over ambient); ambient extras are sanitized like any other.
Report a non-exception problem — see.Violation(name)
The name is a stable identifier (it participates in the issue fingerprint),
so put variable data in .extras(), never the name:
if (rows.length > LIMIT) {
see.Violation("large query")
.causes_the("search results")
.to("be trimmed")
.extras({ rows: rows.length });
}Never use see.Violation() for a caught exception — you'd drop the stack. Pass
the caught Error to see() instead.
Mark expected control flow — see.ControlFlowException(e)
Document an expected exception and report nothing (auto-capture skips marked
errors). The reason must start with "because":
try {
return decodeFoo(blob);
} catch (e) {
see.ControlFlowException(e).because("because it wasn't an encoded Foo");
return decodeBar(blob);
}Where reports land
The Shipeasy errors primitive — fingerprint-grouped issues (open / resolved / ignored, regression auto-reopens) with a near-real-time occurrence timeseries.
Client auto-capture & cross-network correlation
The client SDK does not mint issues of its own for failed requests — a bare
"request to /x failed" names the transport, not what broke for the user, so it's
unactionable. Reporting is always yours: see() the failure where you know the
consequence.
What the SDK does do (autoCollect: { errors }, on by default) is thread a
per-request correlation token so your report links to the backend across the
wire. This works no matter which HTTP client you use — the SDK instruments both
fetch and XMLHttpRequest, so axios (its default adapter), superagent,
jQuery.ajax, and native callers are all covered. On each same-origin request it
mints a token, sends it up on the X-SE-Correlation header (which a server
see() echoes), and stamps it onto the object that surfaces the failure:
- a network failure (offline / DNS / CORS) throws — the token is stamped on
the thrown error, so
see(err)picks it up automatically; - a 5xx over
fetchreturns aResponse— the token is stamped on theResponse, so a fetcher that throwsnew Error(msg, { cause: res })links via the.causechain; - a 5xx (or network failure) over
XMLHttpRequest— the token is stamped on theXMLHttpRequest, which axios and most wrappers expose on the thrown error as.request/.response.request, sosee(err)in yourcatchor axios interceptor picks it up with nothing threaded by hand.
Either way, once you see() the failure, that occurrence and the server-side
issue for the same request fold into one caused_by chain. The SDK also
deliberately does not blanket-report uncaught exceptions or unhandled promise
rejections (no actionable consequence).
Rules
- If you don't know the consequence, don't catch the exception.
- You may
see()then re-throw — the re-thrown error links to its inner report as acaused_bychain instead of double-counting. - Never put PII or high-cardinality data in
extras. - A
see()call beforeconfigure()/shipeasy()warns and drops — it never throws.
Kill switches (`getKillswitch`)
A kill switch is an operational on/off control that ships in the same KV blob as gates and configs. It is not user-bound — it answers a global "is this…
Testing
For unit tests, swap the live configure() for configureForTesting() — a drop-in sibling with no network, ever (no SDK key required). It replaces the active…