Error reporting — `see()`
The Java SDK ships the see() surface: structured error reporting that documents an error's product consequence, not just its stack. It mirrors @shipeasy/sdk…
/docs/ — also served raw at https://shipeasy-ai.github.io/sdk-java/pages/error-reporting.md.The Java SDK ships the see() surface: structured error reporting that
documents an error's product consequence, not just its stack. It mirrors
@shipeasy/sdk (TS) and the other server SDKs. Reports are POSTed
fire-and-forget to /collect.
The grammar
import static ai.shipeasy.See.see;
try {
chargeCard(order);
} catch (Exception e) {
see(e)
.causesThe("checkout")
.to("use the backup processor", Map.of("order_id", order.id()));
}see(problem)— start a report for a caughtThrowable(or any object)..causesThe(subject)— what part of the product is affected (default"app")..to(outcome, extras)— terminal. Builds the wire event and fire-and-forgets the send, with the extras folded in (structured context; merged on repeat, later wins). Calling.to()twice is a no-op; a chain that never calls.to()sends nothing..extras(Map)— standalone setter for the same context; reach for it only when you genuinely cannot pass the context inline.
Reporting never raises into your code — a failure in dispatch is swallowed and logged.
Where extras go in the chain
.causesThe(subject) and .to(outcome) are two halves of one sentence and must
stay adjacent, so fold the extras into the terminal:
// PREFERRED — the consequence reads as one sentence:
see(e).causesThe("checkout").to("use cached prices", Map.of("order_id", oid));.to() returns void, so extras cannot trail the terminal in Java — the call
below does not compile. And never split the sentence with .extras():
// WON'T COMPILE — .to() returns void:
// see(e).causesThe("checkout").to("use cached prices").extras(Map.of("order_id", oid));
// WRONG — extras wedged between the subject and the outcome. You read
// "checkout … order_id … use cached prices" and lose the consequence.
// see(e).causesThe("checkout").extras(Map.of("order_id", oid)).to("use cached prices");When the context already exists above the catch, prefer
See.addExtras over the inline
form — it keeps the catch site a clean one-liner.
Attach context from anywhere: See.addExtras
To attach context without threading it into the catch block, buffer it earlier
in the request with See.addExtras. Every see() report that fires later on the
same thread merges it in:
import ai.shipeasy.See;
// from any layer, early in the request
See.addExtras(Map.of("order_id", order.id(), "tenant", tenant.slug()));
// ...later, deep in a service...
try {
charge(order);
} catch (Exception e) {
See.see(e).causesThe("checkout").to("use cached prices");
// report carries order_id + tenant automatically
}The buffer is thread-local, so concurrent requests never bleed into each
other, and it merges into every report in the request (not just the first). A
chained .extras / inline .to extra of the same key overrides an ambient one.
AnonIdFilter clears the buffer at the end of each request (register it like any
servlet filter); in a background job or script call See.clearExtras() when a
unit of work ends. See.addExtras works even before an engine is configured and
never throws.
Dispatch
The static ai.shipeasy.See.see(...) dispatches against the engine that
Shipeasy.configure(...) built — no handle to pass. A global call before
configure runs logs a warning and returns a no-op chain — it never throws.
Violations (non-throwable problems)
Report a named problem that isn't an exception:
import static ai.shipeasy.See.violation;
violation("inventory_out_of_sync")
.causesThe("fulfillment")
.to("fall back to the nightly count");Expected control flow (reports nothing)
Mark a throwable as expected control flow so it is not reported — only the mark is stamped on the throwable:
import static ai.shipeasy.See.controlFlowException;
try {
return parse(input);
} catch (RetryableException e) {
controlFlowException(e).because("the upstream told us to retry");
throw e; // re-thrown; nothing is sent
}ControlFlowChain.isExpected(throwable) lets callers query the mark. Any
.extras() on the tail are local-debug only and never transmitted.
Limits
see() self-protects: messages/stacks are truncated, extras capped (20 keys,
200-char values), a 30s dedup window suppresses duplicates, and the per-process
cap is 25 sends. Private attributes (see Advanced) are stripped
from outbound extras.
Kill switches
getKillswitch reads an operational kill switch from the cached rules blob. A kill switch is the panic lever: true means "killed" (the protected path should…
Testing
Test mode is a drop-in sibling of Shipeasy.configure(...) with no network, ever (no api key needed): Shipeasy.configureForTesting(...) seeds the values your…