@indigoai-us/hq-cli 5.127.0 → 5.128.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
CHANGED
|
@@ -2,6 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [5.128.0] — 2026-09-22
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Expected hook-timeout warnings are counted as Sentry metrics, so a slow hook
|
|
10
|
+
does not create its own Sentry issue. Genuine timeouts use stable grouping.
|
|
11
|
+
|
|
5
12
|
## [5.127.0] — 2026-09-22
|
|
6
13
|
|
|
7
14
|
### Added
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
* than truncated. Unknown scalar metadata is dropped instead: the allowlist,
|
|
13
13
|
* not a caller version match, determines what may leave the machine.
|
|
14
14
|
*/
|
|
15
|
+
import * as Sentry from "@sentry/node";
|
|
15
16
|
import type { CaptureContext, SeverityLevel } from "@sentry/node";
|
|
16
17
|
export declare const DEFAULT_SENTRY_REPORT_TIMEOUT_MS = 750;
|
|
17
18
|
export declare const MAX_SENTRY_REPORT_TIMEOUT_MS = 5000;
|
|
@@ -41,6 +42,7 @@ export type SentryReportOptions = {
|
|
|
41
42
|
export type SentryReportDependencies = {
|
|
42
43
|
sentry: {
|
|
43
44
|
captureMessage: (message: string, context?: CaptureContext | SeverityLevel) => string;
|
|
45
|
+
metrics: Pick<typeof Sentry.metrics, "count">;
|
|
44
46
|
flush: (timeout?: number) => PromiseLike<boolean>;
|
|
45
47
|
};
|
|
46
48
|
stdout: Pick<NodeJS.WriteStream, "write">;
|
|
@@ -29,6 +29,9 @@ const SENTRY_REPORT_LEVELS = new Set([
|
|
|
29
29
|
"info",
|
|
30
30
|
"debug",
|
|
31
31
|
]);
|
|
32
|
+
const HOOK_TIMEOUT_WARNING_TYPE = "hook_timeout_warning";
|
|
33
|
+
const HOOK_TIMEOUT_WARNING_METRIC = "hq.hook_timeout_warning";
|
|
34
|
+
const HOOK_TIMEOUT_EXCEEDED_FINGERPRINT = "hook-timeout-exceeded";
|
|
32
35
|
/**
|
|
33
36
|
* The only hook diagnostics that can be attached as metadata. The list omits
|
|
34
37
|
* command text, prompts, payloads, company names, and repositories. `hook_path`
|
|
@@ -182,21 +185,43 @@ function resolveTimeout(timeoutMs) {
|
|
|
182
185
|
return null;
|
|
183
186
|
return timeout;
|
|
184
187
|
}
|
|
185
|
-
function sentryEnvelope(event) {
|
|
188
|
+
function sentryEnvelope(event, overrides) {
|
|
186
189
|
return {
|
|
187
|
-
level: event.level,
|
|
188
|
-
fingerprint: [event.fingerprint],
|
|
190
|
+
level: overrides?.level ?? event.level,
|
|
191
|
+
fingerprint: overrides?.fingerprint ?? [event.fingerprint],
|
|
189
192
|
tags: { sentry_event_type: event.type },
|
|
190
193
|
extra: event.metadata,
|
|
191
194
|
};
|
|
192
195
|
}
|
|
193
196
|
/** Build the same scrubbed event projection that the live SDK path uses. */
|
|
194
|
-
function scrubbedSentryEnvelope(event) {
|
|
197
|
+
function scrubbedSentryEnvelope(event, overrides) {
|
|
195
198
|
return beforeSend({
|
|
196
199
|
message: event.message,
|
|
197
|
-
...sentryEnvelope(event),
|
|
200
|
+
...sentryEnvelope(event, overrides),
|
|
198
201
|
}, {});
|
|
199
202
|
}
|
|
203
|
+
/**
|
|
204
|
+
* The watchdog's relative threshold fires before the declared timeout and
|
|
205
|
+
* clamps remaining_ms at zero, so its real warnings stay on the metric path.
|
|
206
|
+
* Keep a defensive branch for a genuine exceeded payload from another
|
|
207
|
+
* producer without restoring per-hook grouping.
|
|
208
|
+
*/
|
|
209
|
+
function hookTimeoutExceededOverrides(event) {
|
|
210
|
+
if (event.type !== HOOK_TIMEOUT_WARNING_TYPE)
|
|
211
|
+
return undefined;
|
|
212
|
+
const elapsedMs = event.metadata.elapsed_ms;
|
|
213
|
+
const declaredTimeoutMs = event.metadata.declared_timeout_ms;
|
|
214
|
+
if (typeof elapsedMs !== "number" ||
|
|
215
|
+
typeof declaredTimeoutMs !== "number" ||
|
|
216
|
+
elapsedMs < declaredTimeoutMs) {
|
|
217
|
+
return undefined;
|
|
218
|
+
}
|
|
219
|
+
const hookEvent = typeof event.metadata.hook_event === "string" ? event.metadata.hook_event : "unknown";
|
|
220
|
+
return {
|
|
221
|
+
level: "warning",
|
|
222
|
+
fingerprint: [HOOK_TIMEOUT_EXCEEDED_FINGERPRINT, hookEvent],
|
|
223
|
+
};
|
|
224
|
+
}
|
|
200
225
|
/**
|
|
201
226
|
* Resolve true only when the local transport queue drains within the budget.
|
|
202
227
|
* This does not confirm that the ingest service accepted the event.
|
|
@@ -234,13 +259,28 @@ export async function reportSentryEvent(stdin, options = {}, dependencies = defa
|
|
|
234
259
|
const noun = result.droppedMetadataKeys === 1 ? "key" : "keys";
|
|
235
260
|
dependencies.stderr.write(`Dropped ${result.droppedMetadataKeys} unsupported Sentry report metadata ${noun}.\n`);
|
|
236
261
|
}
|
|
237
|
-
const
|
|
262
|
+
const overrides = hookTimeoutExceededOverrides(result.event);
|
|
238
263
|
if (options.dryRun) {
|
|
239
|
-
const scrubbed = scrubbedSentryEnvelope(result.event);
|
|
264
|
+
const scrubbed = scrubbedSentryEnvelope(result.event, overrides);
|
|
240
265
|
if (scrubbed)
|
|
241
266
|
dependencies.stdout.write(`${JSON.stringify(scrubbed)}\n`);
|
|
242
267
|
return result;
|
|
243
268
|
}
|
|
269
|
+
if (result.event.type === HOOK_TIMEOUT_WARNING_TYPE && !overrides) {
|
|
270
|
+
try {
|
|
271
|
+
dependencies.sentry.metrics.count(HOOK_TIMEOUT_WARNING_METRIC, 1, {
|
|
272
|
+
attributes: result.event.metadata,
|
|
273
|
+
});
|
|
274
|
+
if (await flushWithin(dependencies.sentry, timeoutMs))
|
|
275
|
+
return result;
|
|
276
|
+
}
|
|
277
|
+
catch {
|
|
278
|
+
// Report failure below without exposing event fields or transport details.
|
|
279
|
+
}
|
|
280
|
+
dependencies.stderr.write("Sentry report failed.\n");
|
|
281
|
+
return invalidInput();
|
|
282
|
+
}
|
|
283
|
+
const envelope = sentryEnvelope(result.event, overrides);
|
|
244
284
|
try {
|
|
245
285
|
dependencies.sentry.captureMessage(result.event.message, envelope);
|
|
246
286
|
if (await flushWithin(dependencies.sentry, timeoutMs))
|