@voltro/cli 0.58.0 → 0.60.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 (83) hide show
  1. package/CHANGELOG.md +113 -0
  2. package/dist/apiBuild-B9OvhITV.js +2 -0
  3. package/dist/{apiBuild-CkpsCqIu.js → apiBuild-BU5cd-Fn.js} +50 -48
  4. package/dist/bin.js +1 -1
  5. package/dist/{build-51qBWfYY.js → build-BxAsLzyU.js} +347 -258
  6. package/dist/{checkCommand-C1EbNmwl.js → checkCommand-C1CQc8lD.js} +1 -1
  7. package/dist/{checkCommand-C30J2F18.js → checkCommand-DC7k1_hP.js} +1 -1
  8. package/dist/{codemodRunner-BKjCwGiN.js → codemodRunner-ClUqkhvR.js} +523 -390
  9. package/dist/{commands-DowWkYtd.js → commands-Ro4hYBHD.js} +12 -12
  10. package/dist/{dashboardCommand-gG1y4PFG.js → dashboardCommand-t8Qsebjr.js} +1 -1
  11. package/dist/{doctorCommand-5ULMpL10.js → doctorCommand-CGjefPmk.js} +2 -2
  12. package/dist/doctorCommand-DXnNpKQT.js +2 -0
  13. package/dist/{evolveCommand-DTawtTxf.js → evolveCommand-CThb0EZZ.js} +2 -2
  14. package/dist/index.js +1 -1
  15. package/dist/{infoCommand-BFksHunl.js → infoCommand-a0WkKcUY.js} +1 -1
  16. package/dist/{runtimeTrace-BUk5zMXY.js → runtimeTrace-DDuKJgVi.js} +1 -1
  17. package/dist/{sdkgen-CAhmmbjx.js → sdkgen-vq1fF9D_.js} +1 -1
  18. package/dist/serveCommand-1R52mo6F.js +2 -0
  19. package/dist/{serveCommand-DhtC7WBq.js → serveCommand-Dr4KXmTS.js} +1 -1
  20. package/dist/serveEntry.js +1 -1
  21. package/dist/{start-C75w6ftA.js → start-BhoDQ25C.js} +1 -1
  22. package/dist/{start-DNtIlBcn.js → start-CcdPRG8L.js} +418 -367
  23. package/dist/startEntry.js +1 -1
  24. package/dist/{updateCommand-CtuD5r1t.js → updateCommand-D9avpQFT.js} +1 -1
  25. package/dist/updateCommand-QMOirvXC.js +2 -0
  26. package/package.json +37 -19
  27. package/templates/AGENTS.md +1 -1
  28. package/templates/agent-docs/_index.md +1 -1
  29. package/templates/agent-docs/ai.md +50 -1
  30. package/templates/agent-docs/cli.md +7 -6
  31. package/templates/agent-docs/plugins/sentry.md +142 -16
  32. package/templates/agent-docs/whats-new.md +25 -168
  33. package/templates/apps/api-ai/package.json +6 -6
  34. package/templates/apps/api-auth/package.json +8 -8
  35. package/templates/apps/api-backend/package.json +7 -7
  36. package/templates/apps/api-backend-deactivation/package.json +7 -7
  37. package/templates/apps/api-backend-mail/package.json +8 -8
  38. package/templates/apps/api-backend-mariadb/package.json +9 -9
  39. package/templates/apps/api-backend-sqlite/package.json +8 -8
  40. package/templates/apps/api-backend-storage/package.json +8 -8
  41. package/templates/apps/api-cms/package.json +9 -9
  42. package/templates/apps/api-collab/package.json +8 -8
  43. package/templates/apps/api-data-advanced/package.json +8 -8
  44. package/templates/apps/api-durable/package.json +8 -8
  45. package/templates/apps/api-feature-flags/package.json +9 -9
  46. package/templates/apps/api-governance/package.json +8 -8
  47. package/templates/apps/api-kv/package.json +8 -8
  48. package/templates/apps/api-moderation/package.json +8 -8
  49. package/templates/apps/api-observability/package.json +8 -8
  50. package/templates/apps/api-ratelimit/package.json +8 -8
  51. package/templates/apps/api-rbac/package.json +8 -8
  52. package/templates/apps/api-rest/package.json +7 -7
  53. package/templates/apps/api-row-history/package.json +8 -8
  54. package/templates/apps/api-saas/package.json +11 -11
  55. package/templates/apps/api-saas-starter/package.json +10 -10
  56. package/templates/apps/api-search/package.json +8 -8
  57. package/templates/apps/api-status/package.json +8 -8
  58. package/templates/apps/api-webhooks/package.json +9 -9
  59. package/templates/apps/changelog/package.json +7 -7
  60. package/templates/apps/edge-functions/package.json +2 -2
  61. package/templates/apps/frontend-admin/package.json +7 -7
  62. package/templates/apps/frontend-app/package.json +8 -8
  63. package/templates/apps/frontend-auth/package.json +7 -7
  64. package/templates/apps/frontend-blank/package.json +6 -6
  65. package/templates/apps/frontend-cms/package.json +8 -8
  66. package/templates/apps/frontend-collab/package.json +9 -9
  67. package/templates/apps/frontend-contact/package.json +7 -7
  68. package/templates/apps/frontend-dashboard/package.json +6 -6
  69. package/templates/apps/frontend-docs/package.json +8 -8
  70. package/templates/apps/frontend-i18n/package.json +6 -6
  71. package/templates/apps/frontend-landing/package.json +7 -7
  72. package/templates/apps/frontend-portal/package.json +7 -7
  73. package/templates/apps/frontend-saas/package.json +7 -7
  74. package/templates/apps/frontend-spa/package.json +6 -6
  75. package/templates/apps/frontend-ssr/package.json +6 -6
  76. package/templates/apps/frontend-ssr-api/package.json +7 -7
  77. package/templates/apps/frontend-static-blog/package.json +8 -8
  78. package/templates/apps/frontend-status/package.json +7 -7
  79. package/templates/apps/mobile-app/package.json +4 -4
  80. package/dist/apiBuild-CDE4NXNi.js +0 -2
  81. package/dist/doctorCommand-B8H2MYCE.js +0 -2
  82. package/dist/serveCommand-BUXUtSF5.js +0 -2
  83. package/dist/updateCommand-eVKVHj3N.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-CcdPRG8L.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-BKjCwGiN.js";
1
+ import { n as e, r as t, t as n } from "./codemodRunner-ClUqkhvR.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-D9avpQFT.js";
2
+ export { t as runApplyCodemods, e as runUpdateCommand };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@voltro/cli",
3
- "version": "0.58.0",
3
+ "version": "0.60.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",
@@ -844,6 +844,18 @@
844
844
  "title": "rerank reaches the gateway through a ProviderConfig",
845
845
  "kind": "manual"
846
846
  },
847
+ {
848
+ "version": "0.59.0",
849
+ "id": "0.59.0/01_sourcemaps_moved_to_the_top_level",
850
+ "title": "`web.sourcemaps` moved to the top level of app.config.ts",
851
+ "kind": "transform"
852
+ },
853
+ {
854
+ "version": "0.59.0",
855
+ "id": "0.59.0/02_costsource_gained_unpriced",
856
+ "title": "`costSource` gained `unpriced`, and AI usage is counted in a unit",
857
+ "kind": "manual"
858
+ },
847
859
  {
848
860
  "version": "0.6.0",
849
861
  "id": "0.6.0/01_no-dev-session-secret",
@@ -880,6 +892,12 @@
880
892
  "title": "UiStrings gained a required `connectAccount` section",
881
893
  "kind": "manual"
882
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
+ },
883
901
  {
884
902
  "version": "0.7.0",
885
903
  "id": "0.7.0/01_row-filter-load-failure",
@@ -926,24 +944,24 @@
926
944
  "@effect/platform-node": "^0.108.0",
927
945
  "@effect/sql": "^0.52.0",
928
946
  "@effect/workflow": "^0.19.0",
929
- "@voltro/ai": "0.58.0",
930
- "@voltro/cache": "0.58.0",
931
- "@voltro/client": "0.58.0",
932
- "@voltro/content": "0.58.0",
933
- "@voltro/data-transfer": "0.58.0",
934
- "@voltro/database": "0.58.0",
935
- "@voltro/env": "0.58.0",
936
- "@voltro/kv": "0.58.0",
937
- "@voltro/logger": "0.58.0",
938
- "@voltro/plugin-auth": "0.58.0",
939
- "@voltro/plugin-broadcast": "0.58.0",
940
- "@voltro/plugin-mail": "0.58.0",
941
- "@voltro/plugin-storage": "0.58.0",
942
- "@voltro/plugin-webhooks": "0.58.0",
943
- "@voltro/protocol": "0.58.0",
944
- "@voltro/runtime": "0.58.0",
945
- "@voltro/serverless": "0.58.0",
946
- "@voltro/workflow": "0.58.0",
947
+ "@voltro/ai": "0.60.0",
948
+ "@voltro/cache": "0.60.0",
949
+ "@voltro/client": "0.60.0",
950
+ "@voltro/content": "0.60.0",
951
+ "@voltro/data-transfer": "0.60.0",
952
+ "@voltro/database": "0.60.0",
953
+ "@voltro/env": "0.60.0",
954
+ "@voltro/kv": "0.60.0",
955
+ "@voltro/logger": "0.60.0",
956
+ "@voltro/plugin-auth": "0.60.0",
957
+ "@voltro/plugin-broadcast": "0.60.0",
958
+ "@voltro/plugin-mail": "0.60.0",
959
+ "@voltro/plugin-storage": "0.60.0",
960
+ "@voltro/plugin-webhooks": "0.60.0",
961
+ "@voltro/protocol": "0.60.0",
962
+ "@voltro/runtime": "0.60.0",
963
+ "@voltro/serverless": "0.60.0",
964
+ "@voltro/workflow": "0.60.0",
947
965
  "chokidar": "^5.0.0",
948
966
  "ioredis": "^5.11.1",
949
967
  "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.58.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.60.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. |
@@ -9,7 +9,7 @@ each plugin's own README.
9
9
 
10
10
  | Topic | Open | Summary |
11
11
  |---|---|---|
12
- | **What's new in 0.58.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.60.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. |
@@ -185,6 +185,20 @@ const usable = models.filter(canExecute).filter((m) => m.capabilities?.vision)
185
185
 
186
186
  Each entry adds `capabilities`, `modalities`, `contextWindow`, `maxOutputTokens`, `releasedAt`, `knowledgeCutoff` and `dataPolicy`.
187
187
 
188
+ Plus **`raw`** — the entry exactly as it came over the wire.
189
+
190
+ The normalised fields are the shared answer to the questions every app on the gateway asks. `raw` is for the ones only one app asks: a chips row built from the vendor's own `tags`, a pricing display that shows a structure we have not normalised, `zdr` and `no_training` kept apart instead of folded into `dataPolicy`.
191
+
192
+ ```ts
193
+ const models = await readGatewayCatalog({ modality: 'language' })
194
+ const chips = models[0]?.raw?.tags // vendor's own vocabulary
195
+ const audioRate = (models[0]?.raw?.pricing as { audio_per_second?: string })?.audio_per_second
196
+ ```
197
+
198
+ Without it, an app with its own display fetches `/v1/models` a second time and keeps a second parser of the same response — and then has to decide again, at every field the gateway adds, whether to wait for a framework release. That is the decision `raw` removes, which is why it is one field rather than three more normalised ones.
199
+
200
+ It is absent on the injected-provider path (the seam a test uses), for the same reason `capabilities` is: that path never saw a response, and an empty object would turn "no wire data" into the claim "the wire carried nothing".
201
+
188
202
  `capabilities` is **unioned from three fields that each half-say it** — `tags`, `supported_parameters` and `modalities` — because they overlap and regularly disagree: a model tagged `reasoning` does not always carry the parameter, and the other way round. Taking either alone produces a false negative, and a false "cannot" removes a model from a picker with no way for anyone to find out why. That union is framework knowledge; without it every app writes the same forty lines, differently.
189
203
 
190
204
  `dataPolicy` carries `zeroRetention` and `noTraining` (`'all' | 'some' | 'none'`). They decide whether a model may see customer text, and an unreadable value reads as `'none'` — the question is "may this", and an unparseable answer is not a yes.
@@ -2589,7 +2603,42 @@ yield* generateSpeech({ text, provider: { name: 'elevenlabs', model: 'eleven_tur
2589
2603
 
2590
2604
  This ships **no** adapters, on purpose: bundling an ElevenLabs client would put the framework in the business of tracking somebody else's REST API, and the app already has the adapter. What the app deletes is its dispatch, not its adapters.
2591
2605
 
2592
- A registered provider receives every option the primitive already had — `voice`, `outputFormat`, `speed`, `language`, `instructions`, `providerOptions` — or the seam would be a narrower primitive wearing the same name. Built-in names (`mock`, `openai`, `gateway`, `anthropic`) cannot be shadowed: silently overriding one would make the same config mean different things depending on which module loaded first.
2606
+ A registered provider receives every option the primitive already had — `voice`, `outputFormat`, `speed`, `language`, `instructions`, `providerOptions` — or the seam would be a narrower primitive wearing the same name.
2607
+
2608
+ #### Taking over a built-in name
2609
+
2610
+ `mock`, `openai`, `gateway` and `anthropic` resolve without a registration, and shadowing one by accident would make the same config mean different things depending on which module loaded first. So it takes a flag:
2611
+
2612
+ ```ts
2613
+ registerSpeechProvider('openai', myOpenAiTts, { replaceBuiltin: true })
2614
+ ```
2615
+
2616
+ The flag exists because a provider name is not always an alias you choose. `openai` is the value 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. Refusing outright did not protect that app; it made it keep its own dispatch for every provider, not just the colliding one.
2617
+
2618
+ What the refusal was protecting is kept: the intent is now in the call, so an override is a decision someone wrote down rather than an accident of load order. Registered providers are resolved **before** built-ins — otherwise the flag would let you register a name and never reach it.
2619
+
2620
+ ### Cost — speech is not billed in tokens
2621
+
2622
+ `generateSpeech` never records a ledger row itself; it returns `usage` and you pass it to `recordAiUsage`, the same as `generateText`. The difference is the unit. Every speech vendor meters characters of input or seconds of audio, so a `usage` shape that only knew tokens could not hold the call at all — speech spend was not un-priced, it was **absent**.
2623
+
2624
+ A registered provider returns a `MeteredUsage`:
2625
+
2626
+ ```ts
2627
+ synthesize: (request) => Effect.succeed({
2628
+ audio,
2629
+ usage: { unit: 'characters', input: request.text.length },
2630
+ })
2631
+ ```
2632
+
2633
+ ```ts
2634
+ yield* recordAiUsage(store, {
2635
+ provider: 'elevenlabs', model: 'eleven_turbo_v3', operation: 'generateSpeech',
2636
+ usage: { unit: 'characters', input: 4200 },
2637
+ price: { inputPer1M: 30, outputPer1M: 0 }, // your rate, in your unit
2638
+ })
2639
+ ```
2640
+
2641
+ The ledger row carries `unit` beside the two counts. **A non-token unit is never priced by the token table**: without a `price` or an `actualCostUsd` the row is written `costSource: 'unpriced'` with the real count and a zero cost, because 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.
2593
2642
 
2594
2643
  ## As a durable step — `transcribeStep`
2595
2644
 
@@ -1455,13 +1455,14 @@ web: {
1455
1455
  // `dist/assets/` to the prefix on deploy. Filenames are content-hashed, so a
1456
1456
  // previous deploy's assets stay valid for a visitor mid-navigation.
1457
1457
  assetPrefix: 'https://cdn.example.com/_assets',
1458
-
1459
- // Emit `.map` files WITHOUT a `//# sourceMappingURL` comment, so nothing in
1460
- // the shipped JS points at them. For `plugin-sentry`: upload them in your
1461
- // deploy step and DELETE them before the image is built. `voltro start`
1462
- // refuses to serve a `.map` regardless, so a forgotten delete is not a leak.
1463
- sourcemaps: 'hidden',
1464
1458
  },
1459
+
1460
+ // Top level, NOT under `web:` — it covers every bundle the build minifies
1461
+ // (browser, SSR, start, and an api app's serve bundle). Emits `.map` files with
1462
+ // no `//# sourceMappingURL` comment, so nothing shipped points at them.
1463
+ // `plugin-sentry` can upload and sweep them for you; `voltro start` refuses to
1464
+ // serve a `.map` regardless, so a forgotten delete is not a leak.
1465
+ sourcemaps: 'hidden',
1465
1466
  http: {
1466
1467
  // Node hangs up an idle keep-alive connection after 5s; every proxy in front
1467
1468
  // holds one longer, and the request that lands in that window comes back as
@@ -161,17 +161,15 @@ The two halves are independent: the api plugin alone gives you server traces + e
161
161
 
162
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.
163
163
 
164
- ## Source maps (browser stack traces)
164
+ ## Source maps (every bundle, browser and server)
165
165
 
166
- The web bundle is minified, so browser errors arrive in Sentry with **minified** stack traces unless the maps are uploaded. `voltro build` does it:
166
+ Every bundle `voltro build` emits is minified, so an error arrives in Sentry with a **minified** stack trace unless the maps are uploaded. `voltro build` does it:
167
167
 
168
168
  ```ts
169
- // app.config.ts
170
- web: {
171
- sourcemaps: {
172
- mode: 'hidden',
173
- upload: { org: 'acme', project: 'web' },
174
- },
169
+ // app.config.ts — top level, for a web app AND an api app
170
+ sourcemaps: {
171
+ mode: 'hidden',
172
+ upload: { org: 'acme', project: 'web' },
175
173
  }
176
174
  ```
177
175
 
@@ -191,11 +189,9 @@ That emits the maps, uploads them through `@sentry/cli`, and then **removes them
191
189
  `app.config.ts` is TypeScript, so the ordinary branch is the answer:
192
190
 
193
191
  ```ts
194
- web: {
195
- sourcemaps: process.env.CI
196
- ? { mode: 'hidden', upload: { org: 'acme', project: 'web' } }
197
- : 'hidden',
198
- }
192
+ sourcemaps: process.env.CI
193
+ ? { mode: 'hidden', upload: { org: 'acme', project: 'web' } }
194
+ : 'hidden',
199
195
  ```
200
196
 
201
197
  `'hidden'` alone emits the maps and **keeps** them — right for reading a stack trace locally, and a leak in an image. Omit the field entirely and nothing is emitted, which is the default.
@@ -206,9 +202,138 @@ web: {
206
202
 
207
203
  It is read from `SENTRY_AUTH_TOKEN` and there is deliberately no `authToken` option: `app.config.ts` is a committed file, and a token with `project:releases` scope can write to every project in the org. Give it to the **build** (a CI secret, a Docker build secret) — not to the deployment, since the upload happens while the bundle is being built.
208
204
 
209
- **Install `@sentry/cli` yourself: `pnpm add -D @sentry/cli` in the web app.** The framework depends on it nowhere, and that is a licence decision rather than a packaging one — it is FSL-1.1-MIT, which restricts competing commercial use, so it must not sit in the dependency graph of a package we publish. A build with an upload configured and the package missing **refuses and names the command**, rather than skipping the upload and leaving you to find out months later that no frame resolves.
205
+ **Install `@sentry/cli` yourself: `pnpm add -D @sentry/cli` in the app being built.** The framework depends on it nowhere, and that is a licence decision rather than a packaging one — it is FSL-1.1-MIT, which restricts competing commercial use, so it must not sit in the dependency graph of a package we publish. A build with an upload configured and the package missing **refuses and names the command**, rather than skipping the upload and leaving you to find out months later that no frame resolves.
206
+
207
+ ### It covers the server too — and that is what it was missing
208
+
209
+ An earlier version of this page said server traces were un-minified already and
210
+ that source maps were a browser-only concern. That was wrong about the built
211
+ artefact, and the correction is the reason the option moved to the top level.
212
+
213
+ `voltro build` minifies **four** bundles, not one: the browser bundle, the SSR
214
+ bundle, the `voltro start` bundle, and — for an api app — the precompiled entry
215
+ plus the serve bundle that `voltro serve` actually runs. `keepNames` rescues the
216
+ handful of identifiers that are read as strings (Effect tags, error `name`s) and
217
+ nothing else, so a frame out of the serve bundle reads:
218
+
219
+ ```
220
+ at l (file:///…/serveBundle/serveEntry.js:79:51)
221
+ at Qo (file:///…/serveBundle/serveEntry.js:236:76405)
222
+ ```
223
+
224
+ The server half is also where it hurts most. A schedule, a workflow step, a
225
+ subscriber or a REST route has no browser caller whose frame could stand in for
226
+ it — a workflow that dies at 03:30 in a cron is only what its stack trace says.
227
+
228
+ So `sourcemaps` is one field at the top level of `app.config.ts` and it covers
229
+ everything the build minifies. **It replaces `web.sourcemaps`**, which reached
230
+ only the browser bundle; the build refuses the old spelling rather than ignoring
231
+ it, and `voltro update` moves it for you.
232
+
233
+ Two details worth knowing before the first upload:
234
+
235
+ - **Each output directory is uploaded under the prefix that matches how its
236
+ files are named.** `dist/assets` is fetched by a browser under `~/assets`; the
237
+ SSR, start and serve bundles are files a node process loaded and go up under
238
+ `~/`. Set `upload.urlPrefix` only if you know your project needs one value for
239
+ both.
240
+ - **`node:*-slim` images have no CA certificates.** `/etc/ssl/certs` is empty and
241
+ the `ca-certificates` package is not installed. Node carries its own roots, so
242
+ nothing in JS notices — but `sentry-cli` is a Rust binary that uses the system
243
+ store, and it fails with `SSL peer certificate … unable to get local issuer
244
+ certificate`. Add `ca-certificates` to the build stage.
245
+
246
+ **Debug ids are what make a server frame resolve, and they need their own
247
+ step.** `sentry-cli sourcemaps upload` does **not** write them — `inject` is a
248
+ separate subcommand, and `upload` only uses ids that are already there, falling
249
+ back to matching on the artifact NAME when they are not.
250
+
251
+ That fallback cannot work for a server bundle: the artifact is named from
252
+ `--url-prefix` (`~/chunk-ABC.js`) while the frame carries the absolute path the
253
+ node process loaded (`/workspace/…/serveBundle/chunk-ABC.js`), and nothing
254
+ rewrites either side. Measured downstream before this was fixed: 4300 artifacts
255
+ uploaded, the release finalised, every frame still minified — nothing red, which
256
+ is the failure this page's own warning describes.
257
+
258
+ `voltro build` runs `inject` first now, so the ids are in both the shipped JS and
259
+ the map before anything is uploaded. The URL prefix stays as the fallback for the
260
+ browser bundle, whose frames really are URLs under `~/assets`.
261
+
262
+ ## The web server is a SECOND process — and it needs the DSN too
263
+
264
+ A framework app runs two server processes. `voltro serve` is the api, where
265
+ `sentryPlugin()` initialises the SDK through the plugin lifecycle. `voltro start`
266
+ is the **web server** — SSR, loaders, ISR, the revalidation legs — and it has no
267
+ plugin lifecycle at all.
268
+
269
+ It now initialises Sentry itself when `SENTRY_DSN` is set:
270
+
271
+ ```sh
272
+ SENTRY_DSN=… SENTRY_ENVIRONMENT=production SENTRY_RELEASE=1.4.2 voltro start
273
+ ```
274
+
275
+ ```
276
+ {"level":"info","msg":"sentry active","environment":"production","release":"1.4.2","traces":false,"profiling":false,"process":"web"}
277
+ ```
278
+
279
+ Same line and same field names as the api half, from the same function — one
280
+ vocabulary across both pods, plus `process` so a collector can tell them apart.
281
+
282
+ **Why this was a hole rather than a missing log line.** An SSR shell throw is
283
+ caught, logged, and answered with a 500. No browser ever renders it, so the
284
+ client-side ErrorBoundary bridge below cannot see it either: the error existed
285
+ only in the pod's stdout. Both ends of the integration worked and the middle was
286
+ dark.
287
+
288
+ Three things to know:
289
+
290
+ - **`@voltro/plugin-sentry` must be installed in the WEB app**, not only the api
291
+ — the web server resolves the shared init from it. With a DSN set and the
292
+ package missing, the boot says so and names the command; it does not fail.
293
+ - **`traces` is `false` here, deliberately.** The api half contributes a span
294
+ processor to the framework's tracer. The web server has no tracer, so
295
+ initialising with tracing on would put `traces: true` in a boot line while
296
+ nothing produces a span.
297
+ - **No DSN is silent.** On the api side `sentryPlugin()` in your config is a
298
+ declaration, so an inert one is worth a warning. Here there is no declaration
299
+ to contradict, and a line about an integration you did not ask for is noise.
300
+
301
+ **Re-run `voltro build` after upgrading.** Production `voltro start` loads the
302
+ app's precompiled start bundle, so the web half picks this up when that bundle
303
+ is rebuilt — not from the framework version alone.
304
+
305
+ ## What reaches Sentry — a declared failure does not
306
+
307
+ A procedure's `error:` union is a **contract**. Somebody wrote the outcome down,
308
+ the client gets it typed and branches on it, and it describes a state of the
309
+ world: `AccessDeniedError` on a team you are not a member of is the system
310
+ working. A **defect** is the opposite — nobody foresaw it. Sentry is built for
311
+ the second.
312
+
313
+ So a cause that carries only declared failures is **not** reported:
314
+
315
+ ```ts
316
+ sentryPlugin() // captureFailures: false — the default
317
+ sentryPlugin({ captureFailures: true }) // report them again
318
+ sentryPlugin({ captureFailures: (e) => (e as { _tag?: string })._tag === 'PaymentDeclined' })
319
+ ```
210
320
 
211
- Server stack traces are un-minified already (the api ships readable JS), so this is a browser-only concern.
321
+ - **Defects are always reported**, including a defect that arrived beside a
322
+ failure — the rule is failures-*only*, so one cannot hide the other.
323
+ - **Both halves take the option.** A rejected call is an rpc error on the client
324
+ too, so `initSentryBrowser({ captureFailures })` applies the same rule to
325
+ `rpc.*` events that carry a `_tag`. Set them together, or the api goes quiet
326
+ while the browser keeps filing the same rejection.
327
+ - **Route render failures and `reportClientError` are never filtered.** Nobody
328
+ declared those, which is the whole distinction.
329
+ - **The boot line names it** — `sentry active … captureFailures=false` — because
330
+ a default the framework picks for you is one nobody finds again.
331
+
332
+ Why the default is off: an issue list that fills with the expected is one nobody
333
+ reads. Every invalid form value, every click on a resource without access and
334
+ every call on a deleted row was a `level: error` issue. If Sentry is where you
335
+ see errors at all, `captureFailures: true` is the one-line answer and it is a
336
+ reasonable choice.
212
337
 
213
338
  ## Catching errors — what's automatic, what's manual
214
339
 
@@ -218,7 +343,8 @@ Once the two halves are wired, most errors are captured **with no per-call code*
218
343
  |---|---|---|
219
344
  | rpc handler throw (mutation/query/action) | ✅ auto | server interceptor → Sentry, trace-correlated + breadcrumbs |
220
345
  | Unhandled browser error / promise rejection | ✅ auto | `@sentry/react` global handlers (after `initSentryBrowser`) |
221
- | **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. |
346
+ | **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. |
347
+ | **React render error, during SSR** (shell throw, loader throw, PPR/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`. Needs `SENTRY_DSN` on the **web** deployment. |
222
348
  | **Loader failure** (page/layout `loader` reject) | ✅ auto | same route bus path |
223
349
  | Manual try/catch (event handler, async effect, anywhere) | ✋ one call | `reportClientError(error, context?)` |
224
350
  | **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) |