Repository navigation
feat: tool report-event for the Claude Code plugin's lineage report
#1718
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
cc430c6
5f599dd
00e9116
354cbe8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| // altimate_change - new file | ||
| // | ||
| // `tool report-event`: the Claude Code plugin records what a person did with its lineage report, as one | ||
| // usage-telemetry event. Only the action and the host session are sent: no model, column, file or project | ||
| // names. The plugin's page itself stays offline; this runs in the CLI, under its telemetry settings, so the | ||
| // usual opt-out turns it off. | ||
|
|
||
| export const REPORT_ACTIONS = ["summary_shown", "open_pressed", "command_run", "built", "build_failed", "no_browser"] as const | ||
| export type ReportAction = (typeof REPORT_ACTIONS)[number] | ||
|
|
||
| /** A Claude Code session id as the plugin passes it: `claude:` and the host's own id. */ | ||
| const HOST_SESSION = /^claude:[A-Za-z0-9_-]{1,128}$/ | ||
|
|
||
| export type ReportEvent = { type: "lineage_report"; timestamp: number; session_id: string; action: ReportAction } | ||
|
|
||
| /** The event for an action and a host session, or why there is none. */ | ||
| export function reportEvent(action: unknown, session: unknown, now = Date.now()): ReportEvent | { error: string } { | ||
| if (!REPORT_ACTIONS.includes(action as ReportAction)) return { error: `the action must be one of ${REPORT_ACTIONS.join(", ")}` } | ||
| if (typeof session !== "string" || !HOST_SESSION.test(session)) return { error: "--session takes a Claude Code session id: claude:<id>" } | ||
| return { type: "lineage_report", timestamp: now, session_id: session, action: action as ReportAction } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -330,12 +330,33 @@ function writeAll(text: string) { | |
| } | ||
| } | ||
|
|
||
| const ToolReportEventCommand = cmd({ | ||
| command: "report-event <action>", | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [SUGGESTION]: A short section saying it is the Claude Code plugin's hook, takes one of the six actions plus
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good point. A short |
||
| describe: "record what a person did with the Claude Code plugin's lineage report (usage telemetry; nothing is sent when telemetry is off)", | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Minor (not blocking): the help text overstates the opt-out. With telemetry off, |
||
| builder: (yargs: Argv) => | ||
| yargs | ||
| .positional("action", { type: "string", describe: "what happened to the report", demandOption: true }) | ||
| .option("session", { type: "string", describe: "the Claude Code session: claude:<id>", demandOption: true }), | ||
| handler: async (args) => { | ||
| const { reportEvent } = await import("../../altimate/tool-cli/report-event") | ||
| const event = reportEvent(args.action, args.session) | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Minor (not blocking): |
||
| if ("error" in event) { | ||
| console.error(event.error) | ||
| process.exitCode = 1 | ||
| return | ||
| } | ||
| // Flushed, or dropped under the opt-out, when the CLI exits. | ||
| const { Telemetry } = await import("../../altimate/telemetry") | ||
| Telemetry.track(event) | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Minor (not blocking): a hanging endpoint holds the command for about 10 s. Against an endpoint that never answers, the call took 10.9 s and exited 0 (unreachable: 0.9 s, exit 0). That is the inherited |
||
| }, | ||
| }) | ||
|
|
||
| /** How long `writeAll` waits for a reader that has stopped taking output before it gives up. */ | ||
| const WRITE_STALL_MS = 60_000 | ||
|
|
||
| export const ToolCommand = cmd({ | ||
| command: "tool", | ||
| describe: "run altimate-code data tools directly, without a model", | ||
| builder: (yargs: Argv) => yargs.command(ToolListCommand).command(ToolContextCommand).command(ToolSchemaCommand).command(ToolRunCommand).command(ToolDaemonCommand).demandCommand(), | ||
| builder: (yargs: Argv) => yargs.command(ToolListCommand).command(ToolContextCommand).command(ToolSchemaCommand).command(ToolRunCommand).command(ToolDaemonCommand).command(ToolReportEventCommand).demandCommand(), | ||
| async handler() {}, | ||
| }) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| import { describe, expect, test } from "bun:test" | ||
| import { REPORT_ACTIONS, reportEvent } from "../../../src/altimate/tool-cli/report-event" | ||
|
|
||
| describe("tool report-event", () => { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [SUGGESTION]: These tests cover
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not in this PR. C2's CLI path was checked by hand from source: a valid call exits 0, and a bad action or a bad session each exits 1 with the reason and sends nothing. A spawned-CLI test of those exits in
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Minor (not blocking): the CLI path is untested. These tests cover |
||
| test("an action and a Claude Code session make one event with nothing else in it", () => { | ||
| expect(REPORT_ACTIONS).toEqual(["summary_shown", "open_pressed", "command_run", "built", "build_failed", "no_browser"]) | ||
| for (const action of REPORT_ACTIONS) { | ||
|
cubic-dev-ai[bot] marked this conversation as resolved.
Resolved
|
||
| expect(reportEvent(action, "claude:3f6c2a10-7b2e-4c1d-9a55-0e1f2a3b4c5d", 1000)).toEqual({ | ||
| type: "lineage_report", | ||
| timestamp: 1000, | ||
| session_id: "claude:3f6c2a10-7b2e-4c1d-9a55-0e1f2a3b4c5d", | ||
| action, | ||
| }) | ||
| } | ||
| }) | ||
|
|
||
| test("an action outside the list is refused", () => { | ||
| for (const action of ["clicked_column", "", undefined, "built; rm -rf /"]) expect(reportEvent(action, "claude:abc")).toHaveProperty("error") | ||
| }) | ||
|
|
||
| test("a session that is not claude:<id> is refused, so no path or name can ride along", () => { | ||
| for (const session of ["abc", "codex:abc", "claude:", "claude:/home/someone/project", "claude:a b", `claude:${"x".repeat(129)}`, undefined]) | ||
| expect(reportEvent("built", session)).toHaveProperty("error") | ||
| }) | ||
| }) | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[SUGGESTION]: The six lineage-report actions are declared twice: here and in
REPORT_ACTIONS(altimate/tool-cli/report-event.ts:8).Adding an action to this union without adding it to
REPORT_ACTIONScompilesTelemetry.trackfine but advertises an action the CLI always rejects with exit 1; only the other direction (list first, union second) is caught by typecheck. Deriving the field from the single list (import type { ReportAction } from "../tool-cli/report-event", thenaction: ReportAction; the module has no imports, so the type-only edge is free) keeps the telemetry reference and the validator from drifting.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed this is a drift risk, but no fix in this PR: as it stands, the union and
REPORT_ACTIONShold the same six actions, and a test pins that list. Deriving the field fromReportActionwith a type-only import is queued for a small follow-up, so this PR can go into today's release without another full CI run.