@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.
@@ -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(outcome.error_code);
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(context.error_code) ?? "unknown_error";
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,