@voltro/client 0.59.0 → 0.61.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -39,6 +39,89 @@ _Changes staged for the next release accumulate here (rolled up from
39
39
 
40
40
  ---
41
41
 
42
+ ## [0.61.0] — 2026-08-31
43
+
44
+ ### ⚠ BREAKING
45
+
46
+ - **@voltro/client, @voltro/web, @voltro/cli** — `RpcError.kind` now distinguishes transport, handler, client, and unknown failures; manually emitted events must add the field, while `useConnectionStatus` now degrades only for transport failures and clears on the next rpc success.
47
+
48
+ **`voltro update` carries you across this** — codemod `0.61.0/01_rpc_errors_have_kinds`. If you pin versions by hand and never run it, print the notes without changing anything: `voltro update --codemods-only --from <your current version> --dry-run` (this one ships in 0.61.0).
49
+ - **@voltro/plugin-sentry, @voltro/cli** — Browser RPC spans now join the active page/navigation transaction and link to the Effect/server trace instead of creating one root transaction per call; `tracesSampleRate`, `browserTracing`, and the new `rpcSpans` switch control the resulting browser trace volume.
50
+
51
+ **`voltro update` carries you across this** — codemod `0.61.0/02_sentry_rpc_spans_join_page`. If you pin versions by hand and never run it, print the notes without changing anything: `voltro update --codemods-only --from <your current version> --dry-run` (this one ships in 0.61.0).
52
+
53
+ ### Fixed
54
+
55
+ - **@voltro/cli** — **Six SSR failure sites report to Sentry, not three.**
56
+
57
+ 0.60.0 gave the web server its own Sentry and wired the three loud sites: the SSR shell throw, the SPA layout shell throw, and the general render error (which covers a loader throw). The docs said "shell throw, loader throw, PPR/SWR refresh" — and an enumeration in a claim is read as exhaustive.
58
+
59
+ The other three were `warn`-level and reported to nobody: a failed PPR hole pass, a failed background SWR refresh, and a `not-found.tsx` that throws while rendering. They are quieter because the request still serves something — stale HTML, an unfilled hole, a plain 404 — which says something about the REQUEST and nothing about who else could find out. The answer to that is nobody: none of them reaches a browser boundary, so the pod's stdout was the only record.
60
+
61
+ They carry their own `voltro.stage` (`not-found-render`, `ppr-holes`, `swr-refresh`), so a quota-conscious project can drop them by stage without losing the three that fail the request.
62
+
63
+ `webSentry.test.ts` now fails if a render-path `log.error`/`log.warn` gains no reporter beside it — the property, rather than the six call sites.
64
+
65
+ ---
66
+
67
+ ## [0.60.0] — 2026-08-31
68
+
69
+ ### ⚠ BREAKING
70
+
71
+ - **@voltro/plugin-sentry** — **A declared failure no longer reaches Sentry by default, and the source-map upload now injects debug ids.** Two separate defects, both found by the same deployment on its first day of real server-side events.
72
+
73
+ **1. The contract was being reported as an incident.** The rpc interceptor skipped only clean interrupts; everything else went to `captureException`. So a failure declared in a procedure's `error:` union — the thing the client gets typed and branches on — arrived as `level: error`, `handled: yes`. The first server-side issue a deployment ever received was a person clicking a team they are not a member of.
74
+
75
+ Effect separates a failure from a defect, this framework leans on that split deliberately (a store refusal was made typed so an app could branch on it; an unlookupable conflict key was deliberately left a defect, because it is a broken call rather than a condition in the data), and a descriptor carries it in `error:`. Reporting both as an incident discarded that one layer up.
76
+
77
+ `shouldCapture` now skips a cause that is failures-ONLY. A defect is reported as before, including a defect that travelled beside a failure — the rule is failures-only rather than "any failure present" precisely so one cannot hide the other. `captureFailures: true` restores the old behaviour; a predicate keeps the ones that are signal.
78
+
79
+ **The browser half moved with it**, or the option would have been half-wired: a rejected call is an rpc error on the client too, published to the client error bus and captured by the browser bridge. `initSentryBrowser` takes the same option and applies it to `rpc.*` events carrying a `_tag`. Route render failures and `reportClientError` calls are never filtered — nobody declared those.
80
+
81
+ **2. `sentry-cli sourcemaps upload` does NOT write debug ids.** `inject` is a separate subcommand; `upload` only uses ids that are already present, and falls back to matching on the artifact NAME when they are not. That fallback cannot work for a server bundle: the artifact is named from `--url-prefix` (`~/chunk- ABC.js`) while the frame carries the absolute path the node process loaded, and nothing rewrites either side.
82
+
83
+ Measured downstream: 4300 artifacts uploaded, release finalised, every frame still minified. Nothing was red — the exact shape this code's own header warns about, an upload that matched nothing looking like one that worked. The comment above the uploader asserted the injection happened, which made it a description standing where a check belonged.
84
+
85
+ `inject` now runs first, over the same directories, and `sourcemapDebugIds.test.ts` drives the real binary to assert an id lands in both the JS and the map. `--url-prefix` stays as the fallback for the browser bundle, whose frames really are URLs.
86
+
87
+ The boot line names the new setting (`sentry active … captureFailures=false`), because a default the framework picks for you is one nobody finds again.
88
+
89
+ **`voltro update` carries you across this** — codemod `0.60.0/01_declared_failures_are_not_incidents`. If you pin versions by hand and never run it, print the notes without changing anything: `voltro update --codemods-only --from <your current version> --dry-run` (this one ships in 0.60.0).
90
+
91
+ ### Added
92
+
93
+ - **@voltro/cli, @voltro/plugin-sentry** — **The web server process now initialises Sentry and reports its own errors.**
94
+
95
+ A framework app runs two server processes. `voltro serve` is the api, where `sentryPlugin()` initialises the SDK through the plugin lifecycle. `voltro start` is the web server — SSR, loaders, ISR, the revalidation legs — and it has no plugin lifecycle, so it had none.
96
+
97
+ That was not a cosmetic difference in boot output. A web pod legitimately logs less than an api pod, because it has no store, no rpc, no scheduler, no workflows and no plugins. What it also had was **no error reporting**: an SSR shell throw is caught, logged and answered with a 500, so no browser ever renders it and the client-side ErrorBoundary bridge cannot see it either. Both ends of the integration worked and the middle was dark — while the docs' "React render error ✅ auto" row, true of the client path, read as covering all of them.
98
+
99
+ Set `SENTRY_DSN` on the web deployment and the boot says `sentry active` with the same message and the same field names as the api half, from the same function — `initSentryServer` in `@voltro/plugin-sentry/server`, which the api plugin now calls too. One init, because a copy of `skipOpenTelemetrySetup`, the traces default, the overrides-first spread and the degrade-on-missing-SDK path would have drifted the moment either half gained a case.
100
+
101
+ Three deliberate asymmetries, each because the two processes are not the same thing:
102
+
103
+ - **`traces: false` on the web side.** The api contributes a span processor to the framework's tracer; the web server has no tracer at all, so tracing on would put `traces: true` in a boot line while nothing produces a span. - **No DSN is SILENT here.** On the api side `sentryPlugin()` is a declaration and an inert one contradicts it. There is no declaration here. - **`@voltro/plugin-sentry` must be installed in the WEB app.** With a DSN set and the package missing, the boot names the command rather than failing — monitoring must never be what stops a deploy.
104
+
105
+ Measured against a real production `voltro start`, both branches. Note that production `voltro start` loads the app's precompiled start bundle, so a web deployment picks this up when that bundle is rebuilt — the framework version alone is not enough.
106
+
107
+ ### Fixed
108
+
109
+ - **@voltro/workflow, @voltro/runtime, @voltro/database, @voltro/cli, @voltro/plugin-sentry** — **Six raw writes removed from production log streams — and the guard that was supposed to catch them rewritten, because it was green for two independent reasons.**
110
+
111
+ A pod tail showed `[voltro:workflow] shard-lock coordination: row-based (dialect=mariadb, mode=row)` sitting between JSON records. `@voltro/logger` is what makes a line JSON in a pod and pretty on a TTY; a `process.stderr.write` bypasses that decision at exactly the place nobody looks, because a dev terminal renders both the same.
112
+
113
+ Fixed at the source: the workflow cluster layer (2), `rpcServer`'s computed-cache warning, the subscription outbox's and the RYW store's `warn`/`onError` defaults — those two are not fallbacks, the callers pass nothing, so the default IS the production path — and the migration file discovery's skip notice, which lands in the migrate job's stream.
114
+
115
+ **The guard is the part worth reading.** `prodLogDiscipline.test.ts` existed for this exact class and reported clean, for two reasons that had to be fixed separately:
116
+
117
+ - Its file set was a hand-written list of five. A guard that opts files IN says nothing about any file added after it was written. - Its matcher was LINE-LOCAL, so `process.stderr.write(` on one line and the `` `[tag] `` on the next never matched — 4 of the 9 call sites in the repo are written that way, including one in a file that WAS on the list. The guard had been pointed straight at an offender and called it clean.
118
+
119
+ It is opt-OUT now: every server-side package's source is scanned, exceptions carry a reason, and a second test fails if an exception's call site disappears — an allowlist entry for code that is gone reads as a rule with a hole in it.
120
+
121
+ Verified against a real `voltro serve` under `NODE_ENV=production`: 35 records, zero framework lines that are not JSON.
122
+
123
+ ---
124
+
42
125
  ## [0.59.0] — 2026-08-30
43
126
 
44
127
  ### ⚠ BREAKING
package/dist/index.d.ts CHANGED
@@ -602,8 +602,8 @@ export declare interface ClientTraceEvent {
602
602
  /** W3C trace id (32 hex) of the call the client just started. */
603
603
  readonly traceId: string;
604
604
  /** W3C span id (16 hex) of the client span — the parent the server
605
- * continues from. Lets a vendor integration (Sentry) place the
606
- * browser-side span in the SAME trace tree as the server transaction. */
605
+ * continues from. Lets a vendor integration correlate browser work to the
606
+ * server trace without changing the browser span's page-owned parent. */
607
607
  readonly spanId: string;
608
608
  /** `'mutation'` | `'action'` | `'subscription'` — which surface started it. */
609
609
  readonly source: 'mutation' | 'action' | 'subscription';
@@ -686,7 +686,7 @@ export declare interface ConnectionState {
686
686
  readonly status: ConnectionStatus;
687
687
  /** `navigator.onLine` (true during SSR — nothing better is knowable there). */
688
688
  readonly online: boolean;
689
- /** Consecutive rpc failures with no success in between. 0 when healthy. */
689
+ /** Consecutive transport failures with no server response or rpc success in between. */
690
690
  readonly failureCount: number;
691
691
  /** When the most recent failure was observed. */
692
692
  readonly lastFailureAt: number | undefined;
@@ -2497,10 +2497,14 @@ export declare interface RpcError {
2497
2497
  /** The raw error value. May be a Schema.TaggedError instance, a plain
2498
2498
  * Error, an unknown thrown value. Pattern-match on `_tag` for typed errors. */
2499
2499
  readonly error: unknown;
2500
- /** Trace id of the failed call the SAME id the server logged + the
2501
- * dashboards show. Lets a listener (or `DefaultErrorFallback`) point at
2502
- * `voltro logs --trace <id>`. Present when a client span was active
2503
- * (always, in practice — Effect's native tracer is ambient). */
2500
+ /** Failure origin. Use `transport` for connection-level UX; `handler` means
2501
+ * the rpc returned a declared failure; `client` is framework work that
2502
+ * failed locally; `unknown` is deliberately unclassified. */
2503
+ readonly kind: RpcErrorKind;
2504
+ /** Trace id of the CLIENT call span. When the server accepted the request,
2505
+ * @effect/rpc propagates this same id into its span. A transport failure may
2506
+ * happen before that, so the id is not proof that server logs exist. Present
2507
+ * only when a client span was active. */
2504
2508
  readonly traceId?: string;
2505
2509
  }
2506
2510
 
@@ -2542,6 +2546,12 @@ export declare class RpcErrorBus {
2542
2546
  emitSuccess(event: RpcSuccess): void;
2543
2547
  }
2544
2548
 
2549
+ /** Where the failure originated, classified only as far as the client can
2550
+ * prove. In particular, `transport` does NOT mean the server definitely saw
2551
+ * nothing: a connection can disappear after a request was accepted but before
2552
+ * its response reached the browser. */
2553
+ export declare type RpcErrorKind = 'transport' | 'handler' | 'client' | 'unknown';
2554
+
2545
2555
  export declare type RpcErrorListener = (event: RpcError) => void;
2546
2556
 
2547
2557
  /** A call that SUCCEEDED. Carries only its source and tag — there is no payload
@@ -3931,10 +3941,10 @@ export declare const useConnections: (apiName: string) => ConnectedAccountsContr
3931
3941
  * {status !== 'connected' && <OfflineBanner status={status} />}
3932
3942
  * ```
3933
3943
  *
3934
- * `degraded` clears as soon as any rpc on that api succeeds again call
3935
- * `reportSuccess()` from a place that knows a call went through if you want to
3936
- * clear it eagerly (the hook already clears it when the browser comes back
3937
- * online).
3944
+ * `degraded` clears as soon as any rpc on that api succeeds or returns a
3945
+ * declared handler failure both prove a server round trip. Call
3946
+ * `reportSuccess()` when another signal proves reachability (the hook also
3947
+ * clears when the browser comes back online).
3938
3948
  */
3939
3949
  export declare const useConnectionStatus: (apiName: string) => ConnectionState & {
3940
3950
  /** Clear the degraded state — call after a known-good round trip. */