@unbrained/pm-cli 2026.5.1 → 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.
Files changed (265) hide show
  1. package/AGENTS.md +8 -1
  2. package/CHANGELOG.md +73 -4
  3. package/CONTRIBUTING.md +11 -5
  4. package/PRD.md +17 -1
  5. package/README.md +55 -1099
  6. package/SECURITY.md +6 -11
  7. package/dist/cli/bootstrap-args.d.ts +18 -0
  8. package/dist/cli/bootstrap-args.js +242 -0
  9. package/dist/cli/bootstrap-args.js.map +1 -0
  10. package/dist/cli/commander-usage.d.ts +17 -0
  11. package/dist/cli/commander-usage.js +178 -0
  12. package/dist/cli/commander-usage.js.map +1 -0
  13. package/dist/cli/commands/activity.d.ts +10 -0
  14. package/dist/cli/commands/activity.js +14 -10
  15. package/dist/cli/commands/activity.js.map +1 -1
  16. package/dist/cli/commands/aggregate.js.map +1 -1
  17. package/dist/cli/commands/append.js.map +1 -1
  18. package/dist/cli/commands/calendar.js +19 -34
  19. package/dist/cli/commands/calendar.js.map +1 -1
  20. package/dist/cli/commands/claim.js.map +1 -1
  21. package/dist/cli/commands/close.js.map +1 -1
  22. package/dist/cli/commands/comments-audit.js.map +1 -1
  23. package/dist/cli/commands/comments.js +1 -9
  24. package/dist/cli/commands/comments.js.map +1 -1
  25. package/dist/cli/commands/completion.js.map +1 -1
  26. package/dist/cli/commands/config.d.ts +21 -3
  27. package/dist/cli/commands/config.js +118 -2
  28. package/dist/cli/commands/config.js.map +1 -1
  29. package/dist/cli/commands/context.d.ts +90 -1
  30. package/dist/cli/commands/context.js +496 -12
  31. package/dist/cli/commands/context.js.map +1 -1
  32. package/dist/cli/commands/contracts.js.map +1 -1
  33. package/dist/cli/commands/create.js +2 -2
  34. package/dist/cli/commands/create.js.map +1 -1
  35. package/dist/cli/commands/dedupe-audit.js +2 -11
  36. package/dist/cli/commands/dedupe-audit.js.map +1 -1
  37. package/dist/cli/commands/delete.js.map +1 -1
  38. package/dist/cli/commands/deps.js.map +1 -1
  39. package/dist/cli/commands/docs.js.map +1 -1
  40. package/dist/cli/commands/extension.js.map +1 -1
  41. package/dist/cli/commands/files.js +14 -2
  42. package/dist/cli/commands/files.js.map +1 -1
  43. package/dist/cli/commands/gc.js.map +1 -1
  44. package/dist/cli/commands/get.js.map +1 -1
  45. package/dist/cli/commands/health.js +16 -12
  46. package/dist/cli/commands/health.js.map +1 -1
  47. package/dist/cli/commands/history.js +1 -9
  48. package/dist/cli/commands/history.js.map +1 -1
  49. package/dist/cli/commands/index.js.map +1 -1
  50. package/dist/cli/commands/init.js.map +1 -1
  51. package/dist/cli/commands/learnings.js +1 -9
  52. package/dist/cli/commands/learnings.js.map +1 -1
  53. package/dist/cli/commands/list.d.ts +1 -0
  54. package/dist/cli/commands/list.js +13 -31
  55. package/dist/cli/commands/list.js.map +1 -1
  56. package/dist/cli/commands/normalize.js +14 -23
  57. package/dist/cli/commands/normalize.js.map +1 -1
  58. package/dist/cli/commands/notes.js +1 -9
  59. package/dist/cli/commands/notes.js.map +1 -1
  60. package/dist/cli/commands/reindex.js +2 -7
  61. package/dist/cli/commands/reindex.js.map +1 -1
  62. package/dist/cli/commands/restore.js.map +1 -1
  63. package/dist/cli/commands/search.js +4 -35
  64. package/dist/cli/commands/search.js.map +1 -1
  65. package/dist/cli/commands/stats.js.map +1 -1
  66. package/dist/cli/commands/templates.js.map +1 -1
  67. package/dist/cli/commands/test-all.js.map +1 -1
  68. package/dist/cli/commands/test-runs.js +1 -11
  69. package/dist/cli/commands/test-runs.js.map +1 -1
  70. package/dist/cli/commands/test.js.map +1 -1
  71. package/dist/cli/commands/update-many.js +1 -6
  72. package/dist/cli/commands/update-many.js.map +1 -1
  73. package/dist/cli/commands/update.js +2 -2
  74. package/dist/cli/commands/update.js.map +1 -1
  75. package/dist/cli/commands/validate.js +23 -18
  76. package/dist/cli/commands/validate.js.map +1 -1
  77. package/dist/cli/error-guidance.d.ts +13 -0
  78. package/dist/cli/error-guidance.js +56 -6
  79. package/dist/cli/error-guidance.js.map +1 -1
  80. package/dist/cli/extension-command-help.d.ts +48 -0
  81. package/dist/cli/extension-command-help.js +389 -0
  82. package/dist/cli/extension-command-help.js.map +1 -0
  83. package/dist/cli/extension-command-options.js.map +1 -1
  84. package/dist/cli/help-content.js +9 -3
  85. package/dist/cli/help-content.js.map +1 -1
  86. package/dist/cli/help-json-payload.d.ts +25 -0
  87. package/dist/cli/help-json-payload.js +265 -0
  88. package/dist/cli/help-json-payload.js.map +1 -0
  89. package/dist/cli/main.js +1000 -4456
  90. package/dist/cli/main.js.map +1 -1
  91. package/dist/cli/migration-gates.d.ts +22 -0
  92. package/dist/cli/migration-gates.js +146 -0
  93. package/dist/cli/migration-gates.js.map +1 -0
  94. package/dist/cli/register-list-query.d.ts +2 -0
  95. package/dist/cli/register-list-query.js +317 -0
  96. package/dist/cli/register-list-query.js.map +1 -0
  97. package/dist/cli/register-mutation.d.ts +2 -0
  98. package/dist/cli/register-mutation.js +795 -0
  99. package/dist/cli/register-mutation.js.map +1 -0
  100. package/dist/cli/register-operations.d.ts +2 -0
  101. package/dist/cli/register-operations.js +610 -0
  102. package/dist/cli/register-operations.js.map +1 -0
  103. package/dist/cli/register-setup.d.ts +2 -0
  104. package/dist/cli/register-setup.js +334 -0
  105. package/dist/cli/register-setup.js.map +1 -0
  106. package/dist/cli/registration-helpers.d.ts +53 -0
  107. package/dist/cli/registration-helpers.js +669 -0
  108. package/dist/cli/registration-helpers.js.map +1 -0
  109. package/dist/cli/shared-parsers.d.ts +6 -0
  110. package/dist/cli/shared-parsers.js +40 -0
  111. package/dist/cli/shared-parsers.js.map +1 -0
  112. package/dist/cli.d.ts +1 -1
  113. package/dist/cli.js +3 -1
  114. package/dist/cli.js.map +1 -1
  115. package/dist/core/extensions/extension-types.d.ts +605 -0
  116. package/dist/core/extensions/extension-types.js +22 -0
  117. package/dist/core/extensions/extension-types.js.map +1 -0
  118. package/dist/core/extensions/index.js.map +1 -1
  119. package/dist/core/extensions/item-fields.js.map +1 -1
  120. package/dist/core/extensions/loader.d.ts +2 -586
  121. package/dist/core/extensions/loader.js +3 -21
  122. package/dist/core/extensions/loader.js.map +1 -1
  123. package/dist/core/extensions/runtime-registrations.js.map +1 -1
  124. package/dist/core/fs/fs-utils.js.map +1 -1
  125. package/dist/core/fs/index.js.map +1 -1
  126. package/dist/core/history/history-stream-policy.js.map +1 -1
  127. package/dist/core/history/history.js.map +1 -1
  128. package/dist/core/history/index.js.map +1 -1
  129. package/dist/core/item/id.js.map +1 -1
  130. package/dist/core/item/index.js.map +1 -1
  131. package/dist/core/item/item-format.js.map +1 -1
  132. package/dist/core/item/parent-reference-policy.js.map +1 -1
  133. package/dist/core/item/parse.js +6 -0
  134. package/dist/core/item/parse.js.map +1 -1
  135. package/dist/core/item/sprint-release-format.js.map +1 -1
  136. package/dist/core/item/status.js.map +1 -1
  137. package/dist/core/item/type-registry.js.map +1 -1
  138. package/dist/core/lock/index.js.map +1 -1
  139. package/dist/core/lock/lock.js +1 -6
  140. package/dist/core/lock/lock.js.map +1 -1
  141. package/dist/core/output/command-aware.js.map +1 -1
  142. package/dist/core/output/output.js.map +1 -1
  143. package/dist/core/schema/runtime-field-filters.js.map +1 -1
  144. package/dist/core/schema/runtime-field-values.js.map +1 -1
  145. package/dist/core/schema/runtime-schema.js.map +1 -1
  146. package/dist/core/search/cache.js +1 -7
  147. package/dist/core/search/cache.js.map +1 -1
  148. package/dist/core/search/embedding-batches.js +4 -0
  149. package/dist/core/search/embedding-batches.js.map +1 -1
  150. package/dist/core/search/http-client.d.ts +29 -0
  151. package/dist/core/search/http-client.js +64 -0
  152. package/dist/core/search/http-client.js.map +1 -0
  153. package/dist/core/search/providers.d.ts +3 -13
  154. package/dist/core/search/providers.js +19 -88
  155. package/dist/core/search/providers.js.map +1 -1
  156. package/dist/core/search/semantic-defaults.js +2 -7
  157. package/dist/core/search/semantic-defaults.js.map +1 -1
  158. package/dist/core/search/vector-stores.d.ts +4 -13
  159. package/dist/core/search/vector-stores.js +40 -93
  160. package/dist/core/search/vector-stores.js.map +1 -1
  161. package/dist/core/sentry/helpers.d.ts +27 -0
  162. package/dist/core/sentry/helpers.js +171 -0
  163. package/dist/core/sentry/helpers.js.map +1 -0
  164. package/dist/core/sentry/instrument.d.ts +25 -0
  165. package/dist/core/sentry/instrument.js +204 -0
  166. package/dist/core/sentry/instrument.js.map +1 -0
  167. package/dist/core/shared/command-types.js.map +1 -1
  168. package/dist/core/shared/conflict-markers.js.map +1 -1
  169. package/dist/core/shared/constants.d.ts +3 -0
  170. package/dist/core/shared/constants.js +58 -1
  171. package/dist/core/shared/constants.js.map +1 -1
  172. package/dist/core/shared/errors.js.map +1 -1
  173. package/dist/core/shared/index.d.ts +1 -0
  174. package/dist/core/shared/index.js +1 -0
  175. package/dist/core/shared/index.js.map +1 -1
  176. package/dist/core/shared/primitives.d.ts +13 -0
  177. package/dist/core/shared/primitives.js +33 -0
  178. package/dist/core/shared/primitives.js.map +1 -0
  179. package/dist/core/shared/serialization.js.map +1 -1
  180. package/dist/core/shared/text-normalization.js.map +1 -1
  181. package/dist/core/shared/time.js.map +1 -1
  182. package/dist/core/store/front-matter-cache.d.ts +6 -0
  183. package/dist/core/store/front-matter-cache.js +150 -0
  184. package/dist/core/store/front-matter-cache.js.map +1 -0
  185. package/dist/core/store/index.js.map +1 -1
  186. package/dist/core/store/item-format-migration.js.map +1 -1
  187. package/dist/core/store/item-store.js +46 -36
  188. package/dist/core/store/item-store.js.map +1 -1
  189. package/dist/core/store/paths.js.map +1 -1
  190. package/dist/core/store/settings.js +36 -0
  191. package/dist/core/store/settings.js.map +1 -1
  192. package/dist/core/telemetry/consent.js.map +1 -1
  193. package/dist/core/telemetry/observability.d.ts +24 -0
  194. package/dist/core/telemetry/observability.js +185 -0
  195. package/dist/core/telemetry/observability.js.map +1 -0
  196. package/dist/core/telemetry/runtime.d.ts +29 -3
  197. package/dist/core/telemetry/runtime.js +337 -25
  198. package/dist/core/telemetry/runtime.js.map +1 -1
  199. package/dist/core/test/background-runs.js.map +1 -1
  200. package/dist/core/test/item-test-run-tracking.js.map +1 -1
  201. package/dist/sdk/cli-contracts.js +28 -0
  202. package/dist/sdk/cli-contracts.js.map +1 -1
  203. package/dist/sdk/index.d.ts +1 -1
  204. package/dist/sdk/index.js.map +1 -1
  205. package/dist/types/index.js.map +1 -1
  206. package/dist/types.d.ts +21 -0
  207. package/dist/types.js +11 -0
  208. package/dist/types.js.map +1 -1
  209. package/docs/AGENT_GUIDE.md +125 -0
  210. package/docs/ARCHITECTURE.md +201 -478
  211. package/docs/COMMANDS.md +209 -0
  212. package/docs/CONFIGURATION.md +146 -0
  213. package/docs/EXTENSIONS.md +146 -645
  214. package/docs/QUICKSTART.md +108 -0
  215. package/docs/README.md +70 -0
  216. package/docs/RELEASING.md +92 -50
  217. package/docs/SDK.md +127 -68
  218. package/docs/TESTING.md +125 -0
  219. package/docs/examples/starter-extension/README.md +39 -25
  220. package/package.json +24 -11
  221. package/dist/command-types.d.ts +0 -1
  222. package/dist/command-types.js +0 -2
  223. package/dist/command-types.js.map +0 -1
  224. package/dist/constants.d.ts +0 -1
  225. package/dist/constants.js +0 -2
  226. package/dist/constants.js.map +0 -1
  227. package/dist/errors.d.ts +0 -1
  228. package/dist/errors.js +0 -2
  229. package/dist/errors.js.map +0 -1
  230. package/dist/fs-utils.d.ts +0 -1
  231. package/dist/fs-utils.js +0 -2
  232. package/dist/fs-utils.js.map +0 -1
  233. package/dist/history.d.ts +0 -1
  234. package/dist/history.js +0 -2
  235. package/dist/history.js.map +0 -1
  236. package/dist/id.d.ts +0 -1
  237. package/dist/id.js +0 -2
  238. package/dist/id.js.map +0 -1
  239. package/dist/item-format.d.ts +0 -1
  240. package/dist/item-format.js +0 -2
  241. package/dist/item-format.js.map +0 -1
  242. package/dist/item-store.d.ts +0 -1
  243. package/dist/item-store.js +0 -2
  244. package/dist/item-store.js.map +0 -1
  245. package/dist/lock.d.ts +0 -1
  246. package/dist/lock.js +0 -2
  247. package/dist/lock.js.map +0 -1
  248. package/dist/output.d.ts +0 -1
  249. package/dist/output.js +0 -2
  250. package/dist/output.js.map +0 -1
  251. package/dist/parse.d.ts +0 -1
  252. package/dist/parse.js +0 -2
  253. package/dist/parse.js.map +0 -1
  254. package/dist/paths.d.ts +0 -1
  255. package/dist/paths.js +0 -2
  256. package/dist/paths.js.map +0 -1
  257. package/dist/serialization.d.ts +0 -1
  258. package/dist/serialization.js +0 -2
  259. package/dist/serialization.js.map +0 -1
  260. package/dist/settings.d.ts +0 -1
  261. package/dist/settings.js +0 -2
  262. package/dist/settings.js.map +0 -1
  263. package/dist/time.d.ts +0 -1
  264. package/dist/time.js +0 -2
  265. package/dist/time.js.map +0 -1
@@ -2,16 +2,24 @@ 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";
6
+ import { nowIso } from "../shared/time.js";
5
7
  import { resolveGlobalPmRoot } from "../store/paths.js";
6
8
  import { readSettings, writeSettings } from "../store/settings.js";
9
+ import { deriveTelemetryCommandResolution, deriveTelemetryCommandTaxonomy, inferTelemetryErrorCode, } from "./observability.js";
7
10
  const TELEMETRY_QUEUE_RELATIVE_PATH = path.join("runtime", "telemetry", "events.jsonl");
8
11
  const TELEMETRY_STATE_RELATIVE_PATH = path.join("runtime", "telemetry", "state.json");
9
12
  const TELEMETRY_SCHEMA_VERSION = 1;
10
13
  const TELEMETRY_FLUSH_BATCH_SIZE = 100;
11
14
  const TELEMETRY_MAX_RETRY_DELAY_MS = 3_600_000;
12
15
  const TELEMETRY_RETRY_BASE_DELAY_MS = 30_000;
13
- const TELEMETRY_HTTP_TIMEOUT_MS = 2_500;
16
+ const TELEMETRY_HTTP_TIMEOUT_MS = 5_000;
14
17
  const MILLISECONDS_PER_DAY = 86_400_000;
18
+ const TELEMETRY_MAX_EVENT_BYTES = 65_536;
19
+ const TELEMETRY_SANITIZE_MAX_DEPTH = 6;
20
+ const TELEMETRY_SANITIZE_MAX_ARRAY_ITEMS = 20;
21
+ const TELEMETRY_MAX_QUEUE_ENTRY_ATTEMPTS = 15;
22
+ const TELEMETRY_RESULT_PREVIEW_MAX_BYTES = 8_192;
15
23
  const OTEL_TRACES_ENDPOINT_ENV = "OTEL_EXPORTER_OTLP_TRACES_ENDPOINT";
16
24
  const OTEL_BASE_ENDPOINT_ENV = "OTEL_EXPORTER_OTLP_ENDPOINT";
17
25
  const OTEL_SERVICE_NAME_ENV = "OTEL_SERVICE_NAME";
@@ -21,6 +29,11 @@ const PM_TELEMETRY_OTEL_DISABLED_ENV = "PM_TELEMETRY_OTEL_DISABLED";
21
29
  const PM_TELEMETRY_OTEL_DISABLED_VALUES = new Set(["1", "true", "yes", "on"]);
22
30
  const PM_TELEMETRY_SOURCE_CONTEXT_ENV = "PM_TELEMETRY_SOURCE_CONTEXT";
23
31
  const PM_TELEMETRY_SOURCE_CONTEXT_VALUES = ["user", "automation", "test", "dogfood", "audit_smoke"];
32
+ let _lastFlushPromise = Promise.resolve();
33
+ /** Wait for the most recent background flush to settle. Test-only helper. */
34
+ export function waitForPendingFlush() {
35
+ return _lastFlushPromise;
36
+ }
24
37
  const PM_TELEMETRY_SOURCE_CONTEXT_SET = new Set(PM_TELEMETRY_SOURCE_CONTEXT_VALUES);
25
38
  const BOOLEAN_TRUE_VALUES = new Set(["1", "true", "yes", "on"]);
26
39
  const PROCESS_SESSION_ID = crypto.randomUUID();
@@ -41,9 +54,9 @@ const SENSITIVE_INLINE_KEY_PATTERN = "(?:token|secret|password|passwd|api[_-]?ke
41
54
  const INLINE_SENSITIVE_ASSIGNMENT_PATTERN = new RegExp(`\\b(${SENSITIVE_INLINE_KEY_PATTERN})\\s*([:=])\\s*([^\\s,;]+)`, "giu");
42
55
  const INLINE_SENSITIVE_FLAG_PATTERN = new RegExp(`(--${SENSITIVE_INLINE_KEY_PATTERN})(=|\\s+)([^\\s,;]+)`, "giu");
43
56
  const ABSOLUTE_PATH_TOKEN_PATTERN = /(^|[\s"'`(=])\/(?:[^\s"'`),;]+)/g;
44
- function nowIso() {
45
- return new Date().toISOString();
46
- }
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;
47
60
  function queuePath(globalPmRoot) {
48
61
  return path.join(globalPmRoot, TELEMETRY_QUEUE_RELATIVE_PATH);
49
62
  }
@@ -94,10 +107,14 @@ function redactInlineSensitiveAssignments(input) {
94
107
  function redactAbsolutePathTokens(input) {
95
108
  return input.replaceAll(ABSOLUTE_PATH_TOKEN_PATTERN, (_match, prefix) => `${prefix}[redacted_path]`);
96
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
+ }
97
115
  function sanitizeStringRedacted(input) {
98
- const withoutEmails = input.replaceAll(/[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/giu, "[redacted_email]");
99
- const withoutBearer = withoutEmails.replaceAll(/bearer\s+[a-z0-9._=-]+/giu, "bearer [redacted_token]");
100
- const withoutInlineSecrets = redactInlineSensitiveAssignments(withoutBearer);
116
+ const withoutCommonSensitiveTokens = sanitizeCommonSensitiveTokens(input);
117
+ const withoutInlineSecrets = redactInlineSensitiveAssignments(withoutCommonSensitiveTokens);
101
118
  const withoutAbsolutePaths = redactAbsolutePathTokens(withoutInlineSecrets);
102
119
  const trimmed = withoutAbsolutePaths.trim();
103
120
  if (trimmed.startsWith("/") && trimmed.length > 1) {
@@ -109,12 +126,17 @@ function sanitizeStringRedacted(input) {
109
126
  return withoutAbsolutePaths;
110
127
  }
111
128
  function sanitizeStringMax(input) {
112
- const withoutBearer = input.replaceAll(/bearer\s+[a-z0-9._=-]+/giu, "bearer [redacted_token]");
113
- const withoutInlineSecrets = redactInlineSensitiveAssignments(withoutBearer);
114
- if (withoutInlineSecrets.length > 2048) {
115
- return `${withoutInlineSecrets.slice(0, 2045)}...`;
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]";
116
135
  }
117
- return withoutInlineSecrets;
136
+ if (withoutAbsolutePaths.length > 2048) {
137
+ return `${withoutAbsolutePaths.slice(0, 2045)}...`;
138
+ }
139
+ return withoutAbsolutePaths;
118
140
  }
119
141
  function sanitizeString(input, captureLevel = "redacted") {
120
142
  if (captureLevel === "max") {
@@ -122,7 +144,7 @@ function sanitizeString(input, captureLevel = "redacted") {
122
144
  }
123
145
  return sanitizeStringRedacted(input);
124
146
  }
125
- function sanitizeValue(value, keyHint, captureLevel = "redacted") {
147
+ function sanitizeValue(value, keyHint, captureLevel = "redacted", depth = 0) {
126
148
  if (keyHint && isSensitiveKey(keyHint)) {
127
149
  return "[redacted]";
128
150
  }
@@ -135,14 +157,17 @@ function sanitizeValue(value, keyHint, captureLevel = "redacted") {
135
157
  if (typeof value === "number" || typeof value === "boolean") {
136
158
  return value;
137
159
  }
160
+ if (depth >= TELEMETRY_SANITIZE_MAX_DEPTH) {
161
+ return "[depth_truncated]";
162
+ }
138
163
  if (Array.isArray(value)) {
139
- return value.map((entry) => sanitizeValue(entry, undefined, captureLevel));
164
+ return value.slice(0, TELEMETRY_SANITIZE_MAX_ARRAY_ITEMS).map((entry) => sanitizeValue(entry, undefined, captureLevel, depth + 1));
140
165
  }
141
166
  if (typeof value === "object") {
142
167
  const record = value;
143
168
  const sanitized = {};
144
169
  for (const [key, nested] of Object.entries(record)) {
145
- sanitized[key] = sanitizeValue(nested, key, captureLevel);
170
+ sanitized[key] = sanitizeValue(nested, key, captureLevel, depth + 1);
146
171
  }
147
172
  return sanitized;
148
173
  }
@@ -196,6 +221,28 @@ function normalizePmVersion(value) {
196
221
  const trimmed = value?.trim() ?? "";
197
222
  return trimmed.length > 0 ? trimmed : "0.0.0";
198
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
+ }
199
246
  function resolveTelemetrySourceContext(globalOptions) {
200
247
  const override = (process.env[PM_TELEMETRY_SOURCE_CONTEXT_ENV] ?? "").trim().toLowerCase();
201
248
  if (PM_TELEMETRY_SOURCE_CONTEXT_SET.has(override)) {
@@ -228,6 +275,44 @@ function resolveTelemetrySourceContext(globalOptions) {
228
275
  function hashWithInstallationId(installationId, value) {
229
276
  return crypto.createHash("sha256").update(`${installationId}:${value}`).digest("hex");
230
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
+ }
231
316
  function telemetryDisabledByEnvironment() {
232
317
  return PM_TELEMETRY_DISABLED_VALUES.has((process.env[PM_TELEMETRY_DISABLED_ENV] ?? "").trim().toLowerCase());
233
318
  }
@@ -294,6 +379,13 @@ async function exportLocalOtelSpan(activeCommand, outcome, finishedAtIso, durati
294
379
  }
295
380
  const serviceNameCandidate = sanitizeString((process.env[OTEL_SERVICE_NAME_ENV] ?? "").trim());
296
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
+ });
297
389
  const attributes = [
298
390
  otlpStringAttribute("pm.command", sanitizeString(activeCommand.command)),
299
391
  otlpStringAttribute("pm.version", activeCommand.pm_version),
@@ -304,8 +396,15 @@ async function exportLocalOtelSpan(activeCommand, outcome, finishedAtIso, durati
304
396
  otlpStringAttribute("pm.pm_root_hash", activeCommand.pm_root_hash),
305
397
  otlpStringAttribute("pm.cwd_hash", activeCommand.cwd_hash),
306
398
  otlpBoolAttribute("pm.ok", outcome.ok),
399
+ otlpIntAttribute("pm.exit_code", normalizedExitCode),
307
400
  otlpIntAttribute("pm.duration_ms", durationMs),
308
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
+ }
309
408
  if (typeof outcome.error === "string" && outcome.error.trim().length > 0) {
310
409
  attributes.push(otlpStringAttribute("pm.error", sanitizeString(outcome.error)));
311
410
  }
@@ -374,8 +473,16 @@ function summarizeResult(result, captureLevel = "redacted") {
374
473
  const record = result;
375
474
  const keys = Object.keys(record).sort((left, right) => left.localeCompare(right));
376
475
  const sanitized = {};
476
+ let previewBytes = 0;
377
477
  for (const key of keys.slice(0, 25)) {
378
- sanitized[key] = sanitizeValue(record[key], key, captureLevel);
478
+ const sanitizedValue = sanitizeValue(record[key], key, captureLevel);
479
+ const entrySize = JSON.stringify(sanitizedValue)?.length ?? 0;
480
+ if (previewBytes + entrySize > TELEMETRY_RESULT_PREVIEW_MAX_BYTES) {
481
+ sanitized[key] = "[preview_truncated]";
482
+ break;
483
+ }
484
+ previewBytes += entrySize;
485
+ sanitized[key] = sanitizedValue;
379
486
  }
380
487
  return {
381
488
  type: "object",
@@ -388,21 +495,37 @@ function summarizeResult(result, captureLevel = "redacted") {
388
495
  }
389
496
  function buildCommandStartPayload(params) {
390
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);
391
503
  if (captureLevel === "minimal") {
392
504
  return {
393
505
  capture_level: captureLevel,
394
506
  pm_version: pmVersion,
395
507
  source_context: sourceContext.source_context,
396
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,
397
514
  };
398
515
  }
399
516
  return {
400
517
  pm_version: pmVersion,
401
518
  source_context: sourceContext.source_context,
402
519
  source_context_source: sourceContext.source_context_source,
520
+ command_taxonomy: commandTaxonomy,
403
521
  command_args: sanitizeCommandArgs(context.args, captureLevel),
522
+ command_args_hashes: hashedArgs.hashes,
523
+ command_args_digest: hashedArgs.digest,
524
+ command_invocation_digest: commandInvocationDigest,
404
525
  command_options: sanitizeValue(context.options, undefined, captureLevel),
526
+ command_options_digest: commandOptionsDigest,
405
527
  global_options: sanitizeValue(context.global, undefined, captureLevel),
528
+ global_options_digest: globalOptionsDigest,
406
529
  pm_root_hash: pmRootHash,
407
530
  cwd_hash: cwdHash,
408
531
  capture_level: captureLevel,
@@ -417,15 +540,25 @@ function buildCommandStartPayload(params) {
417
540
  };
418
541
  }
419
542
  function buildCommandFinishPayload(params) {
420
- 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;
421
547
  if (captureLevel === "minimal") {
422
548
  return {
423
549
  capture_level: captureLevel,
424
550
  pm_version: pmVersion,
425
551
  source_context: sourceContext.source_context,
426
552
  source_context_source: sourceContext.source_context_source,
553
+ command_taxonomy: commandTaxonomy,
554
+ command_resolution: commandResolution,
555
+ resolution_stage: resolutionStage,
427
556
  ok: outcome.ok,
557
+ exit_code: exitCode,
558
+ error_code: errorCode,
559
+ error_category: errorCategory,
428
560
  error: outcome.error ? sanitizeString(outcome.error, "redacted") : undefined,
561
+ error_fingerprint: errorFingerprint,
429
562
  duration_ms: durationMs,
430
563
  };
431
564
  }
@@ -434,13 +567,77 @@ function buildCommandFinishPayload(params) {
434
567
  pm_version: pmVersion,
435
568
  source_context: sourceContext.source_context,
436
569
  source_context_source: sourceContext.source_context_source,
570
+ command_taxonomy: commandTaxonomy,
571
+ command_resolution: commandResolution,
572
+ resolution_stage: resolutionStage,
437
573
  ok: outcome.ok,
574
+ exit_code: exitCode,
575
+ error_code: errorCode,
576
+ error_category: errorCategory,
438
577
  error: outcome.error ? sanitizeString(outcome.error, captureLevel) : undefined,
578
+ error_fingerprint: errorFingerprint,
439
579
  duration_ms: durationMs,
440
580
  started_at: startedAt,
441
581
  result_summary: summarizeResult(outcome.result, captureLevel),
442
582
  };
443
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
+ }
444
641
  async function ensureInstallationId(globalPmRoot) {
445
642
  const settings = await readSettings(globalPmRoot);
446
643
  let changed = false;
@@ -462,7 +659,13 @@ async function enqueueTelemetryEvent(globalPmRoot, event) {
462
659
  event,
463
660
  attempts: 0,
464
661
  };
465
- await appendLineAtomic(queuePath(globalPmRoot), JSON.stringify(queued));
662
+ let serialized = JSON.stringify(queued);
663
+ if (serialized.length > TELEMETRY_MAX_EVENT_BYTES) {
664
+ const trimmed = { ...event, payload: { ...event.payload, result_summary: { truncated: true, reason: "payload_size_exceeded", original_bytes: serialized.length } } };
665
+ const trimmedQueued = { event: trimmed, attempts: 0 };
666
+ serialized = JSON.stringify(trimmedQueued);
667
+ }
668
+ await appendLineAtomic(queuePath(globalPmRoot), serialized);
466
669
  }
467
670
  function parseQueueLines(raw) {
468
671
  const entries = [];
@@ -521,7 +724,9 @@ function pruneExpiredQueueEntries(entries, retentionDays) {
521
724
  const retained = [];
522
725
  let prunedCount = 0;
523
726
  for (const entry of entries) {
524
- if (isExpiredQueueEntry(entry, cutoffMs)) {
727
+ const serializedSize = JSON.stringify(entry).length;
728
+ const oversized = serializedSize > TELEMETRY_MAX_EVENT_BYTES;
729
+ if (oversized || isExpiredQueueEntry(entry, cutoffMs) || entry.attempts >= TELEMETRY_MAX_QUEUE_ENTRY_ATTEMPTS) {
525
730
  prunedCount += 1;
526
731
  continue;
527
732
  }
@@ -578,12 +783,17 @@ async function flushQueue(globalPmRoot, endpoint, retentionDays) {
578
783
  }
579
784
  const dueIds = new Set(dueEntries.map((entry) => entry.event.event_id));
580
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
+ }
581
793
  try {
582
794
  const response = await fetch(normalizedEndpoint, {
583
795
  method: "POST",
584
- headers: {
585
- "content-type": "application/json",
586
- },
796
+ headers: requestHeaders,
587
797
  body: JSON.stringify({
588
798
  schema_version: TELEMETRY_SCHEMA_VERSION,
589
799
  events: dueEntries.map((entry) => entry.event),
@@ -670,11 +880,13 @@ export async function startTelemetryCommand(context) {
670
880
  }),
671
881
  };
672
882
  await enqueueTelemetryEvent(globalPmRoot, event);
673
- await flushQueue(globalPmRoot, endpoint, retentionDays);
883
+ _lastFlushPromise = flushQueue(globalPmRoot, endpoint, retentionDays).catch(() => { });
884
+ const commandTaxonomy = deriveTelemetryCommandTaxonomy(context.command);
674
885
  return {
675
886
  started_at: occurredAt,
676
887
  started_at_ms: Date.now(),
677
888
  command: context.command,
889
+ command_taxonomy: commandTaxonomy,
678
890
  pm_version: pmVersion,
679
891
  source_context: sourceContext.source_context,
680
892
  source_context_source: sourceContext.source_context_source,
@@ -702,6 +914,25 @@ export async function finishTelemetryCommand(activeCommand, outcome) {
702
914
  try {
703
915
  const finishedAt = nowIso();
704
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";
705
936
  const event = {
706
937
  schema_version: TELEMETRY_SCHEMA_VERSION,
707
938
  event_id: crypto.randomUUID(),
@@ -720,11 +951,92 @@ export async function finishTelemetryCommand(activeCommand, outcome) {
720
951
  outcome,
721
952
  durationMs,
722
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,
723
962
  }),
724
963
  };
725
964
  await enqueueTelemetryEvent(activeCommand.global_pm_root, event);
726
- await flushQueue(activeCommand.global_pm_root, activeCommand.endpoint, activeCommand.retention_days);
727
- await exportLocalOtelSpan(activeCommand, outcome, finishedAt, durationMs);
965
+ _lastFlushPromise = flushQueue(activeCommand.global_pm_root, activeCommand.endpoint, activeCommand.retention_days).catch(() => { });
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(() => { });
728
1040
  }
729
1041
  catch {
730
1042
  // Telemetry must never block command execution.