@voltro/plugin-sentry 0.59.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 +58 -0
- package/dist/index.d.ts +27 -1
- package/dist/index.js +85 -101
- package/dist/server-DQn_uTTk.js +57 -0
- package/dist/server.d.ts +58 -0
- package/dist/server.js +2 -0
- package/dist/web.d.ts +31 -0
- package/dist/web.js +25 -15
- package/package.json +10 -5
package/CHANGELOG.md
CHANGED
|
@@ -39,6 +39,64 @@ _Changes staged for the next release accumulate here (rolled up from
|
|
|
39
39
|
|
|
40
40
|
---
|
|
41
41
|
|
|
42
|
+
## [0.60.0] — 2026-08-31
|
|
43
|
+
|
|
44
|
+
### ⚠ BREAKING
|
|
45
|
+
|
|
46
|
+
- **@voltro/plugin-sentry** — **A declared failure no longer reaches Sentry by default, and the source-map upload now injects debug ids.** Two separate defects, both found by the same deployment on its first day of real server-side events.
|
|
47
|
+
|
|
48
|
+
**1. The contract was being reported as an incident.** The rpc interceptor skipped only clean interrupts; everything else went to `captureException`. So a failure declared in a procedure's `error:` union — the thing the client gets typed and branches on — arrived as `level: error`, `handled: yes`. The first server-side issue a deployment ever received was a person clicking a team they are not a member of.
|
|
49
|
+
|
|
50
|
+
Effect separates a failure from a defect, this framework leans on that split deliberately (a store refusal was made typed so an app could branch on it; an unlookupable conflict key was deliberately left a defect, because it is a broken call rather than a condition in the data), and a descriptor carries it in `error:`. Reporting both as an incident discarded that one layer up.
|
|
51
|
+
|
|
52
|
+
`shouldCapture` now skips a cause that is failures-ONLY. A defect is reported as before, including a defect that travelled beside a failure — the rule is failures-only rather than "any failure present" precisely so one cannot hide the other. `captureFailures: true` restores the old behaviour; a predicate keeps the ones that are signal.
|
|
53
|
+
|
|
54
|
+
**The browser half moved with it**, or the option would have been half-wired: a rejected call is an rpc error on the client too, published to the client error bus and captured by the browser bridge. `initSentryBrowser` takes the same option and applies it to `rpc.*` events carrying a `_tag`. Route render failures and `reportClientError` calls are never filtered — nobody declared those.
|
|
55
|
+
|
|
56
|
+
**2. `sentry-cli sourcemaps upload` does NOT write debug ids.** `inject` is a separate subcommand; `upload` only uses ids that are already present, and falls back to matching on the artifact NAME when they are not. That fallback cannot work for a server bundle: the artifact is named from `--url-prefix` (`~/chunk- ABC.js`) while the frame carries the absolute path the node process loaded, and nothing rewrites either side.
|
|
57
|
+
|
|
58
|
+
Measured downstream: 4300 artifacts uploaded, release finalised, every frame still minified. Nothing was red — the exact shape this code's own header warns about, an upload that matched nothing looking like one that worked. The comment above the uploader asserted the injection happened, which made it a description standing where a check belonged.
|
|
59
|
+
|
|
60
|
+
`inject` now runs first, over the same directories, and `sourcemapDebugIds.test.ts` drives the real binary to assert an id lands in both the JS and the map. `--url-prefix` stays as the fallback for the browser bundle, whose frames really are URLs.
|
|
61
|
+
|
|
62
|
+
The boot line names the new setting (`sentry active … captureFailures=false`), because a default the framework picks for you is one nobody finds again.
|
|
63
|
+
|
|
64
|
+
**`voltro update` carries you across this** — codemod `0.60.0/01_declared_failures_are_not_incidents`. If you pin versions by hand and never run it, print the notes without changing anything: `voltro update --codemods-only --from <your current version> --dry-run` (this one ships in 0.60.0).
|
|
65
|
+
|
|
66
|
+
### Added
|
|
67
|
+
|
|
68
|
+
- **@voltro/cli, @voltro/plugin-sentry** — **The web server process now initialises Sentry and reports its own errors.**
|
|
69
|
+
|
|
70
|
+
A framework app runs two server processes. `voltro serve` is the api, where `sentryPlugin()` initialises the SDK through the plugin lifecycle. `voltro start` is the web server — SSR, loaders, ISR, the revalidation legs — and it has no plugin lifecycle, so it had none.
|
|
71
|
+
|
|
72
|
+
That was not a cosmetic difference in boot output. A web pod legitimately logs less than an api pod, because it has no store, no rpc, no scheduler, no workflows and no plugins. What it also had was **no error reporting**: an SSR shell throw is caught, logged and answered with a 500, so no browser ever renders it and the client-side ErrorBoundary bridge cannot see it either. Both ends of the integration worked and the middle was dark — while the docs' "React render error ✅ auto" row, true of the client path, read as covering all of them.
|
|
73
|
+
|
|
74
|
+
Set `SENTRY_DSN` on the web deployment and the boot says `sentry active` with the same message and the same field names as the api half, from the same function — `initSentryServer` in `@voltro/plugin-sentry/server`, which the api plugin now calls too. One init, because a copy of `skipOpenTelemetrySetup`, the traces default, the overrides-first spread and the degrade-on-missing-SDK path would have drifted the moment either half gained a case.
|
|
75
|
+
|
|
76
|
+
Three deliberate asymmetries, each because the two processes are not the same thing:
|
|
77
|
+
|
|
78
|
+
- **`traces: false` on the web side.** The api contributes a span processor to the framework's tracer; the web server has no tracer at all, so tracing on would put `traces: true` in a boot line while nothing produces a span. - **No DSN is SILENT here.** On the api side `sentryPlugin()` is a declaration and an inert one contradicts it. There is no declaration here. - **`@voltro/plugin-sentry` must be installed in the WEB app.** With a DSN set and the package missing, the boot names the command rather than failing — monitoring must never be what stops a deploy.
|
|
79
|
+
|
|
80
|
+
Measured against a real production `voltro start`, both branches. Note that production `voltro start` loads the app's precompiled start bundle, so a web deployment picks this up when that bundle is rebuilt — the framework version alone is not enough.
|
|
81
|
+
|
|
82
|
+
### Fixed
|
|
83
|
+
|
|
84
|
+
- **@voltro/workflow, @voltro/runtime, @voltro/database, @voltro/cli, @voltro/plugin-sentry** — **Six raw writes removed from production log streams — and the guard that was supposed to catch them rewritten, because it was green for two independent reasons.**
|
|
85
|
+
|
|
86
|
+
A pod tail showed `[voltro:workflow] shard-lock coordination: row-based (dialect=mariadb, mode=row)` sitting between JSON records. `@voltro/logger` is what makes a line JSON in a pod and pretty on a TTY; a `process.stderr.write` bypasses that decision at exactly the place nobody looks, because a dev terminal renders both the same.
|
|
87
|
+
|
|
88
|
+
Fixed at the source: the workflow cluster layer (2), `rpcServer`'s computed-cache warning, the subscription outbox's and the RYW store's `warn`/`onError` defaults — those two are not fallbacks, the callers pass nothing, so the default IS the production path — and the migration file discovery's skip notice, which lands in the migrate job's stream.
|
|
89
|
+
|
|
90
|
+
**The guard is the part worth reading.** `prodLogDiscipline.test.ts` existed for this exact class and reported clean, for two reasons that had to be fixed separately:
|
|
91
|
+
|
|
92
|
+
- Its file set was a hand-written list of five. A guard that opts files IN says nothing about any file added after it was written. - Its matcher was LINE-LOCAL, so `process.stderr.write(` on one line and the `` `[tag] `` on the next never matched — 4 of the 9 call sites in the repo are written that way, including one in a file that WAS on the list. The guard had been pointed straight at an offender and called it clean.
|
|
93
|
+
|
|
94
|
+
It is opt-OUT now: every server-side package's source is scanned, exceptions carry a reason, and a second test fails if an exception's call site disappears — an allowlist entry for code that is gone reads as a rule with a hole in it.
|
|
95
|
+
|
|
96
|
+
Verified against a real `voltro serve` under `NODE_ENV=production`: 35 records, zero framework lines that are not JSON.
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
42
100
|
## [0.59.0] — 2026-08-30
|
|
43
101
|
|
|
44
102
|
### ⚠ BREAKING
|
package/dist/index.d.ts
CHANGED
|
@@ -35,7 +35,7 @@ declare interface CaptureContext {
|
|
|
35
35
|
readonly spanId?: string;
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
export declare const captureToSentry: (sentry: SentryLike, cause: Cause.Cause<unknown>, ctx: CaptureContext, breadcrumbs: ReadonlyArray<Breadcrumb
|
|
38
|
+
export declare const captureToSentry: (sentry: SentryLike, cause: Cause.Cause<unknown>, ctx: CaptureContext, breadcrumbs: ReadonlyArray<Breadcrumb>, captureFailures?: boolean | ((error: unknown) => boolean)) => void;
|
|
39
39
|
|
|
40
40
|
/** Unwrap an Effect `Cause` to the underlying error value (Error | tagged
|
|
41
41
|
* error | defect). `Cause.squash` returns the most informative throwable. */
|
|
@@ -105,6 +105,32 @@ export declare interface SentryPluginOptions {
|
|
|
105
105
|
* whole of the plugin for an app with no trace budget.
|
|
106
106
|
*/
|
|
107
107
|
readonly traces?: boolean;
|
|
108
|
+
/**
|
|
109
|
+
* Whether a DECLARED failure reaches Sentry. **Default `false`.**
|
|
110
|
+
*
|
|
111
|
+
* A procedure's `error:` union is a contract: somebody wrote the outcome
|
|
112
|
+
* down, the client gets it typed and branches on it, and it describes a state
|
|
113
|
+
* of the world. `AccessDeniedError` on a team you are not in is the system
|
|
114
|
+
* working. A DEFECT is the opposite — nobody foresaw it — and that is what
|
|
115
|
+
* Sentry is built for. Defects are always reported; this option is only about
|
|
116
|
+
* the other half.
|
|
117
|
+
*
|
|
118
|
+
* The default was `true` by omission rather than by decision, and it filled a
|
|
119
|
+
* deployment's issue list with `level: error` entries for every invalid form
|
|
120
|
+
* value, every click on a resource without access and every call on a deleted
|
|
121
|
+
* row. An issue list of expected things is one nobody reads.
|
|
122
|
+
*
|
|
123
|
+
* Set `true` to keep the old behaviour — worth it if Sentry is where you see
|
|
124
|
+
* errors at all. Or pass a predicate when only some of them are signal:
|
|
125
|
+
*
|
|
126
|
+
* ```ts
|
|
127
|
+
* sentryPlugin({ captureFailures: (e) => (e as { _tag?: string })._tag === 'PaymentDeclined' })
|
|
128
|
+
* ```
|
|
129
|
+
*
|
|
130
|
+
* A cause carrying a defect is reported whatever it travelled with, so this
|
|
131
|
+
* cannot hide one by accident.
|
|
132
|
+
*/
|
|
133
|
+
readonly captureFailures?: boolean | ((error: unknown) => boolean);
|
|
108
134
|
/** Continuous CPU profiling via @sentry/profiling-node. Default false. */
|
|
109
135
|
readonly profiling?: boolean;
|
|
110
136
|
/** Profile sample rate (0..1) when `profiling`. Default 1.0. */
|
package/dist/index.js
CHANGED
|
@@ -1,35 +1,23 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
try {
|
|
9
|
-
let t = new URL(e), n = t.username, r = t.pathname.split("/").filter(Boolean), i = r[r.length - 1];
|
|
10
|
-
return !n || !i ? null : {
|
|
11
|
-
protocol: t.protocol.replace(/:$/, ""),
|
|
12
|
-
publicKey: n,
|
|
13
|
-
host: t.host,
|
|
14
|
-
projectId: i
|
|
15
|
-
};
|
|
16
|
-
} catch {
|
|
17
|
-
return null;
|
|
18
|
-
}
|
|
19
|
-
}, s = {
|
|
1
|
+
import { n as e, r as t, t as n } from "./server-DQn_uTTk.js";
|
|
2
|
+
import { Cause as r, Effect as i } from "effect";
|
|
3
|
+
import { pluginEnv as a } from "@voltro/env";
|
|
4
|
+
import { addSink as o } from "@voltro/logger";
|
|
5
|
+
import { definePlugin as s, subscribeServerErrors as c } from "@voltro/protocol";
|
|
6
|
+
//#region src/breadcrumbs.ts
|
|
7
|
+
var l = {
|
|
20
8
|
trace: "debug",
|
|
21
9
|
debug: "debug",
|
|
22
10
|
info: "info",
|
|
23
11
|
warn: "warning",
|
|
24
12
|
error: "error",
|
|
25
13
|
fatal: "fatal"
|
|
26
|
-
},
|
|
14
|
+
}, u = (e) => ({
|
|
27
15
|
timestamp: e.ts.getTime() / 1e3,
|
|
28
|
-
level:
|
|
16
|
+
level: l[e.level] ?? "info",
|
|
29
17
|
...e.scope ? { category: e.scope } : {},
|
|
30
18
|
message: e.message,
|
|
31
19
|
...Object.keys(e.fields).length > 0 ? { data: { ...e.fields } } : {}
|
|
32
|
-
}),
|
|
20
|
+
}), d = (e = {}, t = () => Date.now()) => {
|
|
33
21
|
let n = e.maxPerTrace ?? 50, r = e.maxTraces ?? 1e3, i = e.ttlMs ?? 3e5, a = /* @__PURE__ */ new Map(), o = () => {
|
|
34
22
|
let e = t() - i;
|
|
35
23
|
for (let [t, n] of a) if (n.at < e) a.delete(t);
|
|
@@ -38,11 +26,11 @@ var o = (e) => {
|
|
|
38
26
|
return {
|
|
39
27
|
add(e, i) {
|
|
40
28
|
o();
|
|
41
|
-
let s = a.get(e),
|
|
29
|
+
let s = a.get(e), c = s ?? {
|
|
42
30
|
at: t(),
|
|
43
31
|
crumbs: []
|
|
44
32
|
};
|
|
45
|
-
for (s && a.delete(e),
|
|
33
|
+
for (s && a.delete(e), c.at = t(), c.crumbs.push(u(i)), c.crumbs.length > n && c.crumbs.shift(), a.set(e, c); a.size > r;) {
|
|
46
34
|
let e = a.keys().next().value;
|
|
47
35
|
if (e === void 0) break;
|
|
48
36
|
a.delete(e);
|
|
@@ -59,24 +47,35 @@ var o = (e) => {
|
|
|
59
47
|
return a.size;
|
|
60
48
|
}
|
|
61
49
|
};
|
|
62
|
-
},
|
|
50
|
+
}, f = (e) => r.squash(e), p = (e, t, n = []) => {
|
|
63
51
|
e.withScope((r) => {
|
|
64
52
|
r.setTag("voltro.errorSource", t.source), t.name !== void 0 && r.setTag("voltro.name", t.name), t.traceId !== void 0 && r.setContext("trace", { trace_id: t.traceId }), t.fields !== void 0 && r.setContext("voltro", t.fields);
|
|
65
53
|
for (let e of n) r.addBreadcrumb(e);
|
|
66
54
|
e.captureException(t.error);
|
|
67
55
|
});
|
|
68
|
-
},
|
|
69
|
-
if (
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
|
|
56
|
+
}, m = (e, t) => {
|
|
57
|
+
if (r.isInterruptedOnly(e)) return !1;
|
|
58
|
+
if (t === !0) return !0;
|
|
59
|
+
let n = [...r.failures(e)];
|
|
60
|
+
return [...r.defects(e)].length > 0 ? !0 : n.length === 0 || t === !1 ? !1 : n.some((e) => {
|
|
61
|
+
try {
|
|
62
|
+
return t(e);
|
|
63
|
+
} catch {
|
|
64
|
+
return !0;
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
}, h = (e, t, n, r, i = !1) => {
|
|
68
|
+
if (!m(t, i)) return;
|
|
69
|
+
let a = f(t);
|
|
70
|
+
e.withScope((t) => {
|
|
71
|
+
t.setContext("trace", {
|
|
72
|
+
trace_id: n.traceId,
|
|
73
|
+
...n.spanId === void 0 ? {} : { span_id: n.spanId }
|
|
74
|
+
}), t.setTag("rpc.tag", n.tag), t.setTag("rpc.kind", n.kind), t.setTag("subject.type", n.subject.type), n.subject.tenantId !== void 0 && n.subject.tenantId !== null && t.setTag("tenant.id", n.subject.tenantId);
|
|
75
|
+
for (let e of r) t.addBreadcrumb(e);
|
|
76
|
+
e.captureException(a);
|
|
78
77
|
});
|
|
79
|
-
},
|
|
78
|
+
}, g = a([
|
|
80
79
|
{
|
|
81
80
|
name: "SENTRY_DSN",
|
|
82
81
|
required: !1,
|
|
@@ -95,76 +94,61 @@ var o = (e) => {
|
|
|
95
94
|
secret: !1,
|
|
96
95
|
description: "Release identifier for Sentry release health + source maps."
|
|
97
96
|
}
|
|
98
|
-
]),
|
|
99
|
-
let
|
|
100
|
-
if (!
|
|
101
|
-
name:
|
|
97
|
+
]), _ = (r = {}) => {
|
|
98
|
+
let a = g.read("SENTRY_DSN", r.dsn), l = t(a), u = r.name ? `@voltro/plugin-sentry#${r.name}` : "@voltro/plugin-sentry";
|
|
99
|
+
if (!l) return s({
|
|
100
|
+
name: u,
|
|
102
101
|
description: "Sentry error tracking (inactive — no valid SENTRY_DSN).",
|
|
103
|
-
onActivate: (e) =>
|
|
104
|
-
onDeactivate: () =>
|
|
102
|
+
onActivate: (e) => i.sync(() => e.logger.warn("sentry inactive — no valid DSN (set `dsn` or SENTRY_DSN)")),
|
|
103
|
+
onDeactivate: () => i.void
|
|
105
104
|
});
|
|
106
|
-
let
|
|
107
|
-
if (
|
|
108
|
-
|
|
105
|
+
let f = g.read("SENTRY_ENVIRONMENT", r.environment), m = g.read("SENTRY_RELEASE", r.release), _ = r.tracesSampleRate ?? 1, v = r.traces ?? !0, y = r.captureFailures ?? !1, b = d({ maxPerTrace: r.maxBreadcrumbs ?? 50 }), x, S = (e, t) => {
|
|
106
|
+
if (x) try {
|
|
107
|
+
h(x, e, t, b.take(t.traceId), y);
|
|
109
108
|
} catch {}
|
|
110
|
-
},
|
|
111
|
-
tag:
|
|
112
|
-
kind:
|
|
113
|
-
subject:
|
|
114
|
-
traceId:
|
|
115
|
-
...
|
|
116
|
-
})))),
|
|
109
|
+
}, C = (e, t) => e.pipe(i.tapErrorCause((e) => i.sync(() => S(e, {
|
|
110
|
+
tag: t.tag,
|
|
111
|
+
kind: t.kind,
|
|
112
|
+
subject: t.subject,
|
|
113
|
+
traceId: t.traceId,
|
|
114
|
+
...t.spanId === void 0 ? {} : { spanId: t.spanId }
|
|
115
|
+
})))), w = [
|
|
117
116
|
"rpc:intercept:mutation",
|
|
118
117
|
"rpc:intercept:query",
|
|
119
118
|
"rpc:intercept:action",
|
|
120
|
-
`network:outbound:${
|
|
121
|
-
],
|
|
122
|
-
return
|
|
123
|
-
name:
|
|
119
|
+
`network:outbound:${l.host}`
|
|
120
|
+
], T, E;
|
|
121
|
+
return s({
|
|
122
|
+
name: u,
|
|
124
123
|
description: "Deep Sentry integration — trace-correlated errors + breadcrumbs + (opt-in) performance traces.",
|
|
125
|
-
permissions:
|
|
126
|
-
declaredEnv:
|
|
127
|
-
interceptMutation:
|
|
128
|
-
interceptQuery:
|
|
129
|
-
interceptAction:
|
|
124
|
+
permissions: w,
|
|
125
|
+
declaredEnv: g.declared,
|
|
126
|
+
interceptMutation: C,
|
|
127
|
+
interceptQuery: C,
|
|
128
|
+
interceptAction: C,
|
|
130
129
|
contributeObservability: async (t) => {
|
|
131
|
-
let
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
} catch {
|
|
135
|
-
t.logger.warn("sentry requested but @sentry/node is not installed — inactive");
|
|
136
|
-
return;
|
|
137
|
-
}
|
|
138
|
-
let i = [];
|
|
139
|
-
if (e.profiling) try {
|
|
140
|
-
let { nodeProfilingIntegration: e } = await import("@sentry/profiling-node");
|
|
141
|
-
i.push(e());
|
|
142
|
-
} catch {
|
|
143
|
-
t.logger.warn("sentry profiling requested but @sentry/profiling-node is not installed");
|
|
144
|
-
}
|
|
145
|
-
let { integrations: a, ...o } = e.init ?? {};
|
|
146
|
-
if (a && i.push(...a), r.init({
|
|
147
|
-
...o,
|
|
148
|
-
dsn: n,
|
|
149
|
-
skipOpenTelemetrySetup: !0,
|
|
150
|
-
tracesSampleRate: h,
|
|
151
|
-
...u ? { environment: u } : {},
|
|
152
|
-
...m ? { release: m } : {},
|
|
153
|
-
...e.profiling ? { profilesSampleRate: e.profilesSampleRate ?? 1 } : {},
|
|
154
|
-
...i.length > 0 ? { integrations: i } : {}
|
|
155
|
-
}), v = r, t.logger.info("sentry active", {
|
|
156
|
-
environment: u,
|
|
130
|
+
let i = {
|
|
131
|
+
dsn: a,
|
|
132
|
+
environment: f,
|
|
157
133
|
release: m,
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
134
|
+
tracesSampleRate: _,
|
|
135
|
+
traces: v,
|
|
136
|
+
profiling: r.profiling,
|
|
137
|
+
profilesSampleRate: r.profilesSampleRate,
|
|
138
|
+
init: r.init
|
|
139
|
+
}, o = await n(i, t.logger);
|
|
140
|
+
if (o === void 0) return;
|
|
141
|
+
let s = o.sdk;
|
|
142
|
+
if (o.client, x = s, t.logger.info("sentry active", {
|
|
143
|
+
...e(o, i),
|
|
144
|
+
captureFailures: typeof y == "function" ? "predicate" : y
|
|
145
|
+
}), v) try {
|
|
162
146
|
let { SentrySpanProcessor: e } = await import("@sentry/opentelemetry");
|
|
163
147
|
return {
|
|
164
148
|
spanProcessors: [new e()],
|
|
165
149
|
resourceAttributes: {
|
|
166
150
|
...m ? { "service.version": m } : {},
|
|
167
|
-
...
|
|
151
|
+
...f ? { "deployment.environment": f } : {}
|
|
168
152
|
}
|
|
169
153
|
};
|
|
170
154
|
} catch {
|
|
@@ -172,18 +156,18 @@ var o = (e) => {
|
|
|
172
156
|
return;
|
|
173
157
|
}
|
|
174
158
|
},
|
|
175
|
-
onActivate: () =>
|
|
176
|
-
|
|
159
|
+
onActivate: () => i.sync(() => {
|
|
160
|
+
T = o((e) => {
|
|
177
161
|
let t = e.fields.traceId;
|
|
178
|
-
typeof t == "string" && t.length > 0 &&
|
|
179
|
-
}),
|
|
180
|
-
if (
|
|
181
|
-
|
|
162
|
+
typeof t == "string" && t.length > 0 && b.add(t, e);
|
|
163
|
+
}), E = c((e) => {
|
|
164
|
+
if (x) try {
|
|
165
|
+
p(x, e, e.traceId ? b.take(e.traceId) : []);
|
|
182
166
|
} catch {}
|
|
183
167
|
});
|
|
184
168
|
}),
|
|
185
|
-
onDeactivate: () =>
|
|
186
|
-
|
|
169
|
+
onDeactivate: () => i.promise(async () => {
|
|
170
|
+
T?.(), T = void 0, E?.(), E = void 0;
|
|
187
171
|
try {
|
|
188
172
|
await (await import("@sentry/node")).close(2e3);
|
|
189
173
|
} catch {}
|
|
@@ -191,4 +175,4 @@ var o = (e) => {
|
|
|
191
175
|
});
|
|
192
176
|
};
|
|
193
177
|
//#endregion
|
|
194
|
-
export {
|
|
178
|
+
export { h as captureToSentry, f as causeToError, d as makeBreadcrumbRing, t as parseDsn, _ as sentryPlugin, u as toBreadcrumb };
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
//#region src/dsn.ts
|
|
2
|
+
var e = (e) => {
|
|
3
|
+
if (!e) return null;
|
|
4
|
+
try {
|
|
5
|
+
let t = new URL(e), n = t.username, r = t.pathname.split("/").filter(Boolean), i = r[r.length - 1];
|
|
6
|
+
return !n || !i ? null : {
|
|
7
|
+
protocol: t.protocol.replace(/:$/, ""),
|
|
8
|
+
publicKey: n,
|
|
9
|
+
host: t.host,
|
|
10
|
+
projectId: i
|
|
11
|
+
};
|
|
12
|
+
} catch {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
}, t = async (t, n) => {
|
|
16
|
+
if (!e(t.dsn)) return;
|
|
17
|
+
let r;
|
|
18
|
+
try {
|
|
19
|
+
r = await import("@sentry/node");
|
|
20
|
+
} catch {
|
|
21
|
+
n.warn("sentry requested but @sentry/node is not installed — inactive");
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
let i = [];
|
|
25
|
+
if (t.profiling) try {
|
|
26
|
+
let { nodeProfilingIntegration: e } = await import("@sentry/profiling-node");
|
|
27
|
+
i.push(e());
|
|
28
|
+
} catch {
|
|
29
|
+
n.warn("sentry profiling requested but @sentry/profiling-node is not installed");
|
|
30
|
+
}
|
|
31
|
+
let { integrations: a, ...o } = t.init ?? {};
|
|
32
|
+
Array.isArray(a) && i.push(...a);
|
|
33
|
+
let s = t.tracesSampleRate ?? 1, c = t.traces ?? !0, l = r.init({
|
|
34
|
+
...o,
|
|
35
|
+
dsn: t.dsn,
|
|
36
|
+
skipOpenTelemetrySetup: !0,
|
|
37
|
+
tracesSampleRate: s,
|
|
38
|
+
...t.environment ? { environment: t.environment } : {},
|
|
39
|
+
...t.release ? { release: t.release } : {},
|
|
40
|
+
...t.profiling ? { profilesSampleRate: t.profilesSampleRate ?? 1 } : {},
|
|
41
|
+
...i.length > 0 ? { integrations: i } : {}
|
|
42
|
+
});
|
|
43
|
+
return {
|
|
44
|
+
sdk: r,
|
|
45
|
+
client: l,
|
|
46
|
+
tracesEnabled: c,
|
|
47
|
+
tracesSampleRate: s
|
|
48
|
+
};
|
|
49
|
+
}, n = (e, t) => ({
|
|
50
|
+
environment: t.environment,
|
|
51
|
+
release: t.release,
|
|
52
|
+
traces: e.tracesEnabled,
|
|
53
|
+
profiling: !!t.profiling,
|
|
54
|
+
...e.tracesEnabled ? { tracesSampleRate: e.tracesSampleRate } : {}
|
|
55
|
+
});
|
|
56
|
+
//#endregion
|
|
57
|
+
export { n, e as r, t };
|
package/dist/server.d.ts
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Initialise `@sentry/node`, or explain why not.
|
|
3
|
+
*
|
|
4
|
+
* Returns `undefined` for the two non-error outcomes — no DSN, or the SDK is
|
|
5
|
+
* not installed — because neither is a reason to fail a boot. The api half made
|
|
6
|
+
* that call first (an optional dependency CAN fail to install, and a monitoring
|
|
7
|
+
* integration must never be the thing that stops a deploy), and the web half
|
|
8
|
+
* inherits it rather than deciding again.
|
|
9
|
+
*/
|
|
10
|
+
export declare const initSentryServer: (options: SentryServerInitOptions, log: SentryServerLog) => Promise<SentryServer | undefined>;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* The fields the `sentry active` line carries.
|
|
14
|
+
*
|
|
15
|
+
* Shared so the two processes cannot describe the same state differently — the
|
|
16
|
+
* whole point of the change is that a reader tailing an api pod and a web pod
|
|
17
|
+
* sees one vocabulary.
|
|
18
|
+
*/
|
|
19
|
+
export declare const sentryActiveFields: (server: SentryServer, options: SentryServerInitOptions) => Record<string, unknown>;
|
|
20
|
+
|
|
21
|
+
export declare interface SentryServer {
|
|
22
|
+
/** The initialised `@sentry/node` namespace. */
|
|
23
|
+
readonly sdk: {
|
|
24
|
+
readonly captureException: (error: unknown, hint?: unknown) => string;
|
|
25
|
+
readonly flush?: (timeout?: number) => Promise<boolean>;
|
|
26
|
+
};
|
|
27
|
+
/** Whatever `Sentry.init` returned — the client, on SDK versions that return
|
|
28
|
+
* one. Passed through rather than re-fetched via `getClient()`, because the
|
|
29
|
+
* two are not the same call on every version and the caller asked for the
|
|
30
|
+
* init's answer. */
|
|
31
|
+
readonly client: unknown;
|
|
32
|
+
readonly tracesEnabled: boolean;
|
|
33
|
+
readonly tracesSampleRate: number;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** What the caller must hand in — resolved from env by the caller, because the
|
|
37
|
+
* two processes read their environment through different gates. */
|
|
38
|
+
export declare interface SentryServerInitOptions {
|
|
39
|
+
readonly dsn: string | undefined;
|
|
40
|
+
readonly environment?: string | undefined;
|
|
41
|
+
readonly release?: string | undefined;
|
|
42
|
+
/** Default 1.0, matching the browser half. */
|
|
43
|
+
readonly tracesSampleRate?: number | undefined;
|
|
44
|
+
/** Default ON — see the plugin's `traces` option for why `false` made the
|
|
45
|
+
* browser half's spans point at nothing. */
|
|
46
|
+
readonly traces?: boolean | undefined;
|
|
47
|
+
readonly profiling?: boolean | undefined;
|
|
48
|
+
readonly profilesSampleRate?: number | undefined;
|
|
49
|
+
/** User escape hatch, spread FIRST so plugin-managed keys stay authoritative. */
|
|
50
|
+
readonly init?: Record<string, unknown> | undefined;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export declare interface SentryServerLog {
|
|
54
|
+
readonly info: (message: string, fields?: Record<string, unknown>) => void;
|
|
55
|
+
readonly warn: (message: string, fields?: Record<string, unknown>) => void;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export { }
|
package/dist/server.js
ADDED
package/dist/web.d.ts
CHANGED
|
@@ -20,6 +20,24 @@ export declare const closeSentryBrowser: () => void;
|
|
|
20
20
|
* server (SSR) and without a DSN. */
|
|
21
21
|
export declare const initSentryBrowser: (options?: SentryBrowserOptions) => Promise<void>;
|
|
22
22
|
|
|
23
|
+
/**
|
|
24
|
+
* Is this client-side event a DECLARED failure?
|
|
25
|
+
*
|
|
26
|
+
* Two conditions, and both are needed. The source must be an `rpc.*` one — a
|
|
27
|
+
* route render throw or a manual `reportClientError` is nobody's declared
|
|
28
|
+
* outcome. And the value must carry a `_tag`: the framework's typed errors are
|
|
29
|
+
* `Schema.TaggedError` subclasses, so a decoded one has one, while a transport
|
|
30
|
+
* failure, an aborted socket or a forwarded defect does not.
|
|
31
|
+
*
|
|
32
|
+
* The rule is deliberately narrow. Anything it cannot positively identify as
|
|
33
|
+
* declared is reported, because the cost of the two mistakes is not symmetric:
|
|
34
|
+
* a noisy issue is read and dismissed, a missing one is never read.
|
|
35
|
+
*/
|
|
36
|
+
export declare const isDeclaredRpcFailure: (event: {
|
|
37
|
+
source: string;
|
|
38
|
+
error: unknown;
|
|
39
|
+
}) => boolean;
|
|
40
|
+
|
|
23
41
|
/** Emit a browser-side Sentry span into the SAME trace as the server
|
|
24
42
|
* transaction the call will produce. */
|
|
25
43
|
export declare const recordClientTraceSpan: (sentry: SentryBrowserLike, event: ClientTraceEvent) => void;
|
|
@@ -53,6 +71,19 @@ export declare interface SentryBrowserOptions {
|
|
|
53
71
|
/** Instrument page loads + client navigations via `browserTracingIntegration`.
|
|
54
72
|
* Default true. */
|
|
55
73
|
readonly browserTracing?: boolean;
|
|
74
|
+
/**
|
|
75
|
+
* Whether a DECLARED failure that came back from an rpc call reaches Sentry.
|
|
76
|
+
* **Default `false`**, matching `sentryPlugin({ captureFailures })`.
|
|
77
|
+
*
|
|
78
|
+
* Both halves need it or neither works. The api stops reporting
|
|
79
|
+
* `AccessDeniedError` and the browser keeps filing the same rejection,
|
|
80
|
+
* because a rejected call is an rpc error on this side too — the option would
|
|
81
|
+
* be half-wired, and the issue list would look unchanged.
|
|
82
|
+
*
|
|
83
|
+
* Route render failures and explicit `reportClientError` calls are NEVER
|
|
84
|
+
* filtered by this: nobody declared them, which is the whole distinction.
|
|
85
|
+
*/
|
|
86
|
+
readonly captureFailures?: boolean | ((error: unknown) => boolean);
|
|
56
87
|
}
|
|
57
88
|
|
|
58
89
|
declare interface SentryBrowserScopeLike {
|
package/dist/web.js
CHANGED
|
@@ -16,33 +16,43 @@ var i = (e, t) => {
|
|
|
16
16
|
}
|
|
17
17
|
}).end();
|
|
18
18
|
});
|
|
19
|
-
}, s = (e) => {
|
|
19
|
+
}, s = (e) => e.source.startsWith("rpc.") && typeof e.error?._tag == "string", c = (e) => {
|
|
20
20
|
let t = r()[e];
|
|
21
21
|
return typeof t == "string" && t !== "" ? t : void 0;
|
|
22
|
-
},
|
|
22
|
+
}, l, u, d = async (e = {}) => {
|
|
23
23
|
if (typeof window > "u") return;
|
|
24
|
-
let r = e.dsn ??
|
|
24
|
+
let r = e.dsn ?? c("VOLTRO_PUBLIC_SENTRY_DSN");
|
|
25
25
|
if (!r) return;
|
|
26
|
-
let a = e.environment ??
|
|
27
|
-
|
|
26
|
+
let a = e.environment ?? c("VOLTRO_PUBLIC_SENTRY_ENVIRONMENT"), d = e.release ?? c("VOLTRO_PUBLIC_SENTRY_RELEASE"), p = await import("@sentry/react");
|
|
27
|
+
p.init({
|
|
28
28
|
dsn: r,
|
|
29
29
|
tracesSampleRate: e.tracesSampleRate ?? 1,
|
|
30
30
|
...a ? { environment: a } : {},
|
|
31
|
-
...
|
|
32
|
-
integrations: e.browserTracing === !1 ? [] : [
|
|
31
|
+
...d ? { release: d } : {},
|
|
32
|
+
integrations: e.browserTracing === !1 ? [] : [p.browserTracingIntegration()]
|
|
33
33
|
});
|
|
34
|
-
let
|
|
35
|
-
|
|
34
|
+
let m = p;
|
|
35
|
+
l?.(), l = n((e) => {
|
|
36
36
|
try {
|
|
37
|
-
o(
|
|
37
|
+
o(m, e);
|
|
38
38
|
} catch {}
|
|
39
|
-
}),
|
|
39
|
+
}), u?.();
|
|
40
|
+
let h = e.captureFailures ?? !1;
|
|
41
|
+
u = t((e) => {
|
|
40
42
|
try {
|
|
41
|
-
|
|
43
|
+
if (s(e) && !f(e.error, h)) return;
|
|
44
|
+
i(m, e);
|
|
42
45
|
} catch {}
|
|
43
46
|
});
|
|
44
|
-
},
|
|
45
|
-
|
|
47
|
+
}, f = (e, t) => {
|
|
48
|
+
if (typeof t != "function") return t;
|
|
49
|
+
try {
|
|
50
|
+
return t(e);
|
|
51
|
+
} catch {
|
|
52
|
+
return !0;
|
|
53
|
+
}
|
|
54
|
+
}, p = () => {
|
|
55
|
+
l?.(), l = void 0, u?.(), u = void 0;
|
|
46
56
|
};
|
|
47
57
|
//#endregion
|
|
48
|
-
export { i as captureClientError,
|
|
58
|
+
export { i as captureClientError, p as closeSentryBrowser, d as initSentryBrowser, s as isDeclaredRpcFailure, o as recordClientTraceSpan, e as reportClientError, a as sentryTraceHeader };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@voltro/plugin-sentry",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.60.0",
|
|
4
4
|
"description": "Deep Sentry integration — error tracking correlated to the distributed trace, breadcrumbs from the framework log sink, and (opt-in) performance traces routed from the framework's OTel tracer via @sentry/node in OTel-consumer mode. Errors carry the active trace_id + span_id + the request's recent log lines.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"voltro",
|
|
@@ -23,6 +23,11 @@
|
|
|
23
23
|
"import": "./dist/index.js",
|
|
24
24
|
"default": "./dist/index.js"
|
|
25
25
|
},
|
|
26
|
+
"./server": {
|
|
27
|
+
"types": "./dist/server.d.ts",
|
|
28
|
+
"import": "./dist/server.js",
|
|
29
|
+
"default": "./dist/server.js"
|
|
30
|
+
},
|
|
26
31
|
"./web": {
|
|
27
32
|
"types": "./dist/web.d.ts",
|
|
28
33
|
"import": "./dist/web.js",
|
|
@@ -38,10 +43,10 @@
|
|
|
38
43
|
"node": ">=24.0.0"
|
|
39
44
|
},
|
|
40
45
|
"dependencies": {
|
|
41
|
-
"@voltro/client": "0.
|
|
42
|
-
"@voltro/env": "0.
|
|
43
|
-
"@voltro/logger": "0.
|
|
44
|
-
"@voltro/protocol": "0.
|
|
46
|
+
"@voltro/client": "0.60.0",
|
|
47
|
+
"@voltro/env": "0.60.0",
|
|
48
|
+
"@voltro/logger": "0.60.0",
|
|
49
|
+
"@voltro/protocol": "0.60.0"
|
|
45
50
|
},
|
|
46
51
|
"optionalDependencies": {
|
|
47
52
|
"@sentry/node": "^10.70.0",
|