@indigoai-us/hq-cli 5.103.26 → 5.103.28
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 +4 -0
- package/dist/commands/dm.js +3 -7
- package/dist/commands/integrations-core.d.ts +15 -3
- package/dist/commands/integrations-core.js +25 -3
- package/dist/commands/integrations-manage.js +4 -1
- package/dist/commands/integrations.js +2 -1
- package/dist/lib/doctor/checks/integrations.d.ts +8 -23
- package/dist/lib/doctor/checks/integrations.js +47 -150
- package/dist/lib/integrations/health.d.ts +78 -0
- package/dist/lib/integrations/health.js +255 -0
- package/dist/lib/integrations/provider-slug.d.ts +10 -0
- package/dist/lib/integrations/provider-slug.js +12 -0
- package/dist/lib/search-index/index.d.ts +44 -0
- package/dist/lib/search-index/index.js +111 -18
- package/dist/main.js +46 -2
- package/dist/utils/qmd-llm-disabled-error.d.ts +10 -0
- package/dist/utils/qmd-llm-disabled-error.js +87 -0
- package/dist/utils/qmd-module-missing-error.d.ts +41 -0
- package/dist/utils/qmd-module-missing-error.js +130 -0
- package/dist/utils/sentry-fingerprint.js +96 -3
- package/package.json +1 -1
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// src/utils/qmd-llm-disabled-error.ts
|
|
2
|
+
//
|
|
3
|
+
// Classify a qmd failure caused by qmd's local LLM being DISABLED IN CI — not an
|
|
4
|
+
// hq-cli code defect. qmd hard-disables every LLM operation whenever `CI` is
|
|
5
|
+
// truthy in the environment (`Error: LLM operations are disabled in CI (set
|
|
6
|
+
// CI=true)`); its semantic/hybrid reads (`vsearch`/`query`) expand the query
|
|
7
|
+
// through that gate and the index build (`embed`) generates embeddings through
|
|
8
|
+
// it, so each dies immediately when CI is set. hq-cli forwards the caller's
|
|
9
|
+
// environment — including CI — to the qmd child, so this is the caller's
|
|
10
|
+
// ENVIRONMENT, not a bug HQ can fix in code: surface an actionable remedy and
|
|
11
|
+
// SKIP Sentry capture. Sibling of qmd-terminated-error.ts (HQ-CLI 7677702704),
|
|
12
|
+
// qmd-collection-missing-error.ts (HQ-CLI-S), qmd-native-binding-error.ts
|
|
13
|
+
// (HQ-CLI-J), environmental-error.ts (HQ-CLI-2) and network-transport-error.ts
|
|
14
|
+
// (HQ-CLI-G): a failure that is NOT an hq-cli defect is printed with an
|
|
15
|
+
// actionable message and never filed as a crash.
|
|
16
|
+
//
|
|
17
|
+
// HQ-CLI 7688850003: `hq search <query> --mode semantic ...` spawned `qmd
|
|
18
|
+
// vsearch`, which refused because CI was set on the host (an automation
|
|
19
|
+
// harness). hq-cli had no classifier for that wording, so finishRunQmd raised a
|
|
20
|
+
// plain QmdExitError whose synthesized message interpolates the caller's whole
|
|
21
|
+
// argv; nothing caught it, so it reached the top-level handler's final else and
|
|
22
|
+
// was captured — and because the message embeds the query, every distinct query
|
|
23
|
+
// minted a brand-new permanent Sentry issue. finishRunQmd now types the
|
|
24
|
+
// condition QmdLlmDisabledError (raised only from qmd's OWN captured streams,
|
|
25
|
+
// message naming the subcommand only); this classifier closes the boundary.
|
|
26
|
+
//
|
|
27
|
+
// The gate is deliberately narrow on TWO axes so it can neither be tripped by
|
|
28
|
+
// user input nor silence a real bug:
|
|
29
|
+
// 1. CLASS: only a QmdLlmDisabledError (the typed subclass finishRunQmd raises
|
|
30
|
+
// for the LLM-disabled wording) qualifies — never a plain QmdExitError, a
|
|
31
|
+
// native-binding failure, or any other error, even one carrying the same
|
|
32
|
+
// wording in a user-controlled field.
|
|
33
|
+
// 2. INVOCATION: only the qmd subcommands that legitimately need the LLM — the
|
|
34
|
+
// caller-supplied semantic/hybrid reads `vsearch`/`query`, plus the index
|
|
35
|
+
// build `embed`. Any other subcommand (e.g. an LLM-disabled failure surfaced
|
|
36
|
+
// by hq's OWN `collection list` reconciliation) returns null and stays on
|
|
37
|
+
// the captured-error path, so a condition hq did not expect is still
|
|
38
|
+
// reported — now grouped per subcommand rather than per query.
|
|
39
|
+
//
|
|
40
|
+
// The remedy is entirely query-free: only the SUBCOMMAND — drawn from a closed
|
|
41
|
+
// allow-list — selects the wording, and NOTHING is interpolated from user input,
|
|
42
|
+
// so there is no injection surface at all. This preserves the bounded-
|
|
43
|
+
// fingerprint doctrine established by HQ-CLI-S and HQ-CLI 7677702704.
|
|
44
|
+
/** Semantic/hybrid reads whose query is EXPANDED through qmd's LLM gate. */
|
|
45
|
+
const LLM_SEARCH_READS = new Set(["vsearch", "query"]);
|
|
46
|
+
/** The index build that generates embeddings through the same LLM gate. */
|
|
47
|
+
const LLM_INDEX_BUILD = "embed";
|
|
48
|
+
/**
|
|
49
|
+
* Remedy for a semantic/hybrid SEARCH read. Leads with `--mode keyword`, which
|
|
50
|
+
* needs no LLM and is correct in EVERY environment (including a genuine CI
|
|
51
|
+
* pipeline where clearing CI is not an option), and offers clearing CI only as
|
|
52
|
+
* the secondary option for a non-CI host that merely has the variable set.
|
|
53
|
+
*/
|
|
54
|
+
const SEARCH_REMEDY = "Semantic and hybrid search need qmd's local LLM, which is switched off " +
|
|
55
|
+
"because CI is set in this environment. Re-run with '--mode keyword' (it needs " +
|
|
56
|
+
"no LLM and works everywhere), or clear CI for the command and try again.";
|
|
57
|
+
/**
|
|
58
|
+
* Remedy for the `embed` index build, which cannot run at all without the LLM —
|
|
59
|
+
* so `--mode keyword` does not apply and the only fix is to clear CI.
|
|
60
|
+
*/
|
|
61
|
+
const EMBED_REMEDY = "Embeddings can't be built while CI is set in this environment, because qmd " +
|
|
62
|
+
"disables its local LLM there. Clear CI for the command and run it again.";
|
|
63
|
+
/**
|
|
64
|
+
* If `err` is a qmd LLM-disabled-in-CI failure on a subcommand that legitimately
|
|
65
|
+
* needs the LLM, return an actionable, query-free remedy; otherwise return
|
|
66
|
+
* `null`. Mirrors qmdTerminatedMessage / qmdMissingCollectionMessage /
|
|
67
|
+
* qmdNativeBindingErrorMessage so the top-level handler branches the same way: a
|
|
68
|
+
* non-null result means print-and-skip-Sentry, null means "handle as usual
|
|
69
|
+
* (capture to Sentry)".
|
|
70
|
+
*/
|
|
71
|
+
export function qmdLlmDisabledMessage(err) {
|
|
72
|
+
if (err === null || typeof err !== "object")
|
|
73
|
+
return null;
|
|
74
|
+
const record = err;
|
|
75
|
+
if (record.name !== "QmdLlmDisabledError")
|
|
76
|
+
return null;
|
|
77
|
+
const args = Array.isArray(record.args) ? record.args : [];
|
|
78
|
+
const subcommand = args[0];
|
|
79
|
+
if (typeof subcommand !== "string")
|
|
80
|
+
return null;
|
|
81
|
+
if (LLM_SEARCH_READS.has(subcommand))
|
|
82
|
+
return SEARCH_REMEDY;
|
|
83
|
+
if (subcommand === LLM_INDEX_BUILD)
|
|
84
|
+
return EMBED_REMEDY;
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
//# sourceMappingURL=qmd-llm-disabled-error.js.map
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The actionable remedy shown to the operator. Input-free — nothing is
|
|
3
|
+
* interpolated from the error, the argv, or qmd's output — so there is no
|
|
4
|
+
* injection surface, matching the bounded-fingerprint discipline of its
|
|
5
|
+
* siblings. Names the concrete reinstall for both a global npm and a global
|
|
6
|
+
* pnpm install.
|
|
7
|
+
*
|
|
8
|
+
* Covers BOTH qmd origins deliberately, because they cannot be told apart at
|
|
9
|
+
* runtime: HQ_QMD_BIN is honoured verbatim, and it is also the seam a dev build
|
|
10
|
+
* (and this repo's e2e harness) uses to inject the bundled qmd, so a truthy
|
|
11
|
+
* HQ_QMD_BIN is NOT a reliable "external qmd" signal. A missing module is not an
|
|
12
|
+
* hq-cli code defect on either path, so the remedy — not the classification —
|
|
13
|
+
* carries the origin nuance: it names the bundled reinstall first, then the
|
|
14
|
+
* HQ_QMD_BIN case so an operator running their own qmd is pointed at that
|
|
15
|
+
* install rather than only at hq.
|
|
16
|
+
*/
|
|
17
|
+
export declare const QMD_MODULE_MISSING_REMEDY: string;
|
|
18
|
+
/**
|
|
19
|
+
* True when `err` carries, in qmd's OWN captured streams, a module-resolution
|
|
20
|
+
* failure corroborated by a Node module-loader artifact — i.e. qmd's dependency
|
|
21
|
+
* tree is incomplete on this machine. finishRunQmd calls this on the raw streams
|
|
22
|
+
* to type the condition {@link import('../lib/search-index/index.js').QmdModuleMissingError};
|
|
23
|
+
* a true result there means the caller should reinstall.
|
|
24
|
+
*/
|
|
25
|
+
export declare function isQmdModuleMissingError(err: unknown): boolean;
|
|
26
|
+
/**
|
|
27
|
+
* If `err` is a caller-driven qmd module-missing failure (a QmdModuleMissingError
|
|
28
|
+
* from `hq search` or its `vsearch`/`query`/`get` siblings), return the
|
|
29
|
+
* actionable, input-free reinstall remedy; otherwise return `null`. Mirrors
|
|
30
|
+
* qmdNativeBindingErrorMessage / qmdMissingCollectionMessage so the top-level
|
|
31
|
+
* handler branches the same way: a non-null result means print-and-skip-Sentry,
|
|
32
|
+
* null means "handle as usual (capture to Sentry)".
|
|
33
|
+
*
|
|
34
|
+
* Gated on the TYPED CLASS plus a caller-reads invocation allow-list: a
|
|
35
|
+
* module-missing raised by hq's OWN reconciliation (`collection list`, `context
|
|
36
|
+
* add`, …) points at a packaging fault hq itself could be responsible for, so it
|
|
37
|
+
* stays a captured internal error rather than being silenced as the user's
|
|
38
|
+
* install.
|
|
39
|
+
*/
|
|
40
|
+
export declare function qmdModuleMissingMessage(err: unknown): string | null;
|
|
41
|
+
//# sourceMappingURL=qmd-module-missing-error.d.ts.map
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
// src/utils/qmd-module-missing-error.ts
|
|
2
|
+
//
|
|
3
|
+
// Classify a qmd failure caused by a MISSING JAVASCRIPT MODULE in qmd's own
|
|
4
|
+
// bundled dependency tree — Node could not load a file qmd needs — as the
|
|
5
|
+
// caller's INCOMPLETE INSTALL, not an hq-cli code defect. hq-cli ships qmd and
|
|
6
|
+
// its dependencies, so a `Cannot find module …` from the qmd child means a
|
|
7
|
+
// partial or interrupted global install (`npm i -g` / `pnpm add -g`) left some
|
|
8
|
+
// files unwritten. That is the caller's install shape, not a bug HQ can fix in
|
|
9
|
+
// code, so the CLI surfaces an actionable reinstall remedy and SKIPS Sentry
|
|
10
|
+
// capture. Sibling of qmd-native-binding-error.ts (HQ-CLI-J, unbuilt
|
|
11
|
+
// better-sqlite3), qmd-collection-missing-error.ts (HQ-CLI-S),
|
|
12
|
+
// qmd-llm-disabled-error.ts (HQ-CLI 7688850003) and qmd-terminated-error.ts
|
|
13
|
+
// (HQ-CLI 7677702704): a failure that is NOT an hq-cli defect is printed with an
|
|
14
|
+
// actionable message and never filed as a crash.
|
|
15
|
+
//
|
|
16
|
+
// HQ-CLI-Y (Sentry 7690356976): a qmd child died with `Cannot find module
|
|
17
|
+
// './stringifyComment.js'`, its `Require stack:` rooted at
|
|
18
|
+
// /usr/lib/node_modules/@indigoai-us/hq-cli/node_modules/yaml/dist/stringify/… .
|
|
19
|
+
// Two compounding defects filed it as a high-priority crash AND minted a NEW
|
|
20
|
+
// permanent issue per child failure: finishRunQmd's generic message
|
|
21
|
+
// interpolated the caller's whole argv (query, -c value, absolute paths) AND
|
|
22
|
+
// embedded the child's RAW multi-line stderr, whose ` at <fn> (<file>:<line>)`
|
|
23
|
+
// lines the Sentry Node SDK then parsed as REAL frames of the hq-cli process —
|
|
24
|
+
// so grouping rode foreign frames and every distinct child failure split into
|
|
25
|
+
// its own issue. Bounding the message (search-index/index.ts) fixes the
|
|
26
|
+
// cardinality; this classifier closes the boundary so the broken-install
|
|
27
|
+
// condition reaches the operator as an actionable reinstall, not an unfixable
|
|
28
|
+
// crash.
|
|
29
|
+
//
|
|
30
|
+
// The gate is deliberately narrow on TWO axes, read ONLY from qmd's OWN captured
|
|
31
|
+
// streams (never the synthesized message — even now that it names the subcommand
|
|
32
|
+
// only, the message must never be the classification substrate), so a user
|
|
33
|
+
// searching for the phrase "Cannot find module" can never trip it:
|
|
34
|
+
// 1. RESOLUTION TOKEN: the module-resolution failure itself — `Cannot find
|
|
35
|
+
// module`, or the CJS/ESM `MODULE_NOT_FOUND` / `ERR_MODULE_NOT_FOUND` code.
|
|
36
|
+
// 2. CORROBORATOR: a Node module-loader artifact a bare query cannot
|
|
37
|
+
// synthesize — a `Require stack:` header, a `node_modules/` path, or the
|
|
38
|
+
// ESM `imported from` clause. Either axis alone would be too loose.
|
|
39
|
+
// The better-sqlite3 native-binding failure (HQ-CLI-J) carries neither
|
|
40
|
+
// resolution token, so it does not match here and keeps its own narrower remedy
|
|
41
|
+
// — and the top-level handler checks that classifier FIRST regardless of this
|
|
42
|
+
// one, so the more-actionable approve-builds remedy always wins when both could.
|
|
43
|
+
/** The module-resolution failure token. `MODULE_NOT_FOUND` covers the ESM
|
|
44
|
+
* `ERR_MODULE_NOT_FOUND` code as a substring; `Cannot find module` is the CJS
|
|
45
|
+
* loader's message text. */
|
|
46
|
+
const MODULE_RESOLUTION_TOKEN = /cannot find module|MODULE_NOT_FOUND/i;
|
|
47
|
+
/** A Node module-loader artifact a user's free-text query cannot synthesize. */
|
|
48
|
+
const MODULE_LOADER_ARTIFACT = /Require stack:|node_modules[\\/]|imported from/i;
|
|
49
|
+
/** qmd subcommands whose collection/query is chosen by the CALLER (read surface). */
|
|
50
|
+
const CALLER_READS = new Set(["search", "vsearch", "query", "get"]);
|
|
51
|
+
/**
|
|
52
|
+
* The actionable remedy shown to the operator. Input-free — nothing is
|
|
53
|
+
* interpolated from the error, the argv, or qmd's output — so there is no
|
|
54
|
+
* injection surface, matching the bounded-fingerprint discipline of its
|
|
55
|
+
* siblings. Names the concrete reinstall for both a global npm and a global
|
|
56
|
+
* pnpm install.
|
|
57
|
+
*
|
|
58
|
+
* Covers BOTH qmd origins deliberately, because they cannot be told apart at
|
|
59
|
+
* runtime: HQ_QMD_BIN is honoured verbatim, and it is also the seam a dev build
|
|
60
|
+
* (and this repo's e2e harness) uses to inject the bundled qmd, so a truthy
|
|
61
|
+
* HQ_QMD_BIN is NOT a reliable "external qmd" signal. A missing module is not an
|
|
62
|
+
* hq-cli code defect on either path, so the remedy — not the classification —
|
|
63
|
+
* carries the origin nuance: it names the bundled reinstall first, then the
|
|
64
|
+
* HQ_QMD_BIN case so an operator running their own qmd is pointed at that
|
|
65
|
+
* install rather than only at hq.
|
|
66
|
+
*/
|
|
67
|
+
export const QMD_MODULE_MISSING_REMEDY = "hq's local search index can't start: one of its bundled modules is missing, " +
|
|
68
|
+
"which means the hq install tree is incomplete — a partial or interrupted " +
|
|
69
|
+
"install can leave some dependencies unwritten. Reinstall hq and run the " +
|
|
70
|
+
"command again: for a global install run `npm i -g @indigoai-us/hq-cli` (or " +
|
|
71
|
+
"the pnpm equivalent, `pnpm add -g @indigoai-us/hq-cli`). If you point " +
|
|
72
|
+
"HQ_QMD_BIN at your own qmd, that install is missing the module instead — " +
|
|
73
|
+
"reinstall its dependencies, or unset HQ_QMD_BIN to use the bundled copy.";
|
|
74
|
+
/**
|
|
75
|
+
* The qmd process's OWN captured diagnostic — stderr then stdout. Unlike
|
|
76
|
+
* qmd-native-binding-error.ts this NEVER falls back to the synthesized
|
|
77
|
+
* `message`: the message can carry the caller's query, so reading it would let
|
|
78
|
+
* free text trip the classifier. A value with no captured streams yields "". A
|
|
79
|
+
* bare string is treated as captured text directly (test convenience).
|
|
80
|
+
*/
|
|
81
|
+
function capturedStreams(err) {
|
|
82
|
+
if (typeof err === "string")
|
|
83
|
+
return err;
|
|
84
|
+
if (err === null || typeof err !== "object")
|
|
85
|
+
return "";
|
|
86
|
+
const record = err;
|
|
87
|
+
const stderr = typeof record.stderr === "string" ? record.stderr : "";
|
|
88
|
+
const stdout = typeof record.stdout === "string" ? record.stdout : "";
|
|
89
|
+
return stderr || stdout ? `${stderr}\n${stdout}` : "";
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* True when `err` carries, in qmd's OWN captured streams, a module-resolution
|
|
93
|
+
* failure corroborated by a Node module-loader artifact — i.e. qmd's dependency
|
|
94
|
+
* tree is incomplete on this machine. finishRunQmd calls this on the raw streams
|
|
95
|
+
* to type the condition {@link import('../lib/search-index/index.js').QmdModuleMissingError};
|
|
96
|
+
* a true result there means the caller should reinstall.
|
|
97
|
+
*/
|
|
98
|
+
export function isQmdModuleMissingError(err) {
|
|
99
|
+
const text = capturedStreams(err);
|
|
100
|
+
if (!text)
|
|
101
|
+
return false;
|
|
102
|
+
return MODULE_RESOLUTION_TOKEN.test(text) && MODULE_LOADER_ARTIFACT.test(text);
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* If `err` is a caller-driven qmd module-missing failure (a QmdModuleMissingError
|
|
106
|
+
* from `hq search` or its `vsearch`/`query`/`get` siblings), return the
|
|
107
|
+
* actionable, input-free reinstall remedy; otherwise return `null`. Mirrors
|
|
108
|
+
* qmdNativeBindingErrorMessage / qmdMissingCollectionMessage so the top-level
|
|
109
|
+
* handler branches the same way: a non-null result means print-and-skip-Sentry,
|
|
110
|
+
* null means "handle as usual (capture to Sentry)".
|
|
111
|
+
*
|
|
112
|
+
* Gated on the TYPED CLASS plus a caller-reads invocation allow-list: a
|
|
113
|
+
* module-missing raised by hq's OWN reconciliation (`collection list`, `context
|
|
114
|
+
* add`, …) points at a packaging fault hq itself could be responsible for, so it
|
|
115
|
+
* stays a captured internal error rather than being silenced as the user's
|
|
116
|
+
* install.
|
|
117
|
+
*/
|
|
118
|
+
export function qmdModuleMissingMessage(err) {
|
|
119
|
+
if (err === null || typeof err !== "object")
|
|
120
|
+
return null;
|
|
121
|
+
const record = err;
|
|
122
|
+
if (record.name !== "QmdModuleMissingError")
|
|
123
|
+
return null;
|
|
124
|
+
const args = Array.isArray(record.args) ? record.args : [];
|
|
125
|
+
const subcommand = args[0];
|
|
126
|
+
if (typeof subcommand !== "string" || !CALLER_READS.has(subcommand))
|
|
127
|
+
return null;
|
|
128
|
+
return QMD_MODULE_MISSING_REMEDY;
|
|
129
|
+
}
|
|
130
|
+
//# sourceMappingURL=qmd-module-missing-error.js.map
|
|
@@ -65,13 +65,84 @@ const DEFAULT_GROUPING = "{{ default }}";
|
|
|
65
65
|
* request identifier spliced into the name — could otherwise mint an unbounded
|
|
66
66
|
* number of groups DESPITE the bounded discriminator. The finite-cardinality
|
|
67
67
|
* promise must hold for the WHOLE key, so any name outside this set collapses to
|
|
68
|
-
* FALLBACK_ERROR_NAME.
|
|
69
|
-
*
|
|
68
|
+
* FALLBACK_ERROR_NAME. IntegrationsCliError carries `rpcCode`/`status`; the Qmd*
|
|
69
|
+
* errors carry a process EXIT CODE in `status` and are fingerprinted on the
|
|
70
|
+
* dedicated qmd branch below (allow-listed here so their names appear verbatim
|
|
71
|
+
* rather than collapsing). Add a name here only when a new bounded carrier
|
|
70
72
|
* genuinely warrants its own family of groups.
|
|
71
73
|
*/
|
|
72
|
-
const KNOWN_ERROR_NAMES = new Set([
|
|
74
|
+
const KNOWN_ERROR_NAMES = new Set([
|
|
75
|
+
"IntegrationsCliError",
|
|
76
|
+
// qmd process failures — all extend QmdExitError except QmdBinaryMissingError,
|
|
77
|
+
// which shares the `Qmd` prefix the qmd branch keys on (HQ-CLI-Y, 7690356976).
|
|
78
|
+
"QmdExitError",
|
|
79
|
+
"QmdBinaryMissingError",
|
|
80
|
+
"QmdCollectionMissingError",
|
|
81
|
+
"QmdCollectionExistsError",
|
|
82
|
+
"QmdTerminatedError",
|
|
83
|
+
"QmdLlmDisabledError",
|
|
84
|
+
"QmdModuleMissingError",
|
|
85
|
+
]);
|
|
73
86
|
/** Fixed bucket for any error name outside the closed allowlist. */
|
|
74
87
|
const FALLBACK_ERROR_NAME = "other";
|
|
88
|
+
/**
|
|
89
|
+
* The CLOSED set of qmd subcommands worth their own group — the surfaces hq
|
|
90
|
+
* drives (caller reads plus reconciliation). Anything else, including a missing
|
|
91
|
+
* or non-string args[0], collapses to `qmd:other`, so the subcommand axis is
|
|
92
|
+
* finite no matter what argv qmd was handed (a query value can never reach it).
|
|
93
|
+
*/
|
|
94
|
+
const KNOWN_QMD_SUBCOMMANDS = new Set([
|
|
95
|
+
"search", "vsearch", "query", "get",
|
|
96
|
+
"collection", "context", "embed", "status", "update", "cleanup",
|
|
97
|
+
]);
|
|
98
|
+
/**
|
|
99
|
+
* The small set of qmd process EXIT CODES worth their own group. qmd exits 1 for
|
|
100
|
+
* a general error; any other numeric code collapses to `exit:other`, so the
|
|
101
|
+
* exit-code axis stays finite. A signalled child (`status` null) never reaches
|
|
102
|
+
* this set — it groups by its signal instead (see {@link qmdDispositionToken}).
|
|
103
|
+
*/
|
|
104
|
+
const KNOWN_QMD_EXIT_CODES = new Set([1, 2]);
|
|
105
|
+
/**
|
|
106
|
+
* qmd child TERMINATION SIGNALS worth their own group. A signalled qmd child
|
|
107
|
+
* (QmdTerminatedError: `status` null, `signal` set) carries no exit code, so
|
|
108
|
+
* without a signal axis every native crash would collapse into one `exit:other`
|
|
109
|
+
* bucket — merging the distinct failures (SIGSEGV vs SIGABRT vs SIGBUS) the
|
|
110
|
+
* termination policy deliberately keeps on separate reporting paths. Bounded:
|
|
111
|
+
* any signal outside this closed set collapses to `signal:other`, so the axis
|
|
112
|
+
* stays finite no matter what killed the child.
|
|
113
|
+
*/
|
|
114
|
+
const KNOWN_QMD_SIGNALS = new Set([
|
|
115
|
+
"SIGSEGV", "SIGABRT", "SIGBUS", "SIGILL", "SIGFPE",
|
|
116
|
+
"SIGKILL", "SIGTERM", "SIGINT", "SIGHUP", "SIGQUIT",
|
|
117
|
+
]);
|
|
118
|
+
/** The bounded qmd subcommand token (`qmd:<sub>`), read only from args[0]. */
|
|
119
|
+
function qmdSubcommandToken(args) {
|
|
120
|
+
const first = Array.isArray(args) ? args[0] : undefined;
|
|
121
|
+
return typeof first === "string" && KNOWN_QMD_SUBCOMMANDS.has(first)
|
|
122
|
+
? `qmd:${first}`
|
|
123
|
+
: "qmd:other";
|
|
124
|
+
}
|
|
125
|
+
/** The bounded qmd exit-code token (`exit:<code>`), read only from `status`. */
|
|
126
|
+
function qmdExitToken(status) {
|
|
127
|
+
return typeof status === "number" &&
|
|
128
|
+
Number.isInteger(status) &&
|
|
129
|
+
KNOWN_QMD_EXIT_CODES.has(status)
|
|
130
|
+
? `exit:${status}`
|
|
131
|
+
: "exit:other";
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* The bounded qmd DISPOSITION token — the fingerprint's fourth component. A
|
|
135
|
+
* signalled child (a non-empty `signal` string, `status` null) groups by
|
|
136
|
+
* `signal:<name>` from the closed allow-list so distinct native crashes stay in
|
|
137
|
+
* distinct groups; an ordinary exit groups by `exit:<code>`. Both inputs are
|
|
138
|
+
* bounded, so the axis stays finite regardless of what qmd reported.
|
|
139
|
+
*/
|
|
140
|
+
function qmdDispositionToken(status, signal) {
|
|
141
|
+
if (typeof signal === "string" && signal.length > 0) {
|
|
142
|
+
return KNOWN_QMD_SIGNALS.has(signal) ? `signal:${signal}` : "signal:other";
|
|
143
|
+
}
|
|
144
|
+
return qmdExitToken(status);
|
|
145
|
+
}
|
|
75
146
|
/**
|
|
76
147
|
* Return a bounded `event.fingerprint` array for `err`, or `null` when `err`
|
|
77
148
|
* carries no discriminator from the closed allowlist (in which case the caller
|
|
@@ -98,6 +169,28 @@ export function sentryFingerprintFor(err) {
|
|
|
98
169
|
const rawName = typeof record.name === "string" && record.name.length > 0 ? record.name : null;
|
|
99
170
|
if (rawName === null)
|
|
100
171
|
return null;
|
|
172
|
+
// qmd process failures carry a PROCESS EXIT CODE in `status` (never an HTTP
|
|
173
|
+
// status) plus a subcommand in args[0]. Fingerprint them on a dedicated,
|
|
174
|
+
// fully-bounded key BEFORE the HTTP-status branch so an exit code is never read
|
|
175
|
+
// as an HTTP status, and so grouping is one group per (subcommand, disposition)
|
|
176
|
+
// instead of one per query — the cardinality blow-up that minted a new
|
|
177
|
+
// permanent issue per qmd child failure (HQ-CLI-Y, Sentry 7690356976). The
|
|
178
|
+
// fourth component is the DISPOSITION: an ordinary exit groups by `exit:<code>`,
|
|
179
|
+
// while a signalled child (QmdTerminatedError: status null, signal set) groups
|
|
180
|
+
// by `signal:<name>` so distinct native crashes (SIGSEGV/SIGABRT/SIGBUS) stay
|
|
181
|
+
// in distinct groups rather than collapsing into one `exit:other`. Keyed on the
|
|
182
|
+
// `Qmd` name prefix; the name is still bounded to the closed allowlist, so an
|
|
183
|
+
// unexpected Qmd* name collapses to FALLBACK_ERROR_NAME while keeping the
|
|
184
|
+
// bounded qmd discriminators.
|
|
185
|
+
if (rawName.startsWith("Qmd")) {
|
|
186
|
+
const qmdName = KNOWN_ERROR_NAMES.has(rawName) ? rawName : FALLBACK_ERROR_NAME;
|
|
187
|
+
return [
|
|
188
|
+
DEFAULT_GROUPING,
|
|
189
|
+
qmdName,
|
|
190
|
+
qmdSubcommandToken(record.args),
|
|
191
|
+
qmdDispositionToken(record.status, record.signal),
|
|
192
|
+
];
|
|
193
|
+
}
|
|
101
194
|
const name = KNOWN_ERROR_NAMES.has(rawName) ? rawName : FALLBACK_ERROR_NAME;
|
|
102
195
|
const rpcCode = record.rpcCode;
|
|
103
196
|
if (typeof rpcCode === "number" && Number.isInteger(rpcCode)) {
|