@unbrained/pm-cli 2026.5.3 → 2026.5.4
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 +48 -0
- package/README.md +9 -1
- package/dist/cli/main.js +269 -15
- package/dist/cli/main.js.map +1 -1
- package/dist/core/sentry/helpers.d.ts +15 -2
- package/dist/core/sentry/helpers.js +73 -3
- package/dist/core/sentry/helpers.js.map +1 -1
- package/dist/core/shared/constants.js +3 -0
- package/dist/core/shared/constants.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 +6 -0
- package/dist/core/telemetry/runtime.js +132 -7
- package/dist/core/telemetry/runtime.js.map +1 -1
- package/docs/CONFIGURATION.md +0 -2
- package/docs/RELEASING.md +43 -39
- package/package.json +6 -1
|
@@ -6,6 +6,7 @@ import { resolveTelemetryErrorCategory } from "../shared/constants.js";
|
|
|
6
6
|
import { nowIso } from "../shared/time.js";
|
|
7
7
|
import { resolveGlobalPmRoot } from "../store/paths.js";
|
|
8
8
|
import { readSettings, writeSettings } from "../store/settings.js";
|
|
9
|
+
import { deriveTelemetryCommandResolution, deriveTelemetryCommandTaxonomy, inferTelemetryErrorCode, } from "./observability.js";
|
|
9
10
|
const TELEMETRY_QUEUE_RELATIVE_PATH = path.join("runtime", "telemetry", "events.jsonl");
|
|
10
11
|
const TELEMETRY_STATE_RELATIVE_PATH = path.join("runtime", "telemetry", "state.json");
|
|
11
12
|
const TELEMETRY_SCHEMA_VERSION = 1;
|
|
@@ -274,6 +275,44 @@ function resolveTelemetrySourceContext(globalOptions) {
|
|
|
274
275
|
function hashWithInstallationId(installationId, value) {
|
|
275
276
|
return crypto.createHash("sha256").update(`${installationId}:${value}`).digest("hex");
|
|
276
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
|
+
}
|
|
277
316
|
function telemetryDisabledByEnvironment() {
|
|
278
317
|
return PM_TELEMETRY_DISABLED_VALUES.has((process.env[PM_TELEMETRY_DISABLED_ENV] ?? "").trim().toLowerCase());
|
|
279
318
|
}
|
|
@@ -456,21 +495,37 @@ function summarizeResult(result, captureLevel = "redacted") {
|
|
|
456
495
|
}
|
|
457
496
|
function buildCommandStartPayload(params) {
|
|
458
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);
|
|
459
503
|
if (captureLevel === "minimal") {
|
|
460
504
|
return {
|
|
461
505
|
capture_level: captureLevel,
|
|
462
506
|
pm_version: pmVersion,
|
|
463
507
|
source_context: sourceContext.source_context,
|
|
464
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,
|
|
465
514
|
};
|
|
466
515
|
}
|
|
467
516
|
return {
|
|
468
517
|
pm_version: pmVersion,
|
|
469
518
|
source_context: sourceContext.source_context,
|
|
470
519
|
source_context_source: sourceContext.source_context_source,
|
|
520
|
+
command_taxonomy: commandTaxonomy,
|
|
471
521
|
command_args: sanitizeCommandArgs(context.args, captureLevel),
|
|
522
|
+
command_args_hashes: hashedArgs.hashes,
|
|
523
|
+
command_args_digest: hashedArgs.digest,
|
|
524
|
+
command_invocation_digest: commandInvocationDigest,
|
|
472
525
|
command_options: sanitizeValue(context.options, undefined, captureLevel),
|
|
526
|
+
command_options_digest: commandOptionsDigest,
|
|
473
527
|
global_options: sanitizeValue(context.global, undefined, captureLevel),
|
|
528
|
+
global_options_digest: globalOptionsDigest,
|
|
474
529
|
pm_root_hash: pmRootHash,
|
|
475
530
|
cwd_hash: cwdHash,
|
|
476
531
|
capture_level: captureLevel,
|
|
@@ -485,18 +540,25 @@ function buildCommandStartPayload(params) {
|
|
|
485
540
|
};
|
|
486
541
|
}
|
|
487
542
|
function buildCommandFinishPayload(params) {
|
|
488
|
-
const { captureLevel, pmVersion, sourceContext, outcome, durationMs, startedAt, exitCode, errorCode, errorCategory } = 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;
|
|
489
547
|
if (captureLevel === "minimal") {
|
|
490
548
|
return {
|
|
491
549
|
capture_level: captureLevel,
|
|
492
550
|
pm_version: pmVersion,
|
|
493
551
|
source_context: sourceContext.source_context,
|
|
494
552
|
source_context_source: sourceContext.source_context_source,
|
|
553
|
+
command_taxonomy: commandTaxonomy,
|
|
554
|
+
command_resolution: commandResolution,
|
|
555
|
+
resolution_stage: resolutionStage,
|
|
495
556
|
ok: outcome.ok,
|
|
496
557
|
exit_code: exitCode,
|
|
497
558
|
error_code: errorCode,
|
|
498
559
|
error_category: errorCategory,
|
|
499
560
|
error: outcome.error ? sanitizeString(outcome.error, "redacted") : undefined,
|
|
561
|
+
error_fingerprint: errorFingerprint,
|
|
500
562
|
duration_ms: durationMs,
|
|
501
563
|
};
|
|
502
564
|
}
|
|
@@ -505,28 +567,43 @@ function buildCommandFinishPayload(params) {
|
|
|
505
567
|
pm_version: pmVersion,
|
|
506
568
|
source_context: sourceContext.source_context,
|
|
507
569
|
source_context_source: sourceContext.source_context_source,
|
|
570
|
+
command_taxonomy: commandTaxonomy,
|
|
571
|
+
command_resolution: commandResolution,
|
|
572
|
+
resolution_stage: resolutionStage,
|
|
508
573
|
ok: outcome.ok,
|
|
509
574
|
exit_code: exitCode,
|
|
510
575
|
error_code: errorCode,
|
|
511
576
|
error_category: errorCategory,
|
|
512
577
|
error: outcome.error ? sanitizeString(outcome.error, captureLevel) : undefined,
|
|
578
|
+
error_fingerprint: errorFingerprint,
|
|
513
579
|
duration_ms: durationMs,
|
|
514
580
|
started_at: startedAt,
|
|
515
581
|
result_summary: summarizeResult(outcome.result, captureLevel),
|
|
516
582
|
};
|
|
517
583
|
}
|
|
518
584
|
function buildCommandErrorPayload(params) {
|
|
519
|
-
const { captureLevel, pmVersion, sourceContext, command, args, options, pmRootHash, cwdHash, installationId, errorCode, errorMessage, errorCategory, exitCode, } = 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);
|
|
520
590
|
if (captureLevel === "minimal") {
|
|
521
591
|
return {
|
|
522
592
|
capture_level: captureLevel,
|
|
523
593
|
pm_version: pmVersion,
|
|
524
594
|
source_context: sourceContext.source_context,
|
|
525
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,
|
|
526
602
|
error_code: errorCode,
|
|
527
603
|
error_category: errorCategory,
|
|
528
604
|
exit_code: exitCode,
|
|
529
605
|
error: sanitizeString(errorMessage, "redacted"),
|
|
606
|
+
error_fingerprint: errorFingerprint,
|
|
530
607
|
};
|
|
531
608
|
}
|
|
532
609
|
return {
|
|
@@ -534,13 +611,21 @@ function buildCommandErrorPayload(params) {
|
|
|
534
611
|
pm_version: pmVersion,
|
|
535
612
|
source_context: sourceContext.source_context,
|
|
536
613
|
source_context_source: sourceContext.source_context_source,
|
|
614
|
+
command_taxonomy: commandTaxonomy,
|
|
615
|
+
command_resolution: commandResolution,
|
|
616
|
+
resolution_stage: resolutionStage,
|
|
537
617
|
attempted_command: sanitizeString(command, captureLevel),
|
|
618
|
+
attempted_command_digest: attemptedCommandDigest,
|
|
538
619
|
attempted_args: sanitizeCommandArgs(args, captureLevel),
|
|
620
|
+
attempted_args_hashes: attemptedArgHashes.hashes,
|
|
621
|
+
attempted_args_digest: attemptedArgHashes.digest,
|
|
539
622
|
attempted_options: sanitizeValue(options, undefined, captureLevel),
|
|
623
|
+
attempted_options_digest: attemptedOptionsDigest,
|
|
540
624
|
error_code: errorCode,
|
|
541
625
|
error_category: errorCategory,
|
|
542
626
|
exit_code: exitCode,
|
|
543
627
|
error: sanitizeString(errorMessage, captureLevel),
|
|
628
|
+
error_fingerprint: errorFingerprint,
|
|
544
629
|
pm_root_hash: pmRootHash,
|
|
545
630
|
cwd_hash: cwdHash,
|
|
546
631
|
runtime: {
|
|
@@ -698,12 +783,17 @@ async function flushQueue(globalPmRoot, endpoint, retentionDays) {
|
|
|
698
783
|
}
|
|
699
784
|
const dueIds = new Set(dueEntries.map((entry) => entry.event.event_id));
|
|
700
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
|
+
}
|
|
701
793
|
try {
|
|
702
794
|
const response = await fetch(normalizedEndpoint, {
|
|
703
795
|
method: "POST",
|
|
704
|
-
headers:
|
|
705
|
-
"content-type": "application/json",
|
|
706
|
-
},
|
|
796
|
+
headers: requestHeaders,
|
|
707
797
|
body: JSON.stringify({
|
|
708
798
|
schema_version: TELEMETRY_SCHEMA_VERSION,
|
|
709
799
|
events: dueEntries.map((entry) => entry.event),
|
|
@@ -791,10 +881,12 @@ export async function startTelemetryCommand(context) {
|
|
|
791
881
|
};
|
|
792
882
|
await enqueueTelemetryEvent(globalPmRoot, event);
|
|
793
883
|
_lastFlushPromise = flushQueue(globalPmRoot, endpoint, retentionDays).catch(() => { });
|
|
884
|
+
const commandTaxonomy = deriveTelemetryCommandTaxonomy(context.command);
|
|
794
885
|
return {
|
|
795
886
|
started_at: occurredAt,
|
|
796
887
|
started_at_ms: Date.now(),
|
|
797
888
|
command: context.command,
|
|
889
|
+
command_taxonomy: commandTaxonomy,
|
|
798
890
|
pm_version: pmVersion,
|
|
799
891
|
source_context: sourceContext.source_context,
|
|
800
892
|
source_context_source: sourceContext.source_context_source,
|
|
@@ -822,13 +914,25 @@ export async function finishTelemetryCommand(activeCommand, outcome) {
|
|
|
822
914
|
try {
|
|
823
915
|
const finishedAt = nowIso();
|
|
824
916
|
const durationMs = Math.max(0, Date.now() - activeCommand.started_at_ms);
|
|
825
|
-
const normalizedErrorCode = normalizeTelemetryErrorCode(
|
|
917
|
+
const normalizedErrorCode = normalizeTelemetryErrorCode(inferTelemetryErrorCode({
|
|
918
|
+
ok: outcome.ok,
|
|
919
|
+
errorCode: outcome.error_code,
|
|
920
|
+
errorMessage: outcome.error,
|
|
921
|
+
exitCode: outcome.exit_code,
|
|
922
|
+
}));
|
|
826
923
|
const normalizedErrorCategory = normalizeTelemetryErrorCategory({
|
|
827
924
|
ok: outcome.ok,
|
|
828
925
|
errorCode: normalizedErrorCode,
|
|
829
926
|
errorCategory: outcome.error_category,
|
|
830
927
|
});
|
|
831
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";
|
|
832
936
|
const event = {
|
|
833
937
|
schema_version: TELEMETRY_SCHEMA_VERSION,
|
|
834
938
|
event_id: crypto.randomUUID(),
|
|
@@ -847,9 +951,14 @@ export async function finishTelemetryCommand(activeCommand, outcome) {
|
|
|
847
951
|
outcome,
|
|
848
952
|
durationMs,
|
|
849
953
|
startedAt: activeCommand.started_at,
|
|
954
|
+
command: activeCommand.command,
|
|
955
|
+
installationId: activeCommand.installation_id,
|
|
956
|
+
commandTaxonomy: activeCommand.command_taxonomy,
|
|
850
957
|
exitCode: normalizedExitCode,
|
|
851
958
|
errorCode: normalizedErrorCode,
|
|
852
959
|
errorCategory: normalizedErrorCategory,
|
|
960
|
+
commandResolution,
|
|
961
|
+
resolutionStage,
|
|
853
962
|
}),
|
|
854
963
|
};
|
|
855
964
|
await enqueueTelemetryEvent(activeCommand.global_pm_root, event);
|
|
@@ -882,10 +991,23 @@ export async function emitTelemetryErrorEvent(context) {
|
|
|
882
991
|
const pmRootHash = hashWithInstallationId(installationId, context.pm_root);
|
|
883
992
|
const cwdHash = hashWithInstallationId(installationId, process.cwd());
|
|
884
993
|
const occurredAt = nowIso();
|
|
885
|
-
const normalizedErrorCode = normalizeTelemetryErrorCode(
|
|
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";
|
|
886
1000
|
const normalizedErrorCategory = context.error_category ?? resolveTelemetryErrorCategory(normalizedErrorCode);
|
|
887
1001
|
const normalizedExitCode = normalizeTelemetryExitCode(context.exit_code, false);
|
|
888
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";
|
|
889
1011
|
const event = {
|
|
890
1012
|
schema_version: TELEMETRY_SCHEMA_VERSION,
|
|
891
1013
|
event_id: crypto.randomUUID(),
|
|
@@ -899,6 +1021,9 @@ export async function emitTelemetryErrorEvent(context) {
|
|
|
899
1021
|
pmVersion,
|
|
900
1022
|
sourceContext,
|
|
901
1023
|
command: normalizedCommand,
|
|
1024
|
+
commandTaxonomy,
|
|
1025
|
+
commandResolution,
|
|
1026
|
+
resolutionStage,
|
|
902
1027
|
args: context.args,
|
|
903
1028
|
options: context.options,
|
|
904
1029
|
pmRootHash,
|