Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/docs/reference/telemetry.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ We collect the following categories of events:
| `rules_delivered` | Rules added to a session's context — at the start, for a request, or for a file the agent touched — with the number of rules (and how many are team rules), their total length, the session's running total, and the token budget. No rule text or file paths. |
| `rules_learning_changed` | The person turned learning from sessions on or off for a project, or back to the workspace's setting, with the resulting state and who decides it (`organization`, `you`, `workspace` or `default`). |
| `rules_command` | `/rules` opened in a client (`view`), or a `learn` subcommand run (its name only, never its arguments). |
| `lineage_report` | What a person did with the Claude Code plugin's lineage report, sent by the plugin through `altimate-code tool report-event`: the end-of-task summary was shown, **Open report** was pressed, `/lineage-report` was run, or the report was built, failed to build or found no browser to open in. Its payload is the action and the Claude Code session id, beside the fields every event carries (below): no model, column, file or project names. The report page itself makes no network requests. |

Each event includes a timestamp, session ID, a per-launch correlation ID (`launch_id` — a random value regenerated every process start, not persisted and not derived from your machine or identity; it exists only to group events from the same run), CLI version, and a machine ID (a random UUID stored in `~/.altimate/machine-id`, generated once and not derived from your hardware or identity). When the machine is signed in to Altimate, each event also carries `user_id` and `tenant`, described under [User Identification](#new-user-identification).

Expand Down
10 changes: 10 additions & 0 deletions packages/opencode/src/altimate/telemetry/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -918,6 +918,16 @@ export namespace Telemetry {
command: string
}
// altimate_change end
// altimate_change start — the Claude Code plugin's lineage report, sent through `tool report-event`
| {
/** What a person did with the Claude Code plugin's lineage report. No model, column, file or project names. */
type: "lineage_report"
timestamp: number
/** The host agent's session: `claude:<id>`. */
session_id: string
action: "summary_shown" | "open_pressed" | "command_run" | "built" | "build_failed" | "no_browser"

Copy link
Copy Markdown

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_ACTIONS compiles Telemetry.track fine 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", then action: ReportAction; the module has no imports, so the type-only edge is free) keeps the telemetry reference and the validator from drifting.

Copy link
Copy Markdown
Contributor Author

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_ACTIONS hold the same six actions, and a test pins that list. Deriving the field from ReportAction with 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.

}
// altimate_change end
| {
// Sent before connecting, so an attempt that hangs or ends the process is still counted.
type: "warehouse_connect_started"
Expand Down
21 changes: 21 additions & 0 deletions packages/opencode/src/altimate/tool-cli/report-event.ts
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 }
}
23 changes: 22 additions & 1 deletion packages/opencode/src/cli/cmd/tool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -330,12 +330,33 @@ function writeAll(text: string) {
}
}

const ToolReportEventCommand = cmd({
command: "report-event <action>",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[SUGGESTION]: docs/docs/usage/tool.md documents every other tool subcommand (list, context, run, daemon); this one is now user-visible in tool --help but has no entry there.

A short section saying it is the Claude Code plugin's hook, takes one of the six actions plus claude:<id>, and sends a single lineage_report event under the usual telemetry opt-out would keep the page complete and put the privacy contract where people reading tool docs will see it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point. A short report-event section in docs/docs/usage/tool.md (what calls it, the six actions, claude:<id>, one lineage_report event under the usual opt-out) is queued for the follow-up. Until then, the telemetry reference page describes the event.

describe: "record what a person did with the Claude Code plugin's lineage report (usage telemetry; nothing is sent when telemetry is off)",

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The 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, lineage_report is never sent (checked against a capture sink with each switch). But the first opted-out run on a machine that has a machine id still sends the one-time telemetry_opted_out marker, as docs/docs/reference/telemetry.md describes for every command. Suggest "usage telemetry, under the usual opt-out" here and the same in C3. Not blocking: wording about pre-existing, documented behaviour.

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)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor (not blocking): first_launch can go out with this command. The CLI middleware runs the welcome banner before this handler. When .installed-version is present, tool report-event bogus --session claude:abc exits 1 and still sends first_launch, and a valid call sends first_launch + lineage_report (reproduced). C1 and C2 hold for what this command adds; worth wording them that way. Not blocking: a pre-existing once-per-install event (version, is_upgrade, install_method, machine id) carrying nothing from the plugin.

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)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The 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 Telemetry.shutdown() flush budget, and the failure is swallowed correctly. Not blocking, because #1717 says the plugin runs this in the background; just make sure the plugin never awaits it on the report path.

},
})

/** 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() {},
})
25 changes: 25 additions & 0 deletions packages/opencode/test/altimate/tool-cli/report-event.test.ts
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", () => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[SUGGESTION]: These tests cover reportEvent() only; the CLI wiring the PR's claims describe (exit 1 and nothing tracked for a refused action or session) has no test.

test/altimate/tool-cli/cli.test.ts already spawns the real CLI through inShell with ALTIMATE_TELEMETRY_DISABLED=1, so a refused-action and a refused-session case (exit code 1, reason on stderr) is cheap and would lock in C2 end to end. Asserting the send path for C1/C3 could reuse the sink pattern from test/altimate/telemetry/* via APPLICATIONINSIGHTS_CONNECTION_STRING.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The 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 cli.test.ts, plus a sink check for C1 and C3, is queued for the same follow-up.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor (not blocking): the CLI path is untested. These tests cover reportEvent() only. With Telemetry.track(event) and process.exitCode = 1 removed from the handler in tool.ts, all test/altimate/tool-cli and test/cli/help tests still pass (106). Also, ALL_EVENT_TYPES in test/telemetry/telemetry.test.ts stays at 55 without lineage_report (the recent rules_* events were added there). Test-only, and fine in the spawned-CLI / sink-check follow-up you queued.

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) {
Comment thread
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")
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,6 @@ Options:
[string]"
`;

// altimate_change start — document the committed-answer echo allowance in run help
exports[`opencode CLI help-text snapshots every documented command emits stable help text: opencode run --help 1`] = `
"altimate-code run [message..]

Expand Down Expand Up @@ -144,7 +143,6 @@ Options:
--dangerously-skip-permissions auto-approve permissions that are not explicitly denied
(dangerous!) [boolean] [default: false]"
`;
// altimate_change end

exports[`opencode CLI help-text snapshots every documented command emits stable help text: opencode debug --help 1`] = `
"altimate-code debug
Expand Down Expand Up @@ -531,13 +529,16 @@ exports[`opencode CLI help-text snapshots every documented command emits stable
run altimate-code data tools directly, without a model

Commands:
altimate-code tool list list the data tools available to \`tool run\`
altimate-code tool context print the analyst method and the connections known at startup
(what \`mcp serve\` sends as instructions)
altimate-code tool schema <name> print one tool's description and JSON input schema
altimate-code tool run <name> run one data tool without a model and print its result
altimate-code tool daemon [action] run, inspect or stop the long-running tool process for a
project
altimate-code tool list list the data tools available to \`tool run\`
altimate-code tool context print the analyst method and the connections known at
startup (what \`mcp serve\` sends as instructions)
altimate-code tool schema <name> print one tool's description and JSON input schema
altimate-code tool run <name> run one data tool without a model and print its result
altimate-code tool daemon [action] run, inspect or stop the long-running tool process for a
project
altimate-code tool report-event <action> record what a person did with the Claude Code plugin's
lineage report (usage telemetry; nothing is sent when
telemetry is off)

Options:
-h, --help show help [boolean]
Expand Down
Loading