@voltro/cli 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.
Files changed (96) hide show
  1. package/CHANGELOG.md +83 -0
  2. package/dist/apiBuild-5U7PxjLa.js +2 -0
  3. package/dist/{apiBuild-BU5cd-Fn.js → apiBuild-C5HhMAEm.js} +2 -2
  4. package/dist/bin.js +1 -1
  5. package/dist/{build-uRMZrT8z.js → build-DQr7SsKK.js} +21 -17
  6. package/dist/{checkCommand-C1CQc8lD.js → checkCommand-ByaOhdc8.js} +1 -1
  7. package/dist/{checkCommand-DC7k1_hP.js → checkCommand-CvSheSdb.js} +3 -3
  8. package/dist/{codegenCommand-kS3Twuc9.js → codegenCommand-CI0mpjxo.js} +1 -1
  9. package/dist/{codemodRunner-kNnh1I7_.js → codemodRunner-Drb-H-jr.js} +258 -159
  10. package/dist/{commands-cu34ENUT.js → commands-pCItjdHG.js} +21 -21
  11. package/dist/{dashboardCommand-gG1y4PFG.js → dashboardCommand-O-ToEgiH.js} +3 -3
  12. package/dist/{dataCommand-hNOGKz4K.js → dataCommand-mn1Zc88W.js} +1 -1
  13. package/dist/{dev-Ce2gEziv.js → dev-dSH-Pkb2.js} +1 -1
  14. package/dist/{dev-Dff_pdik.js → dev-kn9-7RXZ.js} +1 -1
  15. package/dist/doctorCommand-BVat2ejT.js +2 -0
  16. package/dist/{doctorCommand-CGjefPmk.js → doctorCommand-ClB7Ucz5.js} +13 -13
  17. package/dist/{dormancyCommand-DYgI0Kf-.js → dormancyCommand-DZupfuq-.js} +1 -1
  18. package/dist/{embeddingsCommand-yJivQSrI.js → embeddingsCommand-D8_GquVX.js} +1 -1
  19. package/dist/{envCommand-ORJUf8r_.js → envCommand-DRjVSXRp.js} +2 -2
  20. package/dist/{evolveCommand-8CuFBlbQ.js → evolveCommand-Cb0C2b3V.js} +2 -2
  21. package/dist/index.js +1 -1
  22. package/dist/{infoCommand-D02A4WI2.js → infoCommand-BlKnoY2D.js} +1 -1
  23. package/dist/{probeCommand-BUYzc0CT.js → probeCommand-DYsOFm62.js} +1 -1
  24. package/dist/{runtimeTrace-DDuKJgVi.js → runtimeTrace-BI3kdicc.js} +1 -1
  25. package/dist/{sdkgen-vq1fF9D_.js → sdkgen-DZ4zCtV4.js} +1 -1
  26. package/dist/serveCommand-CwK_6Wxt.js +2 -0
  27. package/dist/{serveCommand-Dr4KXmTS.js → serveCommand-DnxqCQ16.js} +3 -3
  28. package/dist/serveEntry.js +1 -1
  29. package/dist/{start-C75w6ftA.js → start-DE4ixCTi.js} +1 -1
  30. package/dist/{start-DNtIlBcn.js → start-bqT4GRH0.js} +286 -226
  31. package/dist/startEntry.js +1 -1
  32. package/dist/{updateCommand-Bw3mCUDT.js → updateCommand-B18ZQNFg.js} +1 -1
  33. package/dist/updateCommand-B4EgDD-m.js +2 -0
  34. package/dist/{webDev-CBN9KkyQ.js → webDev-CYjhmOTZ.js} +1 -1
  35. package/dist/{webDev-Bzb8ZFus.js → webDev-CuG3Ged-.js} +1 -1
  36. package/dist/{webhooksCommand-BPJQOGTe.js → webhooksCommand-Bs-HZ4wY.js} +1 -1
  37. package/package.json +37 -19
  38. package/templates/AGENTS.md +2 -2
  39. package/templates/agent-docs/_index.md +2 -2
  40. package/templates/agent-docs/_manifest.json +1 -1
  41. package/templates/agent-docs/data.md +10 -2
  42. package/templates/agent-docs/observability.md +7 -1
  43. package/templates/agent-docs/plugins/sentry.md +121 -17
  44. package/templates/agent-docs/schema-driven-ui.md +38 -10
  45. package/templates/agent-docs/whats-new.md +11 -41
  46. package/templates/apps/api-ai/package.json +6 -6
  47. package/templates/apps/api-auth/package.json +8 -8
  48. package/templates/apps/api-backend/package.json +7 -7
  49. package/templates/apps/api-backend-deactivation/package.json +7 -7
  50. package/templates/apps/api-backend-mail/package.json +8 -8
  51. package/templates/apps/api-backend-mariadb/package.json +9 -9
  52. package/templates/apps/api-backend-sqlite/package.json +8 -8
  53. package/templates/apps/api-backend-storage/package.json +8 -8
  54. package/templates/apps/api-cms/package.json +9 -9
  55. package/templates/apps/api-collab/package.json +8 -8
  56. package/templates/apps/api-data-advanced/package.json +8 -8
  57. package/templates/apps/api-durable/package.json +8 -8
  58. package/templates/apps/api-feature-flags/package.json +9 -9
  59. package/templates/apps/api-governance/package.json +8 -8
  60. package/templates/apps/api-kv/package.json +8 -8
  61. package/templates/apps/api-moderation/package.json +8 -8
  62. package/templates/apps/api-observability/package.json +8 -8
  63. package/templates/apps/api-ratelimit/package.json +8 -8
  64. package/templates/apps/api-rbac/package.json +8 -8
  65. package/templates/apps/api-rest/package.json +7 -7
  66. package/templates/apps/api-row-history/package.json +8 -8
  67. package/templates/apps/api-saas/package.json +11 -11
  68. package/templates/apps/api-saas-starter/package.json +10 -10
  69. package/templates/apps/api-search/package.json +8 -8
  70. package/templates/apps/api-status/package.json +8 -8
  71. package/templates/apps/api-webhooks/package.json +9 -9
  72. package/templates/apps/changelog/package.json +7 -7
  73. package/templates/apps/edge-functions/package.json +2 -2
  74. package/templates/apps/frontend-admin/package.json +7 -7
  75. package/templates/apps/frontend-app/package.json +8 -8
  76. package/templates/apps/frontend-auth/package.json +7 -7
  77. package/templates/apps/frontend-blank/package.json +6 -6
  78. package/templates/apps/frontend-cms/package.json +8 -8
  79. package/templates/apps/frontend-collab/package.json +9 -9
  80. package/templates/apps/frontend-contact/package.json +7 -7
  81. package/templates/apps/frontend-dashboard/package.json +6 -6
  82. package/templates/apps/frontend-docs/package.json +8 -8
  83. package/templates/apps/frontend-i18n/package.json +6 -6
  84. package/templates/apps/frontend-landing/package.json +7 -7
  85. package/templates/apps/frontend-portal/package.json +7 -7
  86. package/templates/apps/frontend-saas/package.json +7 -7
  87. package/templates/apps/frontend-spa/package.json +6 -6
  88. package/templates/apps/frontend-ssr/package.json +6 -6
  89. package/templates/apps/frontend-ssr-api/package.json +7 -7
  90. package/templates/apps/frontend-static-blog/package.json +8 -8
  91. package/templates/apps/frontend-status/package.json +7 -7
  92. package/templates/apps/mobile-app/package.json +4 -4
  93. package/dist/apiBuild-B9OvhITV.js +0 -2
  94. package/dist/doctorCommand-DXnNpKQT.js +0 -2
  95. package/dist/serveCommand-1R52mo6F.js +0 -2
  96. package/dist/updateCommand-inInBIAh.js +0 -2
@@ -1,3 +1,3 @@
1
1
  import { t as e } from "./loadEnv-D9nEOClM.js";
2
- import { t } from "./start-DNtIlBcn.js";
2
+ import { t } from "./start-bqT4GRH0.js";
3
3
  export { e as loadDotEnv, t as runStartCommand };
@@ -1,4 +1,4 @@
1
- import { n as e, r as t, t as n } from "./codemodRunner-kNnh1I7_.js";
1
+ import { n as e, r as t, t as n } from "./codemodRunner-Drb-H-jr.js";
2
2
  import { basename as r, dirname as i, join as a, relative as o, resolve as s } from "node:path";
3
3
  import { existsSync as c, readFileSync as l, readdirSync as u, statSync as d, writeFileSync as f } from "node:fs";
4
4
  import { totalmem as p } from "node:os";
@@ -0,0 +1,2 @@
1
+ import { m as e, p as t } from "./updateCommand-B18ZQNFg.js";
2
+ export { t as runApplyCodemods, e as runUpdateCommand };
@@ -2556,7 +2556,7 @@ The wrapper is what carries \`match\`, and what lets \`voltro doctor\` report wh
2556
2556
  return [
2557
2557
  "",
2558
2558
  "// Browser-side Sentry (frontend half of the deep integration). Errors +",
2559
- "// browser rpc spans flow to Sentry, sharing the server transactions' trace.",
2559
+ "// page-owned rpc spans linked to their server traces flow to Sentry.",
2560
2560
  n === void 0 ? `await initSentryBrowser(${JSON.stringify(r)})` : `await initSentryBrowser({ ...${JSON.stringify(r)}, ...(await __sentryOptions()) })`
2561
2561
  ];
2562
2562
  }, Fo = (e) => e.env ? ["import './env.public'"] : [], Io = (e) => {
@@ -1,2 +1,2 @@
1
- import { C as e, T as t, m as n } from "./webDev-CBN9KkyQ.js";
1
+ import { C as e, T as t, m as n } from "./webDev-CYjhmOTZ.js";
2
2
  export { n as loadConfig, e as tryRunWebServe, t as walkPagesTree };
@@ -222,7 +222,7 @@ createVerifier({ secret: [process.env.WEBHOOK_SECRET, process.env.WEBHOOK_SECRET
222
222
  ...t === void 0 ? {} : { payload: t }
223
223
  };
224
224
  }, S = u({ scope: "voltro:webhooks" }), C = ["--out", "--name"], w = async (e) => {
225
- let { walk: t, loadDiscovered: n } = await import("./dev-Dff_pdik.js"), { outgoingFromEvents: r } = await import("./webhookDiscovery-il9ti-HE.js");
225
+ let { walk: t, loadDiscovered: n } = await import("./dev-kn9-7RXZ.js"), { outgoingFromEvents: r } = await import("./webhookDiscovery-il9ti-HE.js");
226
226
  return r((await n(await t(e), { root: e })).events.map((e) => ({
227
227
  file: e.file,
228
228
  descriptor: e.descriptor
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@voltro/cli",
3
- "version": "0.59.0",
3
+ "version": "0.61.0",
4
4
  "description": "The `voltro` CLI — dev server, codegen, migrations, project scaffolding, agent-docs seeding, and production serve.",
5
5
  "keywords": [
6
6
  "voltro",
@@ -892,6 +892,24 @@
892
892
  "title": "UiStrings gained a required `connectAccount` section",
893
893
  "kind": "manual"
894
894
  },
895
+ {
896
+ "version": "0.60.0",
897
+ "id": "0.60.0/01_declared_failures_are_not_incidents",
898
+ "title": "Sentry no longer reports declared failures by default",
899
+ "kind": "manual"
900
+ },
901
+ {
902
+ "version": "0.61.0",
903
+ "id": "0.61.0/01_rpc_errors_have_kinds",
904
+ "title": "`RpcError` now carries an explicit failure kind",
905
+ "kind": "manual"
906
+ },
907
+ {
908
+ "version": "0.61.0",
909
+ "id": "0.61.0/02_sentry_rpc_spans_join_page",
910
+ "title": "Sentry rpc spans now join the active browser transaction",
911
+ "kind": "manual"
912
+ },
895
913
  {
896
914
  "version": "0.7.0",
897
915
  "id": "0.7.0/01_row-filter-load-failure",
@@ -938,24 +956,24 @@
938
956
  "@effect/platform-node": "^0.108.0",
939
957
  "@effect/sql": "^0.52.0",
940
958
  "@effect/workflow": "^0.19.0",
941
- "@voltro/ai": "0.59.0",
942
- "@voltro/cache": "0.59.0",
943
- "@voltro/client": "0.59.0",
944
- "@voltro/content": "0.59.0",
945
- "@voltro/data-transfer": "0.59.0",
946
- "@voltro/database": "0.59.0",
947
- "@voltro/env": "0.59.0",
948
- "@voltro/kv": "0.59.0",
949
- "@voltro/logger": "0.59.0",
950
- "@voltro/plugin-auth": "0.59.0",
951
- "@voltro/plugin-broadcast": "0.59.0",
952
- "@voltro/plugin-mail": "0.59.0",
953
- "@voltro/plugin-storage": "0.59.0",
954
- "@voltro/plugin-webhooks": "0.59.0",
955
- "@voltro/protocol": "0.59.0",
956
- "@voltro/runtime": "0.59.0",
957
- "@voltro/serverless": "0.59.0",
958
- "@voltro/workflow": "0.59.0",
959
+ "@voltro/ai": "0.61.0",
960
+ "@voltro/cache": "0.61.0",
961
+ "@voltro/client": "0.61.0",
962
+ "@voltro/content": "0.61.0",
963
+ "@voltro/data-transfer": "0.61.0",
964
+ "@voltro/database": "0.61.0",
965
+ "@voltro/env": "0.61.0",
966
+ "@voltro/kv": "0.61.0",
967
+ "@voltro/logger": "0.61.0",
968
+ "@voltro/plugin-auth": "0.61.0",
969
+ "@voltro/plugin-broadcast": "0.61.0",
970
+ "@voltro/plugin-mail": "0.61.0",
971
+ "@voltro/plugin-storage": "0.61.0",
972
+ "@voltro/plugin-webhooks": "0.61.0",
973
+ "@voltro/protocol": "0.61.0",
974
+ "@voltro/runtime": "0.61.0",
975
+ "@voltro/serverless": "0.61.0",
976
+ "@voltro/workflow": "0.61.0",
959
977
  "chokidar": "^5.0.0",
960
978
  "ioredis": "^5.11.1",
961
979
  "tinyglobby": "^0.2.17",
@@ -749,7 +749,7 @@ each plugin's own README.
749
749
 
750
750
  | Topic | Open | Summary |
751
751
  |---|---|---|
752
- | **What's new in 0.59.0** | `node_modules/@voltro/cli/templates/agent-docs/whats-new.md` | Everything that changed in this version. Read it before hand-rolling something the framework may now ship. |
752
+ | **What's new in 0.61.0** | `node_modules/@voltro/cli/templates/agent-docs/whats-new.md` | Everything that changed in this version. Read it before hand-rolling something the framework may now ship. |
753
753
  | AI | `node_modules/@voltro/cli/templates/agent-docs/ai.md` | How Voltro treats AI — agents, tools, streaming, RAG — all primitives over the same WebSocket as the rest of the framework. |
754
754
  | Authentication | `node_modules/@voltro/cli/templates/agent-docs/authentication.md` | How @voltro/plugin-auth wires password + session-cookie auth across api + web, plus the pluggable identity-strategy protocol. |
755
755
  | Caching | `node_modules/@voltro/cli/templates/agent-docs/caching.md` | Voltro's caching layer (@voltro/cache) — an always-on memory default, swappable Redis-compatible backends, a low-level wrap primitive, and automatic query-result invalidation. |
@@ -832,7 +832,7 @@ each plugin's own README.
832
832
  | row-history | `node_modules/@voltro/cli/templates/agent-docs/plugins/row-history.md` (or `node_modules/@voltro/plugin-row-history/README.md`) | Full row history + time-travel. audit() records who/when; row-history records what-changed-to-what — a value snapshot of every row on every write, with as-of queries. |
833
833
  | scim | `node_modules/@voltro/cli/templates/agent-docs/plugins/scim.md` (or `node_modules/@voltro/plugin-scim/README.md`) | SCIM 2.0 provisioning — Users + Groups REST endpoints so an enterprise IdP (Okta, Entra, OneLogin) can create / update / deactivate users in your app. |
834
834
  | search | `node_modules/@voltro/cli/templates/agent-docs/plugins/search.md` (or `node_modules/@voltro/plugin-search/README.md`) | Keep an external search index (Typesense / Meilisearch / Algolia) in sync with your tables via the ChangeEvent tap, query it tenant-scoped through a typed action + hook. |
835
- | sentry | `node_modules/@voltro/cli/templates/agent-docs/plugins/sentry.md` (or `node_modules/@voltro/plugin-sentry/README.md`) | Deep Sentry integration — errors correlated to the distributed trace, breadcrumbs from the log sink, and opt-in performance traces. |
835
+ | sentry | `node_modules/@voltro/cli/templates/agent-docs/plugins/sentry.md` (or `node_modules/@voltro/plugin-sentry/README.md`) | Deep Sentry integration — errors correlated to the distributed trace, breadcrumbs from the log sink, and controlled performance traces. |
836
836
  | soft-delete | `node_modules/@voltro/cli/templates/agent-docs/plugins/soft-delete.md` (or `node_modules/@voltro/plugin-soft-delete/README.md`) | The softDelete() schema mixin — deletedAt / deletedBy columns, delete() redirected to an UPDATE, default reads filtered, hardDelete() bypass. |
837
837
  | sso-saml | `node_modules/@voltro/cli/templates/agent-docs/plugins/sso-saml.md` (or `node_modules/@voltro/plugin-sso-saml/README.md`) | Enterprise SAML 2.0 SSO — SP-initiated login, ACS assertion consumer, SP metadata. Signature verification via @node-saml/node-saml; framework session minting built in. |
838
838
  | storage | `node_modules/@voltro/cli/templates/agent-docs/plugins/storage.md` (or `node_modules/@voltro/plugin-storage/README.md`) | File storage behind one StorageService — public objects served direct from the bucket/CDN, private objects gated by an access policy + per-object grants. S3 / MinIO (R2 and GCS via their S3 interop) / Azure / database / filesystem / memory providers, presigned URLs, a dashboard browser. |
@@ -9,7 +9,7 @@ each plugin's own README.
9
9
 
10
10
  | Topic | Open | Summary |
11
11
  |---|---|---|
12
- | **What's new in 0.59.0** | `node_modules/@voltro/cli/templates/agent-docs/whats-new.md` | Everything that changed in this version. Read it before hand-rolling something the framework may now ship. |
12
+ | **What's new in 0.61.0** | `node_modules/@voltro/cli/templates/agent-docs/whats-new.md` | Everything that changed in this version. Read it before hand-rolling something the framework may now ship. |
13
13
  | AI | `node_modules/@voltro/cli/templates/agent-docs/ai.md` | How Voltro treats AI — agents, tools, streaming, RAG — all primitives over the same WebSocket as the rest of the framework. |
14
14
  | Authentication | `node_modules/@voltro/cli/templates/agent-docs/authentication.md` | How @voltro/plugin-auth wires password + session-cookie auth across api + web, plus the pluggable identity-strategy protocol. |
15
15
  | Caching | `node_modules/@voltro/cli/templates/agent-docs/caching.md` | Voltro's caching layer (@voltro/cache) — an always-on memory default, swappable Redis-compatible backends, a low-level wrap primitive, and automatic query-result invalidation. |
@@ -92,7 +92,7 @@ each plugin's own README.
92
92
  | row-history | `node_modules/@voltro/cli/templates/agent-docs/plugins/row-history.md` (or `node_modules/@voltro/plugin-row-history/README.md`) | Full row history + time-travel. audit() records who/when; row-history records what-changed-to-what — a value snapshot of every row on every write, with as-of queries. |
93
93
  | scim | `node_modules/@voltro/cli/templates/agent-docs/plugins/scim.md` (or `node_modules/@voltro/plugin-scim/README.md`) | SCIM 2.0 provisioning — Users + Groups REST endpoints so an enterprise IdP (Okta, Entra, OneLogin) can create / update / deactivate users in your app. |
94
94
  | search | `node_modules/@voltro/cli/templates/agent-docs/plugins/search.md` (or `node_modules/@voltro/plugin-search/README.md`) | Keep an external search index (Typesense / Meilisearch / Algolia) in sync with your tables via the ChangeEvent tap, query it tenant-scoped through a typed action + hook. |
95
- | sentry | `node_modules/@voltro/cli/templates/agent-docs/plugins/sentry.md` (or `node_modules/@voltro/plugin-sentry/README.md`) | Deep Sentry integration — errors correlated to the distributed trace, breadcrumbs from the log sink, and opt-in performance traces. |
95
+ | sentry | `node_modules/@voltro/cli/templates/agent-docs/plugins/sentry.md` (or `node_modules/@voltro/plugin-sentry/README.md`) | Deep Sentry integration — errors correlated to the distributed trace, breadcrumbs from the log sink, and controlled performance traces. |
96
96
  | soft-delete | `node_modules/@voltro/cli/templates/agent-docs/plugins/soft-delete.md` (or `node_modules/@voltro/plugin-soft-delete/README.md`) | The softDelete() schema mixin — deletedAt / deletedBy columns, delete() redirected to an UPDATE, default reads filtered, hardDelete() bypass. |
97
97
  | sso-saml | `node_modules/@voltro/cli/templates/agent-docs/plugins/sso-saml.md` (or `node_modules/@voltro/plugin-sso-saml/README.md`) | Enterprise SAML 2.0 SSO — SP-initiated login, ACS assertion consumer, SP metadata. Signature verification via @node-saml/node-saml; framework session minting built in. |
98
98
  | storage | `node_modules/@voltro/cli/templates/agent-docs/plugins/storage.md` (or `node_modules/@voltro/plugin-storage/README.md`) | File storage behind one StorageService — public objects served direct from the bucket/CDN, private objects gated by an access policy + per-object grants. S3 / MinIO (R2 and GCS via their S3 interop) / Azure / database / filesystem / memory providers, presigned URLs, a dashboard browser. |
@@ -660,7 +660,7 @@
660
660
  {
661
661
  "slug": "sentry",
662
662
  "title": "Sentry",
663
- "description": "Deep Sentry integration — errors correlated to the distributed trace, breadcrumbs from the log sink, and opt-in performance traces.",
663
+ "description": "Deep Sentry integration — errors correlated to the distributed trace, breadcrumbs from the log sink, and controlled performance traces.",
664
664
  "pkg": "@voltro/plugin-sentry",
665
665
  "doc": "plugins/sentry.md",
666
666
  "module": "agent-docs/plugins/sentry.md"
@@ -1220,13 +1220,21 @@ try {
1220
1220
  }
1221
1221
  ```
1222
1222
 
1223
- **Trace id for debugging.** An error caught from `useMutation` / `useAction` carries a **non-enumerable `__voltroTraceId`** — the bridge to the server logs for that exact call:
1223
+ **Trace id for debugging.** An error caught from `useMutation` / `useAction`
1224
+ carries the **non-enumerable client-span id `__voltroTraceId`**:
1224
1225
 
1225
1226
  ```ts
1226
1227
  const traceId = (err as { __voltroTraceId?: string }).__voltroTraceId
1227
- // `voltro logs --trace <traceId>` to see the server-side span
1228
+ // If the server accepted the call: `voltro logs --trace <traceId>`
1228
1229
  ```
1229
1230
 
1231
+ For a declared handler error, the server necessarily accepted the call and the
1232
+ id joins to its span. For a transport error it may not: the connection can fail
1233
+ before the request arrived or after the handler ran but before its response
1234
+ returned. Use `kind` from
1235
+ [`useOnRpcError`](/docs/ui/client-utilities/use-on-rpc-error) for that distinction;
1236
+ the presence of the id alone proves only that the client span existed.
1237
+
1230
1238
  `errorTag` lives in `@voltro/protocol` rather than in the client for a reason
1231
1239
  worth knowing before you decide where your own error handling goes: **`_tag` is a
1232
1240
  wire concept, and protocol owns the wire.** So a shared error handler in a package
@@ -213,6 +213,12 @@ Trace continuity is **automatic and on by default** in `voltro dev` — a buffer
213
213
  - **api → api is automatic over HTTP.** An action or handler that calls another api via the framework `HttpClient` sends a W3C `traceparent` header; an inbound `*.webhook.tsx` route (an incoming HTTP handler) **continues** the caller's trace. So frontend → api Y → api Z (via webhook / HTTP) is **one** trace.
214
214
  - **Every log line carries it.** Handlers stamp `fields.traceId` onto each log line they emit, so [`voltro logs --trace <id>`](/docs/observability/cli) returns the whole chain across hops, in order.
215
215
 
216
+ The client span exists before the transport proves delivery. If a connection
217
+ fails, its `traceId` can therefore have no matching server span — or the server
218
+ may have completed the handler and lost only the response. In rpc-error policy,
219
+ use [`RpcError.kind`](/docs/ui/client-utilities/use-on-rpc-error), never the mere
220
+ presence of `traceId`, to distinguish transport failures from handler failures.
221
+
216
222
  ```
217
223
  client.mutation.placeOrder traceId=4bf92f35…
218
224
  └─ mutation.placeOrder traceId=4bf92f35… (api Y, ctx.request.traceId)
@@ -259,7 +265,7 @@ This is the fastest way to answer "where did this error come from?" — far fast
259
265
 
260
266
  The same trace continuity flows to a vendor APM when you install a deep-observability plugin — no `OTEL_*` env. Each contributes an OTel span-processor to the framework's tracer via `contributeObservability`, so the vendor runs as a **consumer** of the existing tracer (the in-app Traces panel keeps working) and receives the identical spans:
261
267
 
262
- - [`@voltro/plugin-sentry`](/docs/plugins/sentry) — every mutation/query/action error becomes a Sentry issue correlated to its `trace_id` + `span_id`, with the request's log lines as breadcrumbs; opt-in performance traces (`traces: true`).
268
+ - [`@voltro/plugin-sentry`](/docs/plugins/sentry) — every mutation/query/action error becomes a Sentry issue correlated to its `trace_id` + `span_id`, with the request's log lines as breadcrumbs; server performance traces are on by default (`traces: false` disables them). The browser integration keeps rpc markers inside the sampled page/navigation transaction and links each marker to its Effect/server trace.
263
269
  - [`@voltro/plugin-datadog`](/docs/plugins/datadog) — framework spans → the Datadog Agent's OTLP receiver (`traces: true`), plus log forwarding with `dd.trace_id` correlation.
264
270
 
265
271
  See [Observability › Routing traces to a vendor](/docs/observability/overview#routing-traces-to-a-vendor) for the contribution surface + the sampler caveat.
@@ -1,6 +1,6 @@
1
1
  # Sentry
2
2
 
3
- > Deep Sentry integration — errors correlated to the distributed trace, breadcrumbs from the log sink, and opt-in performance traces.
3
+ > Deep Sentry integration — errors correlated to the distributed trace, breadcrumbs from the log sink, and controlled performance traces.
4
4
 
5
5
 
6
6
 
@@ -9,9 +9,9 @@
9
9
  <!-- source: en/plugins/sentry.md -->
10
10
  ## Sentry
11
11
 
12
- _Deep Sentry integration — errors correlated to the distributed trace, breadcrumbs from the log sink, and opt-in performance traces._
12
+ _Deep Sentry integration — errors correlated to the distributed trace, breadcrumbs from the log sink, and controlled performance traces._
13
13
 
14
- `@voltro/plugin-sentry` is the deep Sentry integration: every mutation/query/action failure becomes a Sentry issue **correlated to the active distributed trace** (trace_id + span_id) and carrying the request's recent log lines as breadcrumbs. Opt into performance traces and the framework's spans flow to Sentry as transactions.
14
+ `@voltro/plugin-sentry` is the deep Sentry integration: every mutation/query/action failure becomes a Sentry issue **correlated to the active distributed trace** (trace_id + span_id) and carrying the request's recent log lines as breadcrumbs. The framework's spans flow to Sentry as transactions by default.
15
15
 
16
16
  Uses the official `@sentry/node` SDK in **OpenTelemetry-consumer mode** — it consumes the framework's existing tracer (it never becomes the global provider), so the in-app [Traces dashboard](/docs/observability/distributed-tracing) stays intact.
17
17
 
@@ -29,7 +29,7 @@ export default {
29
29
  // dsn defaults to SENTRY_DSN; without a valid DSN the plugin is a no-op.
30
30
  environment: 'production', // or SENTRY_ENVIRONMENT
31
31
  release: '1.4.2', // or SENTRY_RELEASE
32
- traces: true, // route the framework's spans → Sentry (default off)
32
+ traces: true, // route the framework's spans → Sentry (default on)
33
33
  tracesSampleRate: 1.0,
34
34
  profiling: true, // continuous CPU profiler via @sentry/profiling-node
35
35
  }),
@@ -46,7 +46,7 @@ export default {
46
46
  | `dsn` | `string` | `SENTRY_DSN` env | No valid DSN → the plugin is inert (no interceptors registered). |
47
47
  | `environment` | `string` | `SENTRY_ENVIRONMENT` env | Sentry `environment` tag + `deployment.environment` resource attribute. |
48
48
  | `release` | `string` | `SENTRY_RELEASE` env | Release-health + source-map matching; also `service.version`. |
49
- | `traces` | `boolean` | `false` | Route the framework's OTel spans to Sentry as transactions. Off → errors + breadcrumbs only. |
49
+ | `traces` | `boolean` | `true` | Route the framework's OTel spans to Sentry as transactions. Off → errors + breadcrumbs only. |
50
50
  | `tracesSampleRate` | `number` | `1.0` | Applied by the Sentry client (not a tracer sampler). Only relevant with `traces`. |
51
51
  | `profiling` | `boolean` | `false` | Continuous CPU profiler via `@sentry/profiling-node` (lazy; warns if not installed). |
52
52
  | `profilesSampleRate` | `number` | `1.0` | Only relevant with `profiling`. |
@@ -73,15 +73,15 @@ This guarantees the error links to its trace in Sentry **regardless** of the Eff
73
73
 
74
74
  The framework log sink feeds a bounded **per-trace** ring (capped per-trace + LRU + TTL), keyed on each log line's `traceId`. On an error the trace's breadcrumbs are attached to the event; traces that never error are reaped by the TTL. So a Sentry issue shows the exact log lines leading up to it — for that request only.
75
75
 
76
- ## Traces (opt-in `traces:true`)
76
+ ## Server traces
77
77
 
78
78
  The plugin contributes a `SentrySpanProcessor` to the framework's tracer (via the plugin observability surface — see [Observability › Routing traces to a vendor](/docs/observability/overview#routing-traces-to-a-vendor)). Every framework span becomes a Sentry transaction, sharing the same trace id the errors carry — so in Sentry you land on an error inside its full distributed trace.
79
79
 
80
80
  We deliberately do **not** install a `SentrySampler` as the tracer's sampler: that would gate recording for the WHOLE tracer and blind the framework's own in-app Traces buffer. The framework keeps its always-on sampler; `tracesSampleRate` is applied by the Sentry client.
81
81
 
82
- ## Full-stack browser backend in ONE Sentry trace
82
+ ## Correlating the browser page with server traces
83
83
 
84
- The plugin ships a browser half at `@voltro/plugin-sentry/web` so a click in the UI and the server work it triggers appear as **one trace** in Sentry. Turn it on in the web app's `app.config.ts`:
84
+ The plugin ships a browser half at `@voltro/plugin-sentry/web`. It records one page-load or navigation transaction with rpc child spans, while each rpc child links to the Effect trace continued by the server. This avoids a transaction and network envelope per rpc call without losing the connection between browser and server work. Turn it on in the web app's `app.config.ts`:
85
85
 
86
86
  ```ts
87
87
  // apps/web/app.config.ts
@@ -94,6 +94,8 @@ export default {
94
94
  environment: 'production',
95
95
  release: '1.4.2', // MUST match the api's `release` for unified release health
96
96
  tracesSampleRate: 1.0,
97
+ browserTracing: true,
98
+ rpcSpans: true,
97
99
  },
98
100
  }
99
101
  ```
@@ -102,7 +104,22 @@ The framework's generated entry calls `initSentryBrowser()` before mount (no cod
102
104
 
103
105
  What it does:
104
106
  - **Inits `@sentry/react`** with `browserTracingIntegration` — page loads + client navigations become Sentry transactions, and unhandled errors / promise rejections are captured by the SDK's default global handlers.
105
- - **Unifies the trace.** Every framework rpc call (`useMutation`/`useSubscription`/`useAction`) already wraps itself in an Effect client span and propagates that span's W3C `traceId`+`spanId` to the api (the api's `sentryPlugin({ traces:true })` turns the server spans into Sentry transactions under that same trace id). The browser half subscribes to the framework's client-trace bus and emits a browser-side Sentry span into the **same** trace, parented at the client span so Sentry's trace view shows the browser hop and the server transactions in one tree.
107
+ - **Keeps rpc spans page-owned.** Every framework rpc call (`useMutation`/`useSubscription`/`useAction`) already wraps itself in an Effect client span and propagates that span's W3C `traceId`+`spanId` to the api. The browser half adds a child to the currently active page/navigation transaction and attaches the Effect/server trace as a span link. If no browser transaction is active, it emits no rpc root transaction.
108
+ - **Honours sampling.** `tracesSampleRate` decides whether the page/navigation root is recorded; its rpc children inherit that decision. The bridge does not inject a sampled parent decision.
109
+
110
+ Browser trace controls:
111
+
112
+ | Option | Default | Effect |
113
+ |---|---|---|
114
+ | `tracesSampleRate` | `1.0` | Samples page-load/navigation transactions and therefore their rpc children. Error reporting is independent. |
115
+ | `browserTracing` | `true` | Installs page-load/navigation instrumentation. `false` also defaults `rpcSpans` to off. |
116
+ | `rpcSpans` | same as `browserTracing` | Adds rpc children to an active browser transaction. `false` keeps browser errors and page traces but removes the rpc bridge. Set it to `true` with `browserTracing:false` only if your app creates active Sentry transactions itself. |
117
+
118
+ At browser boot the plugin prints the effective values, including the mode:
119
+
120
+ ```text
121
+ [@voltro/plugin-sentry] browser active { tracesSampleRate: 1, browserTracing: true, rpcSpans: true, rpcSpanMode: 'page-child-with-server-link' }
122
+ ```
106
123
 
107
124
  > **Manual wiring** (no app.config field, e.g. an app that builds its own entry): call it yourself once at boot —
108
125
  > ```ts
@@ -157,9 +174,9 @@ in deployment env. Setting one as a deployment variable deploys cleanly and does
157
174
  nothing; `voltro start` warns when it finds one, naming whether the bundle
158
175
  carries that key at all.
159
176
 
160
- The two halves are independent: the api plugin alone gives you server traces + errors; add the web half for the browser hop. For the full frontend→backend waterfall, run **both** and both are on by default, so an app with the plugin on the api and `@voltro/plugin-sentry` as a dependency of the web app gets the whole trace from one `SENTRY_DSN` plus one `VOLTRO_PUBLIC_SENTRY_DSN`. Give both halves the same `release`.
177
+ The two halves are independent: the api plugin alone gives you server traces + errors; add the web half for page performance, browser errors, rpc markers, and links back to the server traces. Both tracing halves are on by default, so an app with the plugin on the api and `@voltro/plugin-sentry` as a dependency of the web app gets server transactions plus one sampled browser transaction per page/navigation from one `SENTRY_DSN` plus one `VOLTRO_PUBLIC_SENTRY_DSN`. Give both halves the same `release`.
161
178
 
162
- **Traces default to on, at `tracesSampleRate: 1.0`.** That is the coherent default rather than a generous one: the browser half already sampled every page load, navigation and rpc call at 1.0, so with the api half off each of those spans was emitted, paid for, and belonged to a trace with no server side a browser hop hanging off nothing. The rate is named on the boot line (`sentry active traces=true tracesSampleRate=1`) so a first boot shows what it is about to send; lower it with `tracesSampleRate`, or set `traces: false` to keep errors and breadcrumbs only.
179
+ **Traces default to on, at `tracesSampleRate: 1.0`.** That is explicit, not a production recommendation. Lower the rate independently on api and web to fit the trace budget. Set api `traces:false` to keep server errors and breadcrumbs only; set web `browserTracing:false` to keep browser error capture without automatic performance transactions; or use web `rpcSpans:false` to keep page performance while omitting rpc child spans.
163
180
 
164
181
  ## Source maps (every bundle, browser and server)
165
182
 
@@ -243,11 +260,97 @@ Two details worth knowing before the first upload:
243
260
  store, and it fails with `SSL peer certificate … unable to get local issuer
244
261
  certificate`. Add `ca-certificates` to the build stage.
245
262
 
246
- **What we have not measured:** how a given Sentry project resolves the uploaded
247
- artifacts end to end. `sentry-cli sourcemaps upload` matches on debug IDs it
248
- injects into the emitted JS, and the URL prefix is the older release-artifact
249
- fallback which is why the prefix is set correctly per directory rather than
250
- left to one default, and why this paragraph is here instead of a claim.
263
+ **Debug ids are what make a server frame resolve, and they need their own
264
+ step.** `sentry-cli sourcemaps upload` does **not** write them — `inject` is a
265
+ separate subcommand, and `upload` only uses ids that are already there, falling
266
+ back to matching on the artifact NAME when they are not.
267
+
268
+ That fallback cannot work for a server bundle: the artifact is named from
269
+ `--url-prefix` (`~/chunk-ABC.js`) while the frame carries the absolute path the
270
+ node process loaded (`/workspace/…/serveBundle/chunk-ABC.js`), and nothing
271
+ rewrites either side. Measured downstream before this was fixed: 4300 artifacts
272
+ uploaded, the release finalised, every frame still minified — nothing red, which
273
+ is the failure this page's own warning describes.
274
+
275
+ `voltro build` runs `inject` first now, so the ids are in both the shipped JS and
276
+ the map before anything is uploaded. The URL prefix stays as the fallback for the
277
+ browser bundle, whose frames really are URLs under `~/assets`.
278
+
279
+ ## The web server is a SECOND process — and it needs the DSN too
280
+
281
+ A framework app runs two server processes. `voltro serve` is the api, where
282
+ `sentryPlugin()` initialises the SDK through the plugin lifecycle. `voltro start`
283
+ is the **web server** — SSR, loaders, ISR, the revalidation legs — and it has no
284
+ plugin lifecycle at all.
285
+
286
+ It now initialises Sentry itself when `SENTRY_DSN` is set:
287
+
288
+ ```sh
289
+ SENTRY_DSN=… SENTRY_ENVIRONMENT=production SENTRY_RELEASE=1.4.2 voltro start
290
+ ```
291
+
292
+ ```
293
+ {"level":"info","msg":"sentry active","environment":"production","release":"1.4.2","traces":false,"profiling":false,"process":"web"}
294
+ ```
295
+
296
+ Same line and same field names as the api half, from the same function — one
297
+ vocabulary across both pods, plus `process` so a collector can tell them apart.
298
+
299
+ **Why this was a hole rather than a missing log line.** An SSR shell throw is
300
+ caught, logged, and answered with a 500. No browser ever renders it, so the
301
+ client-side ErrorBoundary bridge below cannot see it either: the error existed
302
+ only in the pod's stdout. Both ends of the integration worked and the middle was
303
+ dark.
304
+
305
+ Three things to know:
306
+
307
+ - **`@voltro/plugin-sentry` must be installed in the WEB app**, not only the api
308
+ — the web server resolves the shared init from it. With a DSN set and the
309
+ package missing, the boot says so and names the command; it does not fail.
310
+ - **`traces` is `false` here, deliberately.** The api half contributes a span
311
+ processor to the framework's tracer. The web server has no tracer, so
312
+ initialising with tracing on would put `traces: true` in a boot line while
313
+ nothing produces a span.
314
+ - **No DSN is silent.** On the api side `sentryPlugin()` in your config is a
315
+ declaration, so an inert one is worth a warning. Here there is no declaration
316
+ to contradict, and a line about an integration you did not ask for is noise.
317
+
318
+ **Re-run `voltro build` after upgrading.** Production `voltro start` loads the
319
+ app's precompiled start bundle, so the web half picks this up when that bundle
320
+ is rebuilt — not from the framework version alone.
321
+
322
+ ## What reaches Sentry — a declared failure does not
323
+
324
+ A procedure's `error:` union is a **contract**. Somebody wrote the outcome down,
325
+ the client gets it typed and branches on it, and it describes a state of the
326
+ world: `AccessDeniedError` on a team you are not a member of is the system
327
+ working. A **defect** is the opposite — nobody foresaw it. Sentry is built for
328
+ the second.
329
+
330
+ So a cause that carries only declared failures is **not** reported:
331
+
332
+ ```ts
333
+ sentryPlugin() // captureFailures: false — the default
334
+ sentryPlugin({ captureFailures: true }) // report them again
335
+ sentryPlugin({ captureFailures: (e) => (e as { _tag?: string })._tag === 'PaymentDeclined' })
336
+ ```
337
+
338
+ - **Defects are always reported**, including a defect that arrived beside a
339
+ failure — the rule is failures-*only*, so one cannot hide the other.
340
+ - **Both halves take the option.** A rejected call is an rpc error on the client
341
+ too, so `initSentryBrowser({ captureFailures })` applies the same rule to
342
+ `rpc.*` events that carry a `_tag`. Set them together, or the api goes quiet
343
+ while the browser keeps filing the same rejection.
344
+ - **Route render failures and `reportClientError` are never filtered.** Nobody
345
+ declared those, which is the whole distinction.
346
+ - **The boot line names it** — `sentry active … captureFailures=false` — because
347
+ a default the framework picks for you is one nobody finds again.
348
+
349
+ Why the default is off: an issue list that fills with the expected is one nobody
350
+ reads. Every invalid form value, every click on a resource without access and
351
+ every call on a deleted row was a `level: error` issue. If Sentry is where you
352
+ see errors at all, `captureFailures: true` is the one-line answer and it is a
353
+ reasonable choice.
251
354
 
252
355
  ## Catching errors — what's automatic, what's manual
253
356
 
@@ -257,7 +360,8 @@ Once the two halves are wired, most errors are captured **with no per-call code*
257
360
  |---|---|---|
258
361
  | rpc handler throw (mutation/query/action) | ✅ auto | server interceptor → Sentry, trace-correlated + breadcrumbs |
259
362
  | Unhandled browser error / promise rejection | ✅ auto | `@sentry/react` global handlers (after `initSentryBrowser`) |
260
- | **React render error** (any page, incl. catch-all `[...slug]`, `error.tsx` boundary) | ✅ auto | the framework's route ErrorBoundary publishes to a client-error bus → the Sentry web bridge captures it. React swallows boundary-caught errors before `window.onerror`, so this bridge is what makes them reach Sentry — tagged with the route + component stack. |
363
+ | **React render error, in the BROWSER** (any page, incl. catch-all `[...slug]`, `error.tsx` boundary) | ✅ auto | the framework's route ErrorBoundary publishes to a client-error bus → the Sentry web bridge captures it. React swallows boundary-caught errors before `window.onerror`, so this bridge is what makes them reach Sentry — tagged with the route + component stack. |
364
+ | **React render error, during SSR** — six sites: `ssr-shell`, `spa-layout-shell`, `render` (incl. a loader throw), `not-found-render`, `ppr-holes`, `swr-refresh` | ✅ auto, from the WEB server | a different process and a different path: the render never reaches a browser, so the row above cannot cover it. Captured by the web server's own Sentry (see the section above), tagged `voltro.process=web` and `voltro.stage`. The last three are `warn`-level — the request still serves something — and are reported anyway: quieter is not less real, and nothing else can see them. Needs `SENTRY_DSN` on the **web** deployment. |
261
365
  | **Loader failure** (page/layout `loader` reject) | ✅ auto | same route bus path |
262
366
  | Manual try/catch (event handler, async effect, anywhere) | ✋ one call | `reportClientError(error, context?)` |
263
367
  | **Every other server primitive** — REST route (`defineRestRoute`), aggregate, subscriber, schedule, workflow, webhook, startup | ✅ auto | each primitive's error path publishes to the framework's server-error bus → the plugin captures it, tagged `voltro.errorSource` + `voltro.name` (+ traceId for workflows) |
@@ -1194,10 +1194,20 @@ const { status, reportSuccess } = useConnectionStatus('app')
1194
1194
  `status` is `'connected' | 'degraded' | 'offline'`. `offline` is browser-reported
1195
1195
  (`navigator.onLine` plus its `online` / `offline` events) — reliable for "the
1196
1196
  network is gone", and it wins over `degraded`. `degraded` means an rpc on that
1197
- api failed and nothing has succeeded since; it is real evidence, not a guess.
1198
- Coming back online clears the failure count, since failures counted during an
1199
- offline window *are* that window. `reportSuccess()` clears it eagerly from a
1200
- place that knows a call went through.
1197
+ api failed with `kind: 'transport'` and no later call has proved the server
1198
+ reachable. Declared handler errors do **not** colour the connection; if already
1199
+ degraded, a handler reply clears it because the server demonstrably answered.
1200
+ Local client and unknown failures leave the connection state unchanged. Any rpc
1201
+ success also clears the failure count automatically. Coming back online clears
1202
+ it too, since failures counted during an offline window *are* that window.
1203
+ `reportSuccess()` remains the explicit escape hatch for another signal that
1204
+ proves a round trip succeeded.
1205
+
1206
+ Use this hook to **render aggregate connection state**. To classify the failure
1207
+ that is firing right now — for example, to avoid a data-error toast in the same
1208
+ tick the socket drops — branch on `event.kind` inside
1209
+ [`useOnRpcError`](/docs/ui/client-utilities/use-on-rpc-error). React state updates
1210
+ after the event and therefore cannot classify that event retroactively.
1201
1211
 
1202
1212
  Also returned: `online`, `failureCount`, `lastFailureAt`. There is deliberately
1203
1213
  no polling ping — the framework does not call the server just to colour an
@@ -1578,16 +1588,34 @@ import { useOnRpcError } from '@voltro/client'
1578
1588
  import { errorTag } from '@voltro/protocol'
1579
1589
 
1580
1590
  useOnRpcError('app', useCallback((e) => {
1591
+ if (e.kind === 'transport') {
1592
+ showConnectionLost()
1593
+ return
1594
+ }
1581
1595
  if (errorTag(e.error) === 'Unauthenticated') redirectToSignIn()
1582
1596
  }, []))
1583
1597
  ```
1584
1598
 
1585
- The event is `{ source, tag, error, traceId? }`. `source` is
1599
+ The event is `{ source, tag, error, kind, traceId? }`. `source` is
1586
1600
  `'mutation' | 'action' | 'subscription'`, so both unary calls and stream
1587
1601
  failures arrive here. `error` is the raw value — a `Schema.TaggedError`, a plain
1588
1602
  `Error`, or anything else that was thrown; `errorTag(err)` reads `_tag` off
1589
- tagged errors and returns `undefined` otherwise. `traceId` is the same id the
1590
- server logged, so a handler can point at `voltro logs --trace <id>`.
1603
+ tagged errors and returns `undefined` otherwise.
1604
+
1605
+ `kind` is `'transport' | 'handler' | 'client' | 'unknown'`:
1606
+
1607
+ - `transport` — @effect/rpc could not complete the exchange. Use this branch for
1608
+ connection banners and to suppress misleading data-error toasts.
1609
+ - `handler` — the rpc returned a declared failure, such as `Unauthenticated`.
1610
+ - `client` — framework work around the call failed locally.
1611
+ - `unknown` — a combined or opaque operation could not be classified honestly.
1612
+
1613
+ `traceId` identifies the **client call span**. When the server accepted the
1614
+ request, it propagates the same id and `voltro logs --trace <id>` can join the
1615
+ call to server logs. A transport error can happen before the server saw the
1616
+ request, or after it handled the request but before the response returned, so a
1617
+ `traceId` on `kind: 'transport'` is not proof that server logs exist. For the
1618
+ same reason the event deliberately has no `reachedServer` boolean.
1591
1619
 
1592
1620
  Two things to know. The hook re-subscribes whenever the listener **reference**
1593
1621
  changes, so define it with `useCallback` or at module scope unless you want that.
@@ -1598,9 +1626,9 @@ per api name.
1598
1626
  is also published on the client error bus, so a reporter — the Sentry browser
1599
1627
  integration, or anything wired with `subscribeClientErrors` — already sees it,
1600
1628
  under `source: 'rpc.mutation' | 'rpc.action' | 'rpc.subscription'` with the rpc
1601
- tag and `traceId` in `context`. This hook is for cross-cutting *policy* (redirect
1602
- on `Unauthenticated`, toast on a network failure), not for telemetry; bridging
1603
- the two by hand now reports twice.
1629
+ tag, `rpcErrorKind`, and optional `traceId` in `context`. This hook is for
1630
+ cross-cutting *policy* (redirect on `Unauthenticated`, toast on a network
1631
+ failure), not for telemetry; bridging the two by hand now reports twice.
1604
1632
 
1605
1633
  Everything is published, including a reconnect storm. If that is too loud for
1606
1634
  your reporter, gate it in the subscriber — which failures are worth an event is
@@ -1,4 +1,4 @@
1
- # What's new in 0.59.0
1
+ # What's new in 0.61.0
2
2
 
3
3
  Read this FIRST when a task touches an area you have not worked in recently.
4
4
  It is the cheapest way to notice that the framework grew the thing you were
@@ -9,51 +9,21 @@ BREAKING entries name a codemod; run `voltro update` to apply it.
9
9
 
10
10
  ### ⚠ BREAKING
11
11
 
12
- - **@voltro/cli** — **`web.sourcemaps` moved to the top level of `app.config.ts`, and now covers every bundle the build minifies.**
12
+ - **@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.
13
13
 
14
- It reached exactly one of them before: the vite CLIENT build. `voltro build` minifies four the browser bundle, the SSR bundle, the `voltro start` bundle, and for an api app the precompiled entry plus the serve bundle that `voltro serve` actually runs and none of the other three had a `sourcemap` option or a config path that could ask for one. `keepNames` rescues the identifiers that are read as strings and nothing else, so a frame out of the artefact production runs read `at Qo (serveEntry.js:236:76405)`.
14
+ **`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).
15
+ - **@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.
15
16
 
16
- The docs closed that hole with a sentence instead of a field: *server stack traces are un-minified already*. The built file says otherwise, and the server half is where it costs most — a schedule, a workflow step, a subscriber or a REST route has no browser caller whose frame could stand in for it.
17
+ **`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).
17
18
 
18
- A second field (`api.sourcemaps`) would have made the same mistake twice. "Do I want resolvable frames from this app" has one answer; how many bundles the build happens to emit is not the caller's business.
19
-
20
- Three details that come with it:
21
-
22
- - **The old spelling is REFUSED, not ignored.** A moved option that is silently dropped goes green, stops emitting maps, and is found by someone reading an unresolvable frame months later. - **Each output directory uploads under the prefix that matches how its files are named** — `~/assets` for the browser bundle, `~/` for the bundles a node process loaded. One prefix across both is wrong for one of them. - **The upload and sweep run at every exit of the build**, not after the client bundle. Sweeping there would have deleted the client maps before the server ones existed, leaving those neither uploaded nor deleted.
23
-
24
- `minifiedBundlesCarryMaps.test.ts` asserts the property over the SET: every bundler options object in the CLI that turns minification on must also decide `sourcemap`. That is what was missing — not one option, three, because nothing was watching the class.
25
-
26
- **`voltro update` carries you across this** — codemod `0.59.0/01_sourcemaps_moved_to_the_top_level`. 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.59.0).
27
- - **@voltro/ai** — **Speech has a cost path, and a built-in provider name can be taken over.**
28
-
29
- Three changes, one thread: the speech seam existed and nothing could reach it.
30
-
31
- **`registerSpeechProvider(name, provider, { replaceBuiltin: true })`.** Refusing `mock` / `openai` / `gateway` / `anthropic` outright was wrong, and the case that shows it is one the refusal could not see: a provider name is not always an alias the app chooses. `openai` is the segment the gateway puts in its model ids, so it is what a stored catalog's rows carry and what a picker submits — an app told to pick another name cannot, without rewriting persisted rows and live sessions. It keeps its own dispatch instead, for every provider rather than the colliding one. What the refusal protected is kept by putting the intent in the call.
32
-
33
- **Registered providers resolve BEFORE built-ins.** The dispatch decided from a hard-coded list of built-in names, so without this the flag would let you register `openai` and never route to it — declared, documented, wired at zero call sites.
34
-
35
- **`MeteredUsage` — a tally in characters or seconds.** Every speech vendor meters one of those; none meters tokens. A ledger that only knew tokens could not hold a speech call at all, so speech spend was not un-priced, it was *absent*. The token shape is untouched (it is what the AI SDK returns); the new shape is separate rather than a unit tacked onto `AiUsage`, because writing a character count into a field called `inputTokens` puts a lie in a row someone reads later.
36
-
37
- `_voltro_ai_usage` gains a `unit` column, and `costSource` gains `'unpriced'`: a non-token unit with no supplied price is written with the real count and a zero cost rather than a fabricated one, since a per-1M-token rate applied to a character count produces a number indistinguishable from a real one. Sum the count columns grouped by `unit`, never across it.
38
-
39
- BREAKING for a caller that had narrowed `CostBreakdown['costSource']` to two values or reads `estimateCostUsd`'s return positionally. No call site changes SHAPE — the token path takes the same arguments and returns the same numbers — but a narrowing is user-authored code, so this carries a `manual` codemod rather than `none`: nothing here can be transformed (whether the third member belongs in your union is your decision), and the reader who needs it is exactly the one a "touches no user-authored code" line would have sent away. It also names the query that does not fail but answers wrongly: summing `_voltro_ai_usage` counts ACROSS units now adds characters to tokens.
40
-
41
- **`voltro update` carries you across this** — codemod `0.59.0/02_costsource_gained_unpriced`. 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.59.0).
42
-
43
- ### Added
44
-
45
- - **@voltro/ai** — **`GatewayModelInfo.raw`** — the `/v1/models` entry exactly as it came over the wire, beside the normalised fields.
46
-
47
- Normalising is the right default: the union of `tags`, `supported_parameters` and `modalities` is framework knowledge, and every app on the gateway was writing those forty lines differently. But normalising is lossy by construction, and the fields it drops are not decoration — a picker built on the vendor's own capability tags, a pricing display that shows a structure we have not normalised, `zdr` and `no_training` kept apart instead of folded into `dataPolicy`.
48
-
49
- Without `raw`, such an app fetches the endpoint a second time and maintains a second parser of the same response. One field rather than three more normalised ones, because the alternative asks the same question again at every field the gateway adds.
19
+ ### Fixed
50
20
 
51
- Absent on the injected-provider path, for the same reason `capabilities` is: that seam never saw a response, and an empty object would turn "no wire data" into the claim that the wire carried nothing.
21
+ - **@voltro/cli** **Six SSR failure sites report to Sentry, not three.**
52
22
 
53
- ### Fixed
23
+ 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.
54
24
 
55
- - **@voltro/cli** **`@sentry/cli` 3.x loads.** The loader read one export spelling `module.exports = SentryCli`, which is 2.x. 3.x ships `exports.SentryCli = SentryCli`, so `mod.default` is a namespace object rather than the class, and a build with a source-map upload configured stopped with *could not be loaded*.
25
+ 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.
56
26
 
57
- The framework cannot pin its way out of this: `@sentry/cli` is FSL-1.1-MIT, so it is not ours to depend on and the app installs it — which means `pnpm add -D @sentry/cli`, the command we ourselves print, resolves the latest. Reading one spelling turned our own documented instruction into a failing build.
27
+ 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.
58
28
 
59
- All four positions are accepted now (both spellings, each of which `import()` presents twice), and the two failures are told apart: a package that resolved but had no readable constructor no longer says *install it*. That reading is what cost a pipeline locally `require()` resolves and the binary is there, so nothing about the install looks wrong.
29
+ `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.