Skip to content

Commit ed8d149

Browse files
committed
Retire the inference_turn event in favour of $ai_generation
Both events fired from the same hook with the same payload, so every turn was reported twice, and only $ai_generation reaches PostHog's LLM analytics views — inference_turn's numbers were readable in the raw event stream and nowhere else. This is a deliberate removal of the duplicate, not an oversight: nothing outside this repository consumed inference_turn.
1 parent 717954f commit ed8d149

5 files changed

Lines changed: 8 additions & 64 deletions

File tree

docs/TELEMETRY.md

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,12 @@ includes prompts, code, file contents, or paths.
66

77
## What's collected
88

9-
Five events, each with a small set of properties:
9+
Four events, each with a small set of properties:
1010

1111
| Event | When | Properties |
1212
|---|---|---|
1313
| `cli_start` | Once per used session (see First-run disclosure) | (none beyond common properties) |
1414
| `session_end` | When a TUI session finishes | `status`, `turn_count`, `duration_ms`, `session_mode`, `exit_reason` |
15-
| `inference_turn` | Once per completed turn | `provider_id`, `model_id`, `input_tokens`, `output_tokens`, `cache_read_tokens`, `cache_write_tokens`, `thinking_tokens`, `duration_ms` |
1615
| `$ai_generation` | Once per turn — on completion, and again on a turn that ends in an error | `$ai_trace_id`, `$ai_provider`, `$ai_model`, `$ai_input_tokens`, `$ai_output_tokens`, `$ai_latency`, `$ai_is_error`, `$ai_error`, `cache_read_tokens`, `cache_write_tokens`, `thinking_tokens` |
1716
| `$ai_span` | Once per top-level tool call in a completed turn | `$ai_trace_id`, `$ai_span_id`, `$ai_parent_id`, `$ai_span_name`, `$ai_is_error` |
1817

@@ -26,10 +25,9 @@ request IP; no location data is collected by the client.
2625
Every event is capped to an explicit property allowlist before it leaves the
2726
process — no other field can ever be attached, even by accident.
2827

29-
`provider_id` (and its AI-event equivalent `$ai_provider`) is the canonical
30-
provider kind resolved by the runtime (e.g. `openai-compatible`), never the
31-
free-text name you gave the provider in onboarding or settings. `model_id`
32-
(equivalently `$ai_model`) is the model identifier exactly as
28+
`$ai_provider` is the canonical provider kind resolved by the runtime (e.g.
29+
`openai-compatible`), never the free-text name you gave the provider in
30+
onboarding or settings. `$ai_model` is the model identifier exactly as
3331
configured — it is the one user-entered string that is sent, so do not put
3432
anything identifying in a model name.
3533

src/telemetry/ai-observability.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,9 @@ export function emitAiObservability(
6868

6969
telemetry.capture("$ai_generation", {
7070
$ai_trace_id: traceId,
71+
// The canonical provider kind, never ctx.source.sourceId: sourceId is the
72+
// user-typed label from onboarding/settings, and free text must not leave
73+
// the process under the no-PII contract.
7174
$ai_provider: ctx.source.provider,
7275
$ai_model: ctx.source.model,
7376
$ai_input_tokens: ctx.usage.input,

src/telemetry/index.ts

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ export type BatchTuning = {
4242
export const TELEMETRY_NOTICE =
4343
"Anonymous usage telemetry is enabled (no prompts, code, or paths collected). Disable in /settings > Telemetry. Docs: docs/TELEMETRY.md";
4444

45-
export type TelemetryEvent = "cli_start" | "session_end" | "inference_turn" | "$ai_generation" | "$ai_span";
45+
export type TelemetryEvent = "cli_start" | "session_end" | "$ai_generation" | "$ai_span";
4646

4747
// Fixed enum of AI observability span names. The raw tool name is never sent
4848
// as a property: an MCP tool name carries the server identifier it was
@@ -78,16 +78,6 @@ export function getSessionId(): string {
7878
const EVENT_PROPERTY_ALLOWLIST: Record<TelemetryEvent, readonly string[]> = {
7979
cli_start: [],
8080
session_end: ["status", "turn_count", "duration_ms", "session_mode", "exit_reason"],
81-
inference_turn: [
82-
"provider_id",
83-
"model_id",
84-
"input_tokens",
85-
"output_tokens",
86-
"cache_read_tokens",
87-
"cache_write_tokens",
88-
"thinking_tokens",
89-
"duration_ms",
90-
],
9181
// PostHog's LLM analytics views read the $ai_-prefixed properties and
9282
// nothing else, so every field these two events exist to surface has to
9383
// carry the documented name: an unprefixed property still arrives, but

src/tui/runner.ts

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1404,19 +1404,6 @@ export async function runTUI(initialConfig: Config): Promise<number> {
14041404
hookManager,
14051405
initialTurnCount: resumeSeed.turnsUsed,
14061406
onTurnComplete: (ctx) => {
1407-
// provider_id is the canonical provider kind, never ctx.source.sourceId:
1408-
// sourceId is the user-typed label from onboarding/settings, and free
1409-
// text must not leave the process under the no-PII contract.
1410-
getTelemetry().capture("inference_turn", {
1411-
provider_id: ctx.source.provider,
1412-
model_id: ctx.source.model,
1413-
input_tokens: ctx.usage.input,
1414-
output_tokens: ctx.usage.output,
1415-
cache_read_tokens: ctx.usage.cacheRead,
1416-
cache_write_tokens: ctx.usage.cacheWrite,
1417-
thinking_tokens: ctx.usage.thinking,
1418-
duration_ms: ctx.durationMs,
1419-
});
14201407
emitAiObservability(getTelemetry(), ctx, {
14211408
sessionId,
14221409
subagentToolName: taskToolDefinition.name,

tests/unit/telemetry.test.ts

Lines changed: 0 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -144,40 +144,6 @@ test("capture strips properties not in the event's allowlist", async () => {
144144
expect(body.properties.secret_field).toBeUndefined();
145145
});
146146

147-
test("capture strips properties not in inference_turn's allowlist", async () => {
148-
const { impl, events } = recordingFetch();
149-
const telemetry = createTelemetry({
150-
settings: settingsWith("id"),
151-
env: {},
152-
fetchFn: impl,
153-
apiKey: "test-key",
154-
});
155-
telemetry.capture("inference_turn", {
156-
provider_id: "anthropic",
157-
model_id: "claude-x",
158-
input_tokens: 10,
159-
output_tokens: 20,
160-
cache_read_tokens: 1,
161-
cache_write_tokens: 2,
162-
thinking_tokens: 3,
163-
duration_ms: 400,
164-
prompt: "should-not-appear",
165-
});
166-
await telemetry.flush();
167-
expect(events().length).toBe(1);
168-
const body = events()[0];
169-
expect(body.event).toBe("inference_turn");
170-
expect(body.properties.provider_id).toBe("anthropic");
171-
expect(body.properties.model_id).toBe("claude-x");
172-
expect(body.properties.input_tokens).toBe(10);
173-
expect(body.properties.output_tokens).toBe(20);
174-
expect(body.properties.cache_read_tokens).toBe(1);
175-
expect(body.properties.cache_write_tokens).toBe(2);
176-
expect(body.properties.thinking_tokens).toBe(3);
177-
expect(body.properties.duration_ms).toBe(400);
178-
expect(body.properties.prompt).toBeUndefined();
179-
});
180-
181147
test("capture strips properties not in $ai_generation's allowlist", async () => {
182148
const { impl, events } = recordingFetch();
183149
const telemetry = createTelemetry({

0 commit comments

Comments
 (0)