@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.
- package/CHANGELOG.md +113 -0
- package/dist/apiBuild-B9OvhITV.js +2 -0
- package/dist/{apiBuild-CkpsCqIu.js → apiBuild-BU5cd-Fn.js} +50 -48
- package/dist/bin.js +1 -1
- package/dist/{build-51qBWfYY.js → build-BxAsLzyU.js} +347 -258
- package/dist/{checkCommand-C1EbNmwl.js → checkCommand-C1CQc8lD.js} +1 -1
- package/dist/{checkCommand-C30J2F18.js → checkCommand-DC7k1_hP.js} +1 -1
- package/dist/{codemodRunner-BKjCwGiN.js → codemodRunner-ClUqkhvR.js} +523 -390
- package/dist/{commands-DowWkYtd.js → commands-Ro4hYBHD.js} +12 -12
- package/dist/{dashboardCommand-gG1y4PFG.js → dashboardCommand-t8Qsebjr.js} +1 -1
- package/dist/{doctorCommand-5ULMpL10.js → doctorCommand-CGjefPmk.js} +2 -2
- package/dist/doctorCommand-DXnNpKQT.js +2 -0
- package/dist/{evolveCommand-DTawtTxf.js → evolveCommand-CThb0EZZ.js} +2 -2
- package/dist/index.js +1 -1
- package/dist/{infoCommand-BFksHunl.js → infoCommand-a0WkKcUY.js} +1 -1
- package/dist/{runtimeTrace-BUk5zMXY.js → runtimeTrace-DDuKJgVi.js} +1 -1
- package/dist/{sdkgen-CAhmmbjx.js → sdkgen-vq1fF9D_.js} +1 -1
- package/dist/serveCommand-1R52mo6F.js +2 -0
- package/dist/{serveCommand-DhtC7WBq.js → serveCommand-Dr4KXmTS.js} +1 -1
- package/dist/serveEntry.js +1 -1
- package/dist/{start-C75w6ftA.js → start-BhoDQ25C.js} +1 -1
- package/dist/{start-DNtIlBcn.js → start-CcdPRG8L.js} +418 -367
- package/dist/startEntry.js +1 -1
- package/dist/{updateCommand-CtuD5r1t.js → updateCommand-D9avpQFT.js} +1 -1
- package/dist/updateCommand-QMOirvXC.js +2 -0
- package/package.json +37 -19
- package/templates/AGENTS.md +1 -1
- package/templates/agent-docs/_index.md +1 -1
- package/templates/agent-docs/ai.md +50 -1
- package/templates/agent-docs/cli.md +7 -6
- package/templates/agent-docs/plugins/sentry.md +142 -16
- package/templates/agent-docs/whats-new.md +25 -168
- package/templates/apps/api-ai/package.json +6 -6
- package/templates/apps/api-auth/package.json +8 -8
- package/templates/apps/api-backend/package.json +7 -7
- package/templates/apps/api-backend-deactivation/package.json +7 -7
- package/templates/apps/api-backend-mail/package.json +8 -8
- package/templates/apps/api-backend-mariadb/package.json +9 -9
- package/templates/apps/api-backend-sqlite/package.json +8 -8
- package/templates/apps/api-backend-storage/package.json +8 -8
- package/templates/apps/api-cms/package.json +9 -9
- package/templates/apps/api-collab/package.json +8 -8
- package/templates/apps/api-data-advanced/package.json +8 -8
- package/templates/apps/api-durable/package.json +8 -8
- package/templates/apps/api-feature-flags/package.json +9 -9
- package/templates/apps/api-governance/package.json +8 -8
- package/templates/apps/api-kv/package.json +8 -8
- package/templates/apps/api-moderation/package.json +8 -8
- package/templates/apps/api-observability/package.json +8 -8
- package/templates/apps/api-ratelimit/package.json +8 -8
- package/templates/apps/api-rbac/package.json +8 -8
- package/templates/apps/api-rest/package.json +7 -7
- package/templates/apps/api-row-history/package.json +8 -8
- package/templates/apps/api-saas/package.json +11 -11
- package/templates/apps/api-saas-starter/package.json +10 -10
- package/templates/apps/api-search/package.json +8 -8
- package/templates/apps/api-status/package.json +8 -8
- package/templates/apps/api-webhooks/package.json +9 -9
- package/templates/apps/changelog/package.json +7 -7
- package/templates/apps/edge-functions/package.json +2 -2
- package/templates/apps/frontend-admin/package.json +7 -7
- package/templates/apps/frontend-app/package.json +8 -8
- package/templates/apps/frontend-auth/package.json +7 -7
- package/templates/apps/frontend-blank/package.json +6 -6
- package/templates/apps/frontend-cms/package.json +8 -8
- package/templates/apps/frontend-collab/package.json +9 -9
- package/templates/apps/frontend-contact/package.json +7 -7
- package/templates/apps/frontend-dashboard/package.json +6 -6
- package/templates/apps/frontend-docs/package.json +8 -8
- package/templates/apps/frontend-i18n/package.json +6 -6
- package/templates/apps/frontend-landing/package.json +7 -7
- package/templates/apps/frontend-portal/package.json +7 -7
- package/templates/apps/frontend-saas/package.json +7 -7
- package/templates/apps/frontend-spa/package.json +6 -6
- package/templates/apps/frontend-ssr/package.json +6 -6
- package/templates/apps/frontend-ssr-api/package.json +7 -7
- package/templates/apps/frontend-static-blog/package.json +8 -8
- package/templates/apps/frontend-status/package.json +7 -7
- package/templates/apps/mobile-app/package.json +4 -4
- package/dist/apiBuild-CDE4NXNi.js +0 -2
- package/dist/doctorCommand-B8H2MYCE.js +0 -2
- package/dist/serveCommand-BUXUtSF5.js +0 -2
- package/dist/updateCommand-eVKVHj3N.js +0 -2
package/dist/startEntry.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { n as e, r as t, t as n } from "./codemodRunner-
|
|
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";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@voltro/cli",
|
|
3
|
-
"version": "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.
|
|
930
|
-
"@voltro/cache": "0.
|
|
931
|
-
"@voltro/client": "0.
|
|
932
|
-
"@voltro/content": "0.
|
|
933
|
-
"@voltro/data-transfer": "0.
|
|
934
|
-
"@voltro/database": "0.
|
|
935
|
-
"@voltro/env": "0.
|
|
936
|
-
"@voltro/kv": "0.
|
|
937
|
-
"@voltro/logger": "0.
|
|
938
|
-
"@voltro/plugin-auth": "0.
|
|
939
|
-
"@voltro/plugin-broadcast": "0.
|
|
940
|
-
"@voltro/plugin-mail": "0.
|
|
941
|
-
"@voltro/plugin-storage": "0.
|
|
942
|
-
"@voltro/plugin-webhooks": "0.
|
|
943
|
-
"@voltro/protocol": "0.
|
|
944
|
-
"@voltro/runtime": "0.
|
|
945
|
-
"@voltro/serverless": "0.
|
|
946
|
-
"@voltro/workflow": "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",
|
package/templates/AGENTS.md
CHANGED
|
@@ -749,7 +749,7 @@ each plugin's own README.
|
|
|
749
749
|
|
|
750
750
|
| Topic | Open | Summary |
|
|
751
751
|
|---|---|---|
|
|
752
|
-
| **What's new in 0.
|
|
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.
|
|
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.
|
|
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
|
|
164
|
+
## Source maps (every bundle, browser and server)
|
|
165
165
|
|
|
166
|
-
|
|
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
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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
|
|
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
|
-
|
|
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) |
|