Skip to content

Commit 4ef958c

Browse files
committed
docs: add telemetry instrumentation guide
1 parent 52a24ce commit 4ef958c

3 files changed

Lines changed: 110 additions & 0 deletions

File tree

.github/harness/prompts/review.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,5 @@ comment on the PR saying it looks good to merge (or that all issues have already
2525
- **Excessive mocking** — Avoid excessive mocking; it couples tests to implementation details, provides weaker
2626
guarantees, and often points to mismanaged dependencies. Prefer real dependencies (e.g. temp directories over fs
2727
mocks) and only mock at true I/O boundaries (e.g., network calls, AWS SDK clients, HTTP requests).
28+
- **Missing telemetry** — New features should include telemetry instrumentation. See `src/cli/telemetry/README.md` for
29+
guidance on what and how to instrument.

AGENTS.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,10 @@ See `docs/TESTING.md` for details.
143143
- Always look for existing types before creating a new type inline.
144144
- Re-usable constants must be defined in a constants file in the closest sensible subdirectory.
145145

146+
## Telemetry
147+
148+
New features must include telemetry instrumentation. See `src/cli/telemetry/README.md` for how to add metrics.
149+
146150
## Multi-Partition Support (GovCloud, China)
147151

148152
The CLI supports multiple AWS partitions (commercial, GovCloud, China) through a central utility at

src/cli/telemetry/README.md

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
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

Comments
 (0)