@lorekit/cli 1.39.1 → 1.39.2
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/package.json +1 -1
- package/src/telemetry.mjs +80 -7
package/package.json
CHANGED
package/src/telemetry.mjs
CHANGED
|
@@ -39,6 +39,9 @@ const DEFAULT_DATASET = 'default';
|
|
|
39
39
|
// Flags worth counting (e.g. how many installs are --global). Bounded on
|
|
40
40
|
// purpose: only these booleans are ever attached, never free-form values.
|
|
41
41
|
const FLAG_ATTRS = ['global', 'project', 'deep', 'yes', 'force', 'no-hooks', 'json', 'link'];
|
|
42
|
+
// One definition, used both to WRITE a flag attribute and to recognise one as
|
|
43
|
+
// reserved in `commandAttributes` — the two must not be able to drift.
|
|
44
|
+
const FLAG_ATTR_PREFIX = 'lorekit.cli.flag.';
|
|
42
45
|
|
|
43
46
|
const OFF_VALUES = new Set(['0', 'off', 'false', 'no', 'disable', 'disabled']);
|
|
44
47
|
|
|
@@ -245,17 +248,87 @@ function resourceAttributes(version, env = process.env) {
|
|
|
245
248
|
|
|
246
249
|
// ── Payload builders (pure — unit-tested) ─────────────────────────────────────
|
|
247
250
|
|
|
251
|
+
/**
|
|
252
|
+
* The CLOSED vocabulary of `lorekit.cli.outcome`, and the one place it is
|
|
253
|
+
* written down in code.
|
|
254
|
+
*
|
|
255
|
+
* The three values are not synonyms and the distinction is load-bearing:
|
|
256
|
+
*
|
|
257
|
+
* - `ok` — ran, exit 0.
|
|
258
|
+
* - `failure` — RAN TO COMPLETION and reported a negative VERDICT (a failing
|
|
259
|
+
* `doctor` check, a `lint` finding). The command did its job.
|
|
260
|
+
* - `error` — CRASHED. This is the only one that also sets the span status to
|
|
261
|
+
* `STATUS_CODE_ERROR`.
|
|
262
|
+
*
|
|
263
|
+
* That is what keeps the `cli` service's error rate a measure of the CLI being
|
|
264
|
+
* broken rather than of the user's environment being unhealthy (see the note
|
|
265
|
+
* above the non-zero-exit branch in {@link traceCommand}).
|
|
266
|
+
*
|
|
267
|
+
* WHY A FROZEN CONSTANT RATHER THAN THREE STRING LITERALS. The values were only
|
|
268
|
+
* ever written inline, so the vocabulary was discoverable from the emitted
|
|
269
|
+
* telemetry and nowhere else — and read from telemetry alone the distinction is
|
|
270
|
+
* genuinely easy to misread. A `doctor` that CRASHED in one release and FAILED
|
|
271
|
+
* GRACEFULLY in the next shows up as `error` then `failure` for the same
|
|
272
|
+
* user-visible symptom, which reads like the attribute drifting when it is
|
|
273
|
+
* actually the CLI getting better. Naming the set makes the difference legible
|
|
274
|
+
* at the call site and gives `telemetry.test.mjs` something to pin the docs to.
|
|
275
|
+
*/
|
|
276
|
+
export const CLI_OUTCOMES = Object.freeze({
|
|
277
|
+
OK: 'ok',
|
|
278
|
+
FAILURE: 'failure',
|
|
279
|
+
ERROR: 'error',
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
/** The same vocabulary as a value list, for guards and exhaustiveness checks. */
|
|
283
|
+
export const CLI_OUTCOME_VALUES = Object.freeze(Object.values(CLI_OUTCOMES));
|
|
284
|
+
|
|
285
|
+
/** The attribute keys `commandAttributes` owns — see its docblock below. */
|
|
286
|
+
const isReservedAttr = (key) =>
|
|
287
|
+
key === 'lorekit.cli.command' ||
|
|
288
|
+
key === 'lorekit.cli.outcome' ||
|
|
289
|
+
key === 'lorekit.cli.exit_code' ||
|
|
290
|
+
key.startsWith(FLAG_ATTR_PREFIX);
|
|
291
|
+
|
|
248
292
|
/**
|
|
249
293
|
* Collect the bounded, non-PII attributes for a command invocation. Only the
|
|
250
294
|
* command name, allow-listed boolean flags, the outcome and the exit code.
|
|
295
|
+
*
|
|
296
|
+
* Deliberately does NOT validate `outcome`: this runs inside the `finally` of
|
|
297
|
+
* every traced command, where throwing would turn a telemetry problem into a
|
|
298
|
+
* command failure. The vocabulary is enforced at the call sites (all of which
|
|
299
|
+
* are in this file) and pinned by `telemetry.test.mjs`.
|
|
300
|
+
*
|
|
301
|
+
* The keys this function owns — command, outcome, exit code, flags — are a
|
|
302
|
+
* RESERVED NAMESPACE: an `extraAttrs` entry under one of them is dropped, and
|
|
303
|
+
* the owned value (if any) is written afterwards. `extraAttrs` used to be
|
|
304
|
+
* merged over last, which meant a command returning
|
|
305
|
+
* `{ exitCode, 'lorekit.cli.outcome': … }` silently replaced the frozen value on
|
|
306
|
+
* its way out — a runtime path the source scan cannot see, because it proves
|
|
307
|
+
* the literal at the call site and not that the value reaches the wire.
|
|
308
|
+
*
|
|
309
|
+
* Reserving the NAMESPACE rather than just overwriting key by key matters
|
|
310
|
+
* because two of the owned keys are written conditionally: `exit_code` only
|
|
311
|
+
* when `exitCode` is a number, and each flag only when it is truthy. Overwriting
|
|
312
|
+
* alone therefore left the gap open in exactly the cases where the CLI emits
|
|
313
|
+
* nothing — an extras value would have been the only `lorekit.cli.exit_code` on
|
|
314
|
+
* the span, sourced from the command rather than from here.
|
|
315
|
+
*
|
|
316
|
+
* A collision is dropped, not rejected: this runs inside the `finally` of every
|
|
317
|
+
* traced command, where throwing would turn a telemetry problem into a command
|
|
318
|
+
* failure. Losing a datum a command should not have put there is the smaller
|
|
319
|
+
* harm than emitting an unowned value under an owned key.
|
|
251
320
|
*/
|
|
252
321
|
export function commandAttributes({ command, args = {}, outcome, exitCode, extraAttrs = {} }) {
|
|
253
|
-
const attrs = {
|
|
322
|
+
const attrs = {};
|
|
323
|
+
for (const [key, value] of Object.entries(extraAttrs)) {
|
|
324
|
+
if (!isReservedAttr(key)) attrs[key] = value;
|
|
325
|
+
}
|
|
326
|
+
attrs['lorekit.cli.command'] = command;
|
|
327
|
+
attrs['lorekit.cli.outcome'] = outcome;
|
|
254
328
|
if (typeof exitCode === 'number') attrs['lorekit.cli.exit_code'] = exitCode;
|
|
255
329
|
for (const flag of FLAG_ATTRS) {
|
|
256
|
-
if (args[flag]) attrs[
|
|
330
|
+
if (args[flag]) attrs[`${FLAG_ATTR_PREFIX}${flag}`] = true;
|
|
257
331
|
}
|
|
258
|
-
Object.assign(attrs, extraAttrs);
|
|
259
332
|
return attrs;
|
|
260
333
|
}
|
|
261
334
|
|
|
@@ -389,7 +462,7 @@ export async function probeTelemetryExport(config, { version = '0.0.0', timeoutM
|
|
|
389
462
|
name: 'lorekit.cli.doctor.telemetry_probe',
|
|
390
463
|
attributes: {
|
|
391
464
|
'lorekit.cli.command': 'doctor',
|
|
392
|
-
'lorekit.cli.outcome':
|
|
465
|
+
'lorekit.cli.outcome': CLI_OUTCOMES.OK,
|
|
393
466
|
'lorekit.telemetry.probe': true,
|
|
394
467
|
},
|
|
395
468
|
startMs: now,
|
|
@@ -533,7 +606,7 @@ export async function traceCommand(command, args, version, run) {
|
|
|
533
606
|
// `outcome` is the command's VERDICT (ok | failure | error); `status` is the
|
|
534
607
|
// SPAN status, and only a crash sets it to error. See the note above the
|
|
535
608
|
// non-zero-exit branch below.
|
|
536
|
-
let outcome =
|
|
609
|
+
let outcome = CLI_OUTCOMES.OK;
|
|
537
610
|
let status = 'ok';
|
|
538
611
|
let statusMessage;
|
|
539
612
|
let extraAttrs = {};
|
|
@@ -563,11 +636,11 @@ export async function traceCommand(command, args, version, run) {
|
|
|
563
636
|
// the CLI being broken rather than of the user's environment being
|
|
564
637
|
// unhealthy. Query the failure verdicts on those attributes, never on the
|
|
565
638
|
// span status.
|
|
566
|
-
outcome =
|
|
639
|
+
outcome = CLI_OUTCOMES.FAILURE;
|
|
567
640
|
}
|
|
568
641
|
return exitCode;
|
|
569
642
|
} catch (e) {
|
|
570
|
-
outcome =
|
|
643
|
+
outcome = CLI_OUTCOMES.ERROR;
|
|
571
644
|
status = 'error';
|
|
572
645
|
// Record only a bounded, non-PII identifier — NEVER e.message. Node fs /
|
|
573
646
|
// network error messages embed absolute paths (e.g. "ENOENT: ... open
|