@unbrained/pm-cli 2026.5.2 → 2026.5.3-5
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/AGENTS.md +8 -1
- package/CHANGELOG.md +53 -0
- package/README.md +9 -1
- package/dist/cli/bootstrap-args.d.ts +18 -0
- package/dist/cli/bootstrap-args.js +242 -0
- package/dist/cli/bootstrap-args.js.map +1 -0
- package/dist/cli/commander-usage.d.ts +17 -0
- package/dist/cli/commander-usage.js +178 -0
- package/dist/cli/commander-usage.js.map +1 -0
- package/dist/cli/commands/activity.js +1 -9
- package/dist/cli/commands/activity.js.map +1 -1
- package/dist/cli/commands/calendar.js +3 -29
- package/dist/cli/commands/calendar.js.map +1 -1
- package/dist/cli/commands/comments.js +1 -9
- package/dist/cli/commands/comments.js.map +1 -1
- package/dist/cli/commands/config.d.ts +21 -3
- package/dist/cli/commands/config.js +118 -2
- package/dist/cli/commands/config.js.map +1 -1
- package/dist/cli/commands/context.d.ts +90 -1
- package/dist/cli/commands/context.js +485 -23
- package/dist/cli/commands/context.js.map +1 -1
- package/dist/cli/commands/dedupe-audit.js +2 -11
- package/dist/cli/commands/dedupe-audit.js.map +1 -1
- package/dist/cli/commands/history.js +1 -9
- package/dist/cli/commands/history.js.map +1 -1
- package/dist/cli/commands/learnings.js +1 -9
- package/dist/cli/commands/learnings.js.map +1 -1
- package/dist/cli/commands/list.js +3 -29
- package/dist/cli/commands/list.js.map +1 -1
- package/dist/cli/commands/normalize.js +9 -6
- package/dist/cli/commands/normalize.js.map +1 -1
- package/dist/cli/commands/notes.js +1 -9
- package/dist/cli/commands/notes.js.map +1 -1
- package/dist/cli/commands/reindex.js +2 -7
- package/dist/cli/commands/reindex.js.map +1 -1
- package/dist/cli/commands/search.js +4 -35
- package/dist/cli/commands/search.js.map +1 -1
- package/dist/cli/commands/test-runs.js +1 -11
- package/dist/cli/commands/test-runs.js.map +1 -1
- package/dist/cli/error-guidance.d.ts +13 -0
- package/dist/cli/error-guidance.js +43 -4
- package/dist/cli/error-guidance.js.map +1 -1
- package/dist/cli/extension-command-help.d.ts +48 -0
- package/dist/cli/extension-command-help.js +389 -0
- package/dist/cli/extension-command-help.js.map +1 -0
- package/dist/cli/help-content.js +9 -3
- package/dist/cli/help-content.js.map +1 -1
- package/dist/cli/help-json-payload.d.ts +25 -0
- package/dist/cli/help-json-payload.js +265 -0
- package/dist/cli/help-json-payload.js.map +1 -0
- package/dist/cli/main.js +996 -4468
- package/dist/cli/main.js.map +1 -1
- package/dist/cli/migration-gates.d.ts +22 -0
- package/dist/cli/migration-gates.js +146 -0
- package/dist/cli/migration-gates.js.map +1 -0
- package/dist/cli/register-list-query.d.ts +2 -0
- package/dist/cli/register-list-query.js +317 -0
- package/dist/cli/register-list-query.js.map +1 -0
- package/dist/cli/register-mutation.d.ts +2 -0
- package/dist/cli/register-mutation.js +795 -0
- package/dist/cli/register-mutation.js.map +1 -0
- package/dist/cli/register-operations.d.ts +2 -0
- package/dist/cli/register-operations.js +610 -0
- package/dist/cli/register-operations.js.map +1 -0
- package/dist/cli/register-setup.d.ts +2 -0
- package/dist/cli/register-setup.js +334 -0
- package/dist/cli/register-setup.js.map +1 -0
- package/dist/cli/registration-helpers.d.ts +53 -0
- package/dist/cli/registration-helpers.js +669 -0
- package/dist/cli/registration-helpers.js.map +1 -0
- package/dist/cli/shared-parsers.d.ts +6 -0
- package/dist/cli/shared-parsers.js +40 -0
- package/dist/cli/shared-parsers.js.map +1 -0
- package/dist/core/search/http-client.d.ts +29 -0
- package/dist/core/search/http-client.js +64 -0
- package/dist/core/search/http-client.js.map +1 -0
- package/dist/core/search/providers.d.ts +3 -13
- package/dist/core/search/providers.js +19 -69
- package/dist/core/search/providers.js.map +1 -1
- package/dist/core/search/semantic-defaults.js +2 -7
- package/dist/core/search/semantic-defaults.js.map +1 -1
- package/dist/core/search/vector-stores.d.ts +3 -13
- package/dist/core/search/vector-stores.js +17 -66
- package/dist/core/search/vector-stores.js.map +1 -1
- package/dist/core/sentry/helpers.d.ts +23 -2
- package/dist/core/sentry/helpers.js +101 -3
- package/dist/core/sentry/helpers.js.map +1 -1
- package/dist/core/sentry/instrument.d.ts +21 -0
- package/dist/core/sentry/instrument.js +34 -3
- package/dist/core/sentry/instrument.js.map +1 -1
- package/dist/core/shared/constants.d.ts +3 -0
- package/dist/core/shared/constants.js +58 -1
- package/dist/core/shared/constants.js.map +1 -1
- package/dist/core/store/front-matter-cache.d.ts +6 -0
- package/dist/core/store/front-matter-cache.js +150 -0
- package/dist/core/store/front-matter-cache.js.map +1 -0
- package/dist/core/store/item-store.js +2 -1
- package/dist/core/store/item-store.js.map +1 -1
- package/dist/core/store/settings.js +36 -0
- package/dist/core/store/settings.js.map +1 -1
- package/dist/core/telemetry/observability.d.ts +24 -0
- package/dist/core/telemetry/observability.js +185 -0
- package/dist/core/telemetry/observability.js.map +1 -0
- package/dist/core/telemetry/runtime.d.ts +27 -3
- package/dist/core/telemetry/runtime.js +298 -13
- package/dist/core/telemetry/runtime.js.map +1 -1
- package/dist/sdk/cli-contracts.js +28 -0
- package/dist/sdk/cli-contracts.js.map +1 -1
- package/dist/types.d.ts +21 -0
- package/dist/types.js +11 -0
- package/dist/types.js.map +1 -1
- package/docs/ARCHITECTURE.md +7 -1
- package/docs/COMMANDS.md +11 -1
- package/docs/RELEASING.md +56 -29
- package/package.json +8 -3
|
@@ -2,9 +2,11 @@ import crypto from "node:crypto";
|
|
|
2
2
|
import os from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { appendLineAtomic, readFileIfExists, writeFileAtomic } from "../fs/fs-utils.js";
|
|
5
|
+
import { resolveTelemetryErrorCategory } from "../shared/constants.js";
|
|
5
6
|
import { nowIso } from "../shared/time.js";
|
|
6
7
|
import { resolveGlobalPmRoot } from "../store/paths.js";
|
|
7
8
|
import { readSettings, writeSettings } from "../store/settings.js";
|
|
9
|
+
import { deriveTelemetryCommandResolution, deriveTelemetryCommandTaxonomy, inferTelemetryErrorCode, } from "./observability.js";
|
|
8
10
|
const TELEMETRY_QUEUE_RELATIVE_PATH = path.join("runtime", "telemetry", "events.jsonl");
|
|
9
11
|
const TELEMETRY_STATE_RELATIVE_PATH = path.join("runtime", "telemetry", "state.json");
|
|
10
12
|
const TELEMETRY_SCHEMA_VERSION = 1;
|
|
@@ -52,6 +54,9 @@ const SENSITIVE_INLINE_KEY_PATTERN = "(?:token|secret|password|passwd|api[_-]?ke
|
|
|
52
54
|
const INLINE_SENSITIVE_ASSIGNMENT_PATTERN = new RegExp(`\\b(${SENSITIVE_INLINE_KEY_PATTERN})\\s*([:=])\\s*([^\\s,;]+)`, "giu");
|
|
53
55
|
const INLINE_SENSITIVE_FLAG_PATTERN = new RegExp(`(--${SENSITIVE_INLINE_KEY_PATTERN})(=|\\s+)([^\\s,;]+)`, "giu");
|
|
54
56
|
const ABSOLUTE_PATH_TOKEN_PATTERN = /(^|[\s"'`(=])\/(?:[^\s"'`),;]+)/g;
|
|
57
|
+
const EMAIL_PATTERN = /[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/giu;
|
|
58
|
+
const BEARER_TOKEN_PATTERN = /bearer\s+[a-z0-9._=-]+/giu;
|
|
59
|
+
const PRIVATE_IP_PATTERN = /\b(?:10\.(?:25[0-5]|2[0-4]\d|[01]?\d?\d)\.(?:25[0-5]|2[0-4]\d|[01]?\d?\d)\.(?:25[0-5]|2[0-4]\d|[01]?\d?\d)|172\.(?:1[6-9]|2\d|3[01])\.(?:25[0-5]|2[0-4]\d|[01]?\d?\d)\.(?:25[0-5]|2[0-4]\d|[01]?\d?\d)|192\.168\.(?:25[0-5]|2[0-4]\d|[01]?\d?\d)\.(?:25[0-5]|2[0-4]\d|[01]?\d?\d))\b/g;
|
|
55
60
|
function queuePath(globalPmRoot) {
|
|
56
61
|
return path.join(globalPmRoot, TELEMETRY_QUEUE_RELATIVE_PATH);
|
|
57
62
|
}
|
|
@@ -102,10 +107,14 @@ function redactInlineSensitiveAssignments(input) {
|
|
|
102
107
|
function redactAbsolutePathTokens(input) {
|
|
103
108
|
return input.replaceAll(ABSOLUTE_PATH_TOKEN_PATTERN, (_match, prefix) => `${prefix}[redacted_path]`);
|
|
104
109
|
}
|
|
110
|
+
function sanitizeCommonSensitiveTokens(input) {
|
|
111
|
+
const withoutEmails = input.replaceAll(EMAIL_PATTERN, "[redacted_email]");
|
|
112
|
+
const withoutBearer = withoutEmails.replaceAll(BEARER_TOKEN_PATTERN, "bearer [redacted_token]");
|
|
113
|
+
return withoutBearer.replaceAll(PRIVATE_IP_PATTERN, "[redacted_ip]");
|
|
114
|
+
}
|
|
105
115
|
function sanitizeStringRedacted(input) {
|
|
106
|
-
const
|
|
107
|
-
const
|
|
108
|
-
const withoutInlineSecrets = redactInlineSensitiveAssignments(withoutBearer);
|
|
116
|
+
const withoutCommonSensitiveTokens = sanitizeCommonSensitiveTokens(input);
|
|
117
|
+
const withoutInlineSecrets = redactInlineSensitiveAssignments(withoutCommonSensitiveTokens);
|
|
109
118
|
const withoutAbsolutePaths = redactAbsolutePathTokens(withoutInlineSecrets);
|
|
110
119
|
const trimmed = withoutAbsolutePaths.trim();
|
|
111
120
|
if (trimmed.startsWith("/") && trimmed.length > 1) {
|
|
@@ -117,12 +126,17 @@ function sanitizeStringRedacted(input) {
|
|
|
117
126
|
return withoutAbsolutePaths;
|
|
118
127
|
}
|
|
119
128
|
function sanitizeStringMax(input) {
|
|
120
|
-
const
|
|
121
|
-
const withoutInlineSecrets = redactInlineSensitiveAssignments(
|
|
122
|
-
|
|
123
|
-
|
|
129
|
+
const withoutCommonSensitiveTokens = sanitizeCommonSensitiveTokens(input);
|
|
130
|
+
const withoutInlineSecrets = redactInlineSensitiveAssignments(withoutCommonSensitiveTokens);
|
|
131
|
+
const withoutAbsolutePaths = redactAbsolutePathTokens(withoutInlineSecrets);
|
|
132
|
+
const trimmed = withoutAbsolutePaths.trim();
|
|
133
|
+
if (trimmed.startsWith("/") && trimmed.length > 1) {
|
|
134
|
+
return "[redacted_path]";
|
|
135
|
+
}
|
|
136
|
+
if (withoutAbsolutePaths.length > 2048) {
|
|
137
|
+
return `${withoutAbsolutePaths.slice(0, 2045)}...`;
|
|
124
138
|
}
|
|
125
|
-
return
|
|
139
|
+
return withoutAbsolutePaths;
|
|
126
140
|
}
|
|
127
141
|
function sanitizeString(input, captureLevel = "redacted") {
|
|
128
142
|
if (captureLevel === "max") {
|
|
@@ -207,6 +221,28 @@ function normalizePmVersion(value) {
|
|
|
207
221
|
const trimmed = value?.trim() ?? "";
|
|
208
222
|
return trimmed.length > 0 ? trimmed : "0.0.0";
|
|
209
223
|
}
|
|
224
|
+
function normalizeTelemetryErrorCode(value) {
|
|
225
|
+
const normalized = value?.trim();
|
|
226
|
+
return normalized && normalized.length > 0 ? normalized : undefined;
|
|
227
|
+
}
|
|
228
|
+
function normalizeTelemetryExitCode(exitCode, ok) {
|
|
229
|
+
if (Number.isFinite(exitCode)) {
|
|
230
|
+
return Math.max(0, Math.trunc(exitCode ?? 0));
|
|
231
|
+
}
|
|
232
|
+
return ok ? 0 : 1;
|
|
233
|
+
}
|
|
234
|
+
function normalizeTelemetryErrorCategory(params) {
|
|
235
|
+
if (params.ok) {
|
|
236
|
+
return undefined;
|
|
237
|
+
}
|
|
238
|
+
if (typeof params.errorCategory === "string" && params.errorCategory.trim().length > 0) {
|
|
239
|
+
return params.errorCategory;
|
|
240
|
+
}
|
|
241
|
+
if (typeof params.errorCode === "string" && params.errorCode.trim().length > 0) {
|
|
242
|
+
return resolveTelemetryErrorCategory(params.errorCode);
|
|
243
|
+
}
|
|
244
|
+
return "unknown";
|
|
245
|
+
}
|
|
210
246
|
function resolveTelemetrySourceContext(globalOptions) {
|
|
211
247
|
const override = (process.env[PM_TELEMETRY_SOURCE_CONTEXT_ENV] ?? "").trim().toLowerCase();
|
|
212
248
|
if (PM_TELEMETRY_SOURCE_CONTEXT_SET.has(override)) {
|
|
@@ -239,6 +275,44 @@ function resolveTelemetrySourceContext(globalOptions) {
|
|
|
239
275
|
function hashWithInstallationId(installationId, value) {
|
|
240
276
|
return crypto.createHash("sha256").update(`${installationId}:${value}`).digest("hex");
|
|
241
277
|
}
|
|
278
|
+
function normalizeForHash(value, depth = 0) {
|
|
279
|
+
if (value === null || value === undefined) {
|
|
280
|
+
return value;
|
|
281
|
+
}
|
|
282
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
283
|
+
return value;
|
|
284
|
+
}
|
|
285
|
+
if (depth >= TELEMETRY_SANITIZE_MAX_DEPTH) {
|
|
286
|
+
return "[depth_truncated]";
|
|
287
|
+
}
|
|
288
|
+
if (Array.isArray(value)) {
|
|
289
|
+
return value.slice(0, TELEMETRY_SANITIZE_MAX_ARRAY_ITEMS).map((entry) => normalizeForHash(entry, depth + 1));
|
|
290
|
+
}
|
|
291
|
+
if (typeof value === "object") {
|
|
292
|
+
const record = value;
|
|
293
|
+
const normalized = {};
|
|
294
|
+
const sortedKeys = Object.keys(record).sort((left, right) => left.localeCompare(right));
|
|
295
|
+
for (const key of sortedKeys) {
|
|
296
|
+
normalized[key] = normalizeForHash(record[key], depth + 1);
|
|
297
|
+
}
|
|
298
|
+
return normalized;
|
|
299
|
+
}
|
|
300
|
+
return String(value);
|
|
301
|
+
}
|
|
302
|
+
function hashTelemetryValue(installationId, value) {
|
|
303
|
+
return hashWithInstallationId(installationId, JSON.stringify(normalizeForHash(value)));
|
|
304
|
+
}
|
|
305
|
+
function hashCommandArgs(installationId, args) {
|
|
306
|
+
return {
|
|
307
|
+
hashes: args.map((arg) => hashWithInstallationId(installationId, arg)),
|
|
308
|
+
digest: hashWithInstallationId(installationId, args.join("\u0000")),
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
function hashTelemetryErrorFingerprint(installationId, command, errorCode, errorMessage) {
|
|
312
|
+
const normalizedMessage = sanitizeString(errorMessage ?? "", "redacted");
|
|
313
|
+
const fingerprintSource = `${command}\u0000${errorCode ?? "unknown_error"}\u0000${normalizedMessage}`;
|
|
314
|
+
return hashWithInstallationId(installationId, fingerprintSource);
|
|
315
|
+
}
|
|
242
316
|
function telemetryDisabledByEnvironment() {
|
|
243
317
|
return PM_TELEMETRY_DISABLED_VALUES.has((process.env[PM_TELEMETRY_DISABLED_ENV] ?? "").trim().toLowerCase());
|
|
244
318
|
}
|
|
@@ -305,6 +379,13 @@ async function exportLocalOtelSpan(activeCommand, outcome, finishedAtIso, durati
|
|
|
305
379
|
}
|
|
306
380
|
const serviceNameCandidate = sanitizeString((process.env[OTEL_SERVICE_NAME_ENV] ?? "").trim());
|
|
307
381
|
const serviceName = serviceNameCandidate.length > 0 ? serviceNameCandidate : "pm-cli";
|
|
382
|
+
const normalizedExitCode = normalizeTelemetryExitCode(outcome.exit_code, outcome.ok);
|
|
383
|
+
const normalizedErrorCode = normalizeTelemetryErrorCode(outcome.error_code);
|
|
384
|
+
const normalizedErrorCategory = normalizeTelemetryErrorCategory({
|
|
385
|
+
ok: outcome.ok,
|
|
386
|
+
errorCode: normalizedErrorCode,
|
|
387
|
+
errorCategory: outcome.error_category,
|
|
388
|
+
});
|
|
308
389
|
const attributes = [
|
|
309
390
|
otlpStringAttribute("pm.command", sanitizeString(activeCommand.command)),
|
|
310
391
|
otlpStringAttribute("pm.version", activeCommand.pm_version),
|
|
@@ -315,8 +396,15 @@ async function exportLocalOtelSpan(activeCommand, outcome, finishedAtIso, durati
|
|
|
315
396
|
otlpStringAttribute("pm.pm_root_hash", activeCommand.pm_root_hash),
|
|
316
397
|
otlpStringAttribute("pm.cwd_hash", activeCommand.cwd_hash),
|
|
317
398
|
otlpBoolAttribute("pm.ok", outcome.ok),
|
|
399
|
+
otlpIntAttribute("pm.exit_code", normalizedExitCode),
|
|
318
400
|
otlpIntAttribute("pm.duration_ms", durationMs),
|
|
319
401
|
];
|
|
402
|
+
if (typeof normalizedErrorCode === "string") {
|
|
403
|
+
attributes.push(otlpStringAttribute("pm.error_code", normalizedErrorCode));
|
|
404
|
+
}
|
|
405
|
+
if (typeof normalizedErrorCategory === "string") {
|
|
406
|
+
attributes.push(otlpStringAttribute("pm.error_category", normalizedErrorCategory));
|
|
407
|
+
}
|
|
320
408
|
if (typeof outcome.error === "string" && outcome.error.trim().length > 0) {
|
|
321
409
|
attributes.push(otlpStringAttribute("pm.error", sanitizeString(outcome.error)));
|
|
322
410
|
}
|
|
@@ -407,21 +495,37 @@ function summarizeResult(result, captureLevel = "redacted") {
|
|
|
407
495
|
}
|
|
408
496
|
function buildCommandStartPayload(params) {
|
|
409
497
|
const { captureLevel, context, pmVersion, sourceContext, pmRootHash, cwdHash, installationId } = params;
|
|
498
|
+
const commandTaxonomy = deriveTelemetryCommandTaxonomy(context.command);
|
|
499
|
+
const hashedArgs = hashCommandArgs(installationId, context.args);
|
|
500
|
+
const commandInvocationDigest = hashWithInstallationId(installationId, `${context.command}\u0000${context.args.join("\u0000")}`);
|
|
501
|
+
const commandOptionsDigest = hashTelemetryValue(installationId, context.options);
|
|
502
|
+
const globalOptionsDigest = hashTelemetryValue(installationId, context.global);
|
|
410
503
|
if (captureLevel === "minimal") {
|
|
411
504
|
return {
|
|
412
505
|
capture_level: captureLevel,
|
|
413
506
|
pm_version: pmVersion,
|
|
414
507
|
source_context: sourceContext.source_context,
|
|
415
508
|
source_context_source: sourceContext.source_context_source,
|
|
509
|
+
command_taxonomy: commandTaxonomy,
|
|
510
|
+
command_args_digest: hashedArgs.digest,
|
|
511
|
+
command_invocation_digest: commandInvocationDigest,
|
|
512
|
+
command_options_digest: commandOptionsDigest,
|
|
513
|
+
global_options_digest: globalOptionsDigest,
|
|
416
514
|
};
|
|
417
515
|
}
|
|
418
516
|
return {
|
|
419
517
|
pm_version: pmVersion,
|
|
420
518
|
source_context: sourceContext.source_context,
|
|
421
519
|
source_context_source: sourceContext.source_context_source,
|
|
520
|
+
command_taxonomy: commandTaxonomy,
|
|
422
521
|
command_args: sanitizeCommandArgs(context.args, captureLevel),
|
|
522
|
+
command_args_hashes: hashedArgs.hashes,
|
|
523
|
+
command_args_digest: hashedArgs.digest,
|
|
524
|
+
command_invocation_digest: commandInvocationDigest,
|
|
423
525
|
command_options: sanitizeValue(context.options, undefined, captureLevel),
|
|
526
|
+
command_options_digest: commandOptionsDigest,
|
|
424
527
|
global_options: sanitizeValue(context.global, undefined, captureLevel),
|
|
528
|
+
global_options_digest: globalOptionsDigest,
|
|
425
529
|
pm_root_hash: pmRootHash,
|
|
426
530
|
cwd_hash: cwdHash,
|
|
427
531
|
capture_level: captureLevel,
|
|
@@ -436,15 +540,25 @@ function buildCommandStartPayload(params) {
|
|
|
436
540
|
};
|
|
437
541
|
}
|
|
438
542
|
function buildCommandFinishPayload(params) {
|
|
439
|
-
const { captureLevel, pmVersion, sourceContext, outcome, durationMs, startedAt } = params;
|
|
543
|
+
const { captureLevel, pmVersion, sourceContext, outcome, durationMs, startedAt, command, installationId, commandTaxonomy, exitCode, errorCode, errorCategory, commandResolution, resolutionStage, } = params;
|
|
544
|
+
const errorFingerprint = outcome.ok === false
|
|
545
|
+
? hashTelemetryErrorFingerprint(installationId, command, errorCode, outcome.error)
|
|
546
|
+
: undefined;
|
|
440
547
|
if (captureLevel === "minimal") {
|
|
441
548
|
return {
|
|
442
549
|
capture_level: captureLevel,
|
|
443
550
|
pm_version: pmVersion,
|
|
444
551
|
source_context: sourceContext.source_context,
|
|
445
552
|
source_context_source: sourceContext.source_context_source,
|
|
553
|
+
command_taxonomy: commandTaxonomy,
|
|
554
|
+
command_resolution: commandResolution,
|
|
555
|
+
resolution_stage: resolutionStage,
|
|
446
556
|
ok: outcome.ok,
|
|
557
|
+
exit_code: exitCode,
|
|
558
|
+
error_code: errorCode,
|
|
559
|
+
error_category: errorCategory,
|
|
447
560
|
error: outcome.error ? sanitizeString(outcome.error, "redacted") : undefined,
|
|
561
|
+
error_fingerprint: errorFingerprint,
|
|
448
562
|
duration_ms: durationMs,
|
|
449
563
|
};
|
|
450
564
|
}
|
|
@@ -453,13 +567,77 @@ function buildCommandFinishPayload(params) {
|
|
|
453
567
|
pm_version: pmVersion,
|
|
454
568
|
source_context: sourceContext.source_context,
|
|
455
569
|
source_context_source: sourceContext.source_context_source,
|
|
570
|
+
command_taxonomy: commandTaxonomy,
|
|
571
|
+
command_resolution: commandResolution,
|
|
572
|
+
resolution_stage: resolutionStage,
|
|
456
573
|
ok: outcome.ok,
|
|
574
|
+
exit_code: exitCode,
|
|
575
|
+
error_code: errorCode,
|
|
576
|
+
error_category: errorCategory,
|
|
457
577
|
error: outcome.error ? sanitizeString(outcome.error, captureLevel) : undefined,
|
|
578
|
+
error_fingerprint: errorFingerprint,
|
|
458
579
|
duration_ms: durationMs,
|
|
459
580
|
started_at: startedAt,
|
|
460
581
|
result_summary: summarizeResult(outcome.result, captureLevel),
|
|
461
582
|
};
|
|
462
583
|
}
|
|
584
|
+
function buildCommandErrorPayload(params) {
|
|
585
|
+
const { captureLevel, pmVersion, sourceContext, command, commandTaxonomy, commandResolution, resolutionStage, args, options, pmRootHash, cwdHash, installationId, errorCode, errorMessage, errorCategory, exitCode, } = params;
|
|
586
|
+
const attemptedArgHashes = hashCommandArgs(installationId, args);
|
|
587
|
+
const attemptedCommandDigest = hashWithInstallationId(installationId, command);
|
|
588
|
+
const attemptedOptionsDigest = hashTelemetryValue(installationId, options);
|
|
589
|
+
const errorFingerprint = hashTelemetryErrorFingerprint(installationId, command, errorCode, errorMessage);
|
|
590
|
+
if (captureLevel === "minimal") {
|
|
591
|
+
return {
|
|
592
|
+
capture_level: captureLevel,
|
|
593
|
+
pm_version: pmVersion,
|
|
594
|
+
source_context: sourceContext.source_context,
|
|
595
|
+
source_context_source: sourceContext.source_context_source,
|
|
596
|
+
command_taxonomy: commandTaxonomy,
|
|
597
|
+
command_resolution: commandResolution,
|
|
598
|
+
resolution_stage: resolutionStage,
|
|
599
|
+
attempted_command_digest: attemptedCommandDigest,
|
|
600
|
+
attempted_args_digest: attemptedArgHashes.digest,
|
|
601
|
+
attempted_options_digest: attemptedOptionsDigest,
|
|
602
|
+
error_code: errorCode,
|
|
603
|
+
error_category: errorCategory,
|
|
604
|
+
exit_code: exitCode,
|
|
605
|
+
error: sanitizeString(errorMessage, "redacted"),
|
|
606
|
+
error_fingerprint: errorFingerprint,
|
|
607
|
+
};
|
|
608
|
+
}
|
|
609
|
+
return {
|
|
610
|
+
capture_level: captureLevel,
|
|
611
|
+
pm_version: pmVersion,
|
|
612
|
+
source_context: sourceContext.source_context,
|
|
613
|
+
source_context_source: sourceContext.source_context_source,
|
|
614
|
+
command_taxonomy: commandTaxonomy,
|
|
615
|
+
command_resolution: commandResolution,
|
|
616
|
+
resolution_stage: resolutionStage,
|
|
617
|
+
attempted_command: sanitizeString(command, captureLevel),
|
|
618
|
+
attempted_command_digest: attemptedCommandDigest,
|
|
619
|
+
attempted_args: sanitizeCommandArgs(args, captureLevel),
|
|
620
|
+
attempted_args_hashes: attemptedArgHashes.hashes,
|
|
621
|
+
attempted_args_digest: attemptedArgHashes.digest,
|
|
622
|
+
attempted_options: sanitizeValue(options, undefined, captureLevel),
|
|
623
|
+
attempted_options_digest: attemptedOptionsDigest,
|
|
624
|
+
error_code: errorCode,
|
|
625
|
+
error_category: errorCategory,
|
|
626
|
+
exit_code: exitCode,
|
|
627
|
+
error: sanitizeString(errorMessage, captureLevel),
|
|
628
|
+
error_fingerprint: errorFingerprint,
|
|
629
|
+
pm_root_hash: pmRootHash,
|
|
630
|
+
cwd_hash: cwdHash,
|
|
631
|
+
runtime: {
|
|
632
|
+
node: process.version,
|
|
633
|
+
platform: process.platform,
|
|
634
|
+
arch: process.arch,
|
|
635
|
+
hostname_hash: hashWithInstallationId(installationId, os.hostname()),
|
|
636
|
+
stdin_tty: process.stdin.isTTY === true,
|
|
637
|
+
stdout_tty: process.stdout.isTTY === true,
|
|
638
|
+
},
|
|
639
|
+
};
|
|
640
|
+
}
|
|
463
641
|
async function ensureInstallationId(globalPmRoot) {
|
|
464
642
|
const settings = await readSettings(globalPmRoot);
|
|
465
643
|
let changed = false;
|
|
@@ -605,12 +783,17 @@ async function flushQueue(globalPmRoot, endpoint, retentionDays) {
|
|
|
605
783
|
}
|
|
606
784
|
const dueIds = new Set(dueEntries.map((entry) => entry.event.event_id));
|
|
607
785
|
const attemptTime = nowIso();
|
|
786
|
+
const requestHeaders = {
|
|
787
|
+
"content-type": "application/json",
|
|
788
|
+
};
|
|
789
|
+
const ingestKey = process.env.PM_TELEMETRY_INGEST_KEY?.trim();
|
|
790
|
+
if (ingestKey) {
|
|
791
|
+
requestHeaders["x-pm-telemetry-key"] = ingestKey;
|
|
792
|
+
}
|
|
608
793
|
try {
|
|
609
794
|
const response = await fetch(normalizedEndpoint, {
|
|
610
795
|
method: "POST",
|
|
611
|
-
headers:
|
|
612
|
-
"content-type": "application/json",
|
|
613
|
-
},
|
|
796
|
+
headers: requestHeaders,
|
|
614
797
|
body: JSON.stringify({
|
|
615
798
|
schema_version: TELEMETRY_SCHEMA_VERSION,
|
|
616
799
|
events: dueEntries.map((entry) => entry.event),
|
|
@@ -698,10 +881,12 @@ export async function startTelemetryCommand(context) {
|
|
|
698
881
|
};
|
|
699
882
|
await enqueueTelemetryEvent(globalPmRoot, event);
|
|
700
883
|
_lastFlushPromise = flushQueue(globalPmRoot, endpoint, retentionDays).catch(() => { });
|
|
884
|
+
const commandTaxonomy = deriveTelemetryCommandTaxonomy(context.command);
|
|
701
885
|
return {
|
|
702
886
|
started_at: occurredAt,
|
|
703
887
|
started_at_ms: Date.now(),
|
|
704
888
|
command: context.command,
|
|
889
|
+
command_taxonomy: commandTaxonomy,
|
|
705
890
|
pm_version: pmVersion,
|
|
706
891
|
source_context: sourceContext.source_context,
|
|
707
892
|
source_context_source: sourceContext.source_context_source,
|
|
@@ -729,6 +914,25 @@ export async function finishTelemetryCommand(activeCommand, outcome) {
|
|
|
729
914
|
try {
|
|
730
915
|
const finishedAt = nowIso();
|
|
731
916
|
const durationMs = Math.max(0, Date.now() - activeCommand.started_at_ms);
|
|
917
|
+
const normalizedErrorCode = normalizeTelemetryErrorCode(inferTelemetryErrorCode({
|
|
918
|
+
ok: outcome.ok,
|
|
919
|
+
errorCode: outcome.error_code,
|
|
920
|
+
errorMessage: outcome.error,
|
|
921
|
+
exitCode: outcome.exit_code,
|
|
922
|
+
}));
|
|
923
|
+
const normalizedErrorCategory = normalizeTelemetryErrorCategory({
|
|
924
|
+
ok: outcome.ok,
|
|
925
|
+
errorCode: normalizedErrorCode,
|
|
926
|
+
errorCategory: outcome.error_category,
|
|
927
|
+
});
|
|
928
|
+
const normalizedExitCode = normalizeTelemetryExitCode(outcome.exit_code, outcome.ok);
|
|
929
|
+
const commandResolution = outcome.command_resolution ??
|
|
930
|
+
deriveTelemetryCommandResolution({
|
|
931
|
+
ok: outcome.ok,
|
|
932
|
+
errorCode: normalizedErrorCode,
|
|
933
|
+
errorCategory: normalizedErrorCategory,
|
|
934
|
+
});
|
|
935
|
+
const resolutionStage = outcome.resolution_stage ?? "execute";
|
|
732
936
|
const event = {
|
|
733
937
|
schema_version: TELEMETRY_SCHEMA_VERSION,
|
|
734
938
|
event_id: crypto.randomUUID(),
|
|
@@ -747,11 +951,92 @@ export async function finishTelemetryCommand(activeCommand, outcome) {
|
|
|
747
951
|
outcome,
|
|
748
952
|
durationMs,
|
|
749
953
|
startedAt: activeCommand.started_at,
|
|
954
|
+
command: activeCommand.command,
|
|
955
|
+
installationId: activeCommand.installation_id,
|
|
956
|
+
commandTaxonomy: activeCommand.command_taxonomy,
|
|
957
|
+
exitCode: normalizedExitCode,
|
|
958
|
+
errorCode: normalizedErrorCode,
|
|
959
|
+
errorCategory: normalizedErrorCategory,
|
|
960
|
+
commandResolution,
|
|
961
|
+
resolutionStage,
|
|
750
962
|
}),
|
|
751
963
|
};
|
|
752
964
|
await enqueueTelemetryEvent(activeCommand.global_pm_root, event);
|
|
753
965
|
_lastFlushPromise = flushQueue(activeCommand.global_pm_root, activeCommand.endpoint, activeCommand.retention_days).catch(() => { });
|
|
754
|
-
void exportLocalOtelSpan(activeCommand,
|
|
966
|
+
void exportLocalOtelSpan(activeCommand, {
|
|
967
|
+
...outcome,
|
|
968
|
+
exit_code: normalizedExitCode,
|
|
969
|
+
error_code: normalizedErrorCode,
|
|
970
|
+
error_category: normalizedErrorCategory,
|
|
971
|
+
}, finishedAt, durationMs).catch(() => { });
|
|
972
|
+
}
|
|
973
|
+
catch {
|
|
974
|
+
// Telemetry must never block command execution.
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
export async function emitTelemetryErrorEvent(context) {
|
|
978
|
+
if (telemetryDisabledByEnvironment()) {
|
|
979
|
+
return;
|
|
980
|
+
}
|
|
981
|
+
try {
|
|
982
|
+
const globalPmRoot = resolveGlobalPmRoot(process.cwd());
|
|
983
|
+
const settings = await readSettings(globalPmRoot);
|
|
984
|
+
if (!settings.telemetry.enabled) {
|
|
985
|
+
return;
|
|
986
|
+
}
|
|
987
|
+
const captureLevel = normalizeCaptureLevel(settings.telemetry.capture_level);
|
|
988
|
+
const { installationId, endpoint, retentionDays } = await ensureInstallationId(globalPmRoot);
|
|
989
|
+
const pmVersion = normalizePmVersion(context.pm_version);
|
|
990
|
+
const sourceContext = resolveTelemetrySourceContext(context.global);
|
|
991
|
+
const pmRootHash = hashWithInstallationId(installationId, context.pm_root);
|
|
992
|
+
const cwdHash = hashWithInstallationId(installationId, process.cwd());
|
|
993
|
+
const occurredAt = nowIso();
|
|
994
|
+
const normalizedErrorCode = normalizeTelemetryErrorCode(inferTelemetryErrorCode({
|
|
995
|
+
ok: false,
|
|
996
|
+
errorCode: context.error_code,
|
|
997
|
+
errorMessage: context.error_message,
|
|
998
|
+
exitCode: context.exit_code,
|
|
999
|
+
})) ?? "unknown_error";
|
|
1000
|
+
const normalizedErrorCategory = context.error_category ?? resolveTelemetryErrorCategory(normalizedErrorCode);
|
|
1001
|
+
const normalizedExitCode = normalizeTelemetryExitCode(context.exit_code, false);
|
|
1002
|
+
const normalizedCommand = context.command.trim().length > 0 ? context.command : "<unknown>";
|
|
1003
|
+
const commandTaxonomy = deriveTelemetryCommandTaxonomy(normalizedCommand);
|
|
1004
|
+
const commandResolution = context.command_resolution ??
|
|
1005
|
+
deriveTelemetryCommandResolution({
|
|
1006
|
+
ok: false,
|
|
1007
|
+
errorCode: normalizedErrorCode,
|
|
1008
|
+
errorCategory: normalizedErrorCategory,
|
|
1009
|
+
});
|
|
1010
|
+
const resolutionStage = context.resolution_stage ?? "unknown";
|
|
1011
|
+
const event = {
|
|
1012
|
+
schema_version: TELEMETRY_SCHEMA_VERSION,
|
|
1013
|
+
event_id: crypto.randomUUID(),
|
|
1014
|
+
event_type: "command_error",
|
|
1015
|
+
occurred_at: occurredAt,
|
|
1016
|
+
installation_id: installationId,
|
|
1017
|
+
session_id: PROCESS_SESSION_ID,
|
|
1018
|
+
command: sanitizeString(normalizedCommand, "redacted"),
|
|
1019
|
+
payload: buildCommandErrorPayload({
|
|
1020
|
+
captureLevel,
|
|
1021
|
+
pmVersion,
|
|
1022
|
+
sourceContext,
|
|
1023
|
+
command: normalizedCommand,
|
|
1024
|
+
commandTaxonomy,
|
|
1025
|
+
commandResolution,
|
|
1026
|
+
resolutionStage,
|
|
1027
|
+
args: context.args,
|
|
1028
|
+
options: context.options,
|
|
1029
|
+
pmRootHash,
|
|
1030
|
+
cwdHash,
|
|
1031
|
+
installationId,
|
|
1032
|
+
errorCode: normalizedErrorCode,
|
|
1033
|
+
errorCategory: normalizedErrorCategory,
|
|
1034
|
+
exitCode: normalizedExitCode,
|
|
1035
|
+
errorMessage: context.error_message,
|
|
1036
|
+
}),
|
|
1037
|
+
};
|
|
1038
|
+
await enqueueTelemetryEvent(globalPmRoot, event);
|
|
1039
|
+
_lastFlushPromise = flushQueue(globalPmRoot, endpoint, retentionDays).catch(() => { });
|
|
755
1040
|
}
|
|
756
1041
|
catch {
|
|
757
1042
|
// Telemetry must never block command execution.
|