|
| 1 | +# Adding New Telemetry Metrics |
| 2 | + |
| 3 | +## Overview |
| 4 | + |
| 5 | +Every CLI command emits a `command_run` metric with a command key, exit reason, and command-specific attributes. This |
| 6 | +guide shows how to add telemetry to a new command. |
| 7 | + |
| 8 | +## Step 1: Register the command in `schemas/command-run.ts` |
| 9 | + |
| 10 | +Add an entry to `COMMAND_SCHEMAS`: |
| 11 | + |
| 12 | +```ts |
| 13 | +// No attributes: |
| 14 | +'remove.widget': NoAttrs, |
| 15 | + |
| 16 | +// With attributes: |
| 17 | +'add.widget': safeSchema({ |
| 18 | + widget_type: WidgetType, // z.enum(), z.boolean(), z.number(), or z.literal() only |
| 19 | + count: Count, |
| 20 | +}), |
| 21 | +``` |
| 22 | + |
| 23 | +`safeSchema` enforces allowed field types at compile time. No `z.string()` fields. |
| 24 | + |
| 25 | +## Step 2: Add enums to `schemas/common-shapes.ts` |
| 26 | + |
| 27 | +```ts |
| 28 | +export const WidgetType = z.enum(['basic', 'advanced']); |
| 29 | +``` |
| 30 | + |
| 31 | +Use `standardize()` to normalize input before recording: |
| 32 | + |
| 33 | +```ts |
| 34 | +import { WidgetType, standardize } from '../telemetry/schemas/common-shapes.js'; |
| 35 | + |
| 36 | +const type = standardize(WidgetType, userInput); |
| 37 | +``` |
| 38 | + |
| 39 | +## Step 3: Instrument the command handler |
| 40 | + |
| 41 | +Use **`withCommandRunTelemetry`** — the primary helper for recording telemetry: |
| 42 | + |
| 43 | +```ts |
| 44 | +import { withCommandRunTelemetry } from '../telemetry/cli-command-run.js'; |
| 45 | + |
| 46 | +const result = await withCommandRunTelemetry('remove.gateway', {}, () => this.remove(name)); |
| 47 | +``` |
| 48 | + |
| 49 | +**Signature:** |
| 50 | + |
| 51 | +```ts |
| 52 | +async function withCommandRunTelemetry<C extends Command, R extends OperationResult>( |
| 53 | + command: C, |
| 54 | + attrs: CommandAttrs<C>, |
| 55 | + fn: () => Promise<R> |
| 56 | +): Promise<R>; |
| 57 | +``` |
| 58 | + |
| 59 | +- `command` — the registered command key (e.g. `'add.widget'`) |
| 60 | +- `attrs` — attribute object matching the schema registered in Step 1 |
| 61 | +- `fn` — async callback returning `{ success: true } | { success: false; error: string }` |
| 62 | + |
| 63 | +**Behavior:** |
| 64 | + |
| 65 | +- On success (`{ success: true }`): records success telemetry with `attrs`, returns the result. |
| 66 | +- On failure (`{ success: false, error }`): records failure telemetry, returns the result to the caller. |
| 67 | +- On throw: records failure telemetry, returns `{ success: false, error }` so callers don't leak unhandled rejections. |
| 68 | +- If telemetry is unavailable: runs `fn()` untracked. |
| 69 | + |
| 70 | +**Example with attributes:** |
| 71 | + |
| 72 | +```ts |
| 73 | +const result = await withCommandRunTelemetry( |
| 74 | + 'add.widget', |
| 75 | + { widget_type: standardize(WidgetType, config.type), count: config.items.length }, |
| 76 | + () => widgetPrimitive.add(config) |
| 77 | +); |
| 78 | + |
| 79 | +if (!result.success) { |
| 80 | + console.error(result.error); |
| 81 | + process.exit(1); |
| 82 | +} |
| 83 | +``` |
| 84 | + |
| 85 | +### `runCliCommand` (alternative for top-level CLI handlers) |
| 86 | + |
| 87 | +For CLI handlers that own `process.exit`, use `runCliCommand` instead. The callback throws on failure and returns attrs |
| 88 | +on success: |
| 89 | + |
| 90 | +```ts |
| 91 | +await runCliCommand('add.widget', !!options.json, async () => { |
| 92 | + const result = await widgetPrimitive.add(options); |
| 93 | + if (!result.success) throw new Error(result.error); |
| 94 | + return { widget_type: standardize(WidgetType, options.type), count: options.items.length }; |
| 95 | +}); |
| 96 | +``` |
| 97 | + |
| 98 | +## Key Points |
| 99 | + |
| 100 | +- Telemetry never crashes the CLI — `standardize()` falls back gracefully, `resilientParse` defaults invalid fields to |
| 101 | + `'unknown'`. |
| 102 | +- Prefer `withCommandRunTelemetry` for new code — it returns the `Result` for the caller to handle output and control |
| 103 | + flow. |
| 104 | +- Use `runCliCommand` only when the handler owns `process.exit` and prints its own output. |
0 commit comments