Skip to content

Commit 87b1a9b

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 fa15c3a commit 87b1a9b

5 files changed

Lines changed: 7 additions & 66 deletions

File tree

docs/TELEMETRY.md

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ Each event carries a small set of 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 once for a turn that ends in an error instead | `$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
| `slash_command` | A slash command is dispatched in the TUI | `command_name` |
@@ -39,10 +38,9 @@ request IP; no location data is collected by the client.
3938
Every event is capped to an explicit property allowlist before it leaves the
4039
process — no other field can ever be attached, even by accident.
4140

42-
`provider_id` (and its AI-event equivalent `$ai_provider`) is the canonical
43-
provider kind resolved by the runtime (e.g. `openai-compatible`), never the
44-
free-text name you gave the provider in onboarding or settings. `model_id`
45-
(equivalently `$ai_model`) is the model identifier exactly as
41+
`$ai_provider` is the canonical provider kind resolved by the runtime (e.g.
42+
`openai-compatible`), never the free-text name you gave the provider in
43+
onboarding or settings. `$ai_model` is the model identifier exactly as
4644
configured — it is the one user-entered string that is sent, so do not put
4745
anything identifying in a model name.
4846

src/telemetry/ai-observability.ts

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

7979
telemetry.capture("$ai_generation", {
8080
$ai_trace_id: traceId,
81+
// The canonical provider kind, never ctx.source.sourceId: sourceId is the
82+
// user-typed label from onboarding/settings, and free text must not leave
83+
// the process under the no-PII contract.
8184
$ai_provider: ctx.source.provider,
8285
$ai_model: ctx.source.model,
8386
$ai_input_tokens: ctx.usage.input,

src/telemetry/index.ts

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,6 @@ export const TELEMETRY_NOTICE =
4545
export type TelemetryEvent =
4646
| "cli_start"
4747
| "session_end"
48-
| "inference_turn"
4948
| "$ai_generation"
5049
| "$ai_span"
5150
| "slash_command"
@@ -92,16 +91,6 @@ export function getSessionId(): string {
9291
const EVENT_PROPERTY_ALLOWLIST: Record<TelemetryEvent, readonly string[]> = {
9392
cli_start: [],
9493
session_end: ["status", "turn_count", "duration_ms", "session_mode", "exit_reason"],
95-
inference_turn: [
96-
"provider_id",
97-
"model_id",
98-
"input_tokens",
99-
"output_tokens",
100-
"cache_read_tokens",
101-
"cache_write_tokens",
102-
"thinking_tokens",
103-
"duration_ms",
104-
],
10594
// PostHog's LLM analytics views read the $ai_-prefixed properties and
10695
// nothing else, so every field these two events exist to surface has to
10796
// carry the documented name: an unprefixed property still arrives, but

src/tui/runner.ts

Lines changed: 1 addition & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1416,22 +1416,7 @@ export async function runTUI(initialConfig: Config): Promise<number> {
14161416
emitter,
14171417
hookManager,
14181418
initialTurnCount: resumeSeed.turnsUsed,
1419-
onTurnComplete: (ctx) => {
1420-
// provider_id is the canonical provider kind, never ctx.source.sourceId:
1421-
// sourceId is the user-typed label from onboarding/settings, and free
1422-
// text must not leave the process under the no-PII contract.
1423-
getTelemetry().capture("inference_turn", {
1424-
provider_id: ctx.source.provider,
1425-
model_id: ctx.source.model,
1426-
input_tokens: ctx.usage.input,
1427-
output_tokens: ctx.usage.output,
1428-
cache_read_tokens: ctx.usage.cacheRead,
1429-
cache_write_tokens: ctx.usage.cacheWrite,
1430-
thinking_tokens: ctx.usage.thinking,
1431-
duration_ms: ctx.durationMs,
1432-
});
1433-
turnObserver.onTurnComplete(ctx);
1434-
},
1419+
onTurnComplete: turnObserver.onTurnComplete,
14351420
onTurnFailed: turnObserver.onTurnFailed,
14361421
// persistRunSnapshot is defined below but not invoked until the stream
14371422
// starts consuming events, well after this closure captures it.

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)