apps/server/src/telemetry/FeatureUsage.ts
import { type OrchestrationV2Command, WS_METHODS } from "@t3tools/contracts";
export interface FeatureUsage {
readonly feature: string;
readonly variant?: string;
}
type Payload = Readonly<Record<string, unknown>>;
const literal = (payload: Payload, key: string) =>
typeof payload[key] === "string" ? (payload[key] as string) : undefined;
const use = (feature: string, variant?: string): FeatureUsage =>
variant === undefined ? { feature } : { feature, variant };
/**
* Exclude housekeeping. `client.turn.requested` counts every `message.dispatch`; only sending a
* guest's message is also a feature. Record at command dispatch so retries do not count twice.
*/
const COMMAND_FEATURES: {
readonly [Type in OrchestrationV2Command["type"]]?: (
command: Extract<OrchestrationV2Command, { readonly type: Type }>,
) => FeatureUsage | undefined;
} = {
"message.dispatch": (command) =>
command.guestMessageId === undefined ? undefined : { feature: "guest_message.send" },
"checkpoint.rollback": (command) => ({
feature: "checkpoint.rollback",
variant: command.restoreFiles === false ? "conversation_only" : "with_files",
}),
"thread.fork": () => ({ feature: "thread.fork" }),
"thread.merge_back": () => ({ feature: "thread.merge_back" }),
"provider.switch": () => ({ feature: "provider.switch" }),
"thread.archive": () => ({ feature: "thread.archive" }),
"thread.snooze": () => ({ feature: "thread.snooze" }),
"thread.pin": () => ({ feature: "thread.pin" }),
"thread.settle": () => ({ feature: "thread.settle" }),
"thread.pull-request.link": () => ({ feature: "thread.pull_request.link" }),
"thread.pull-request.watch": (command) =>
use("thread.pull_request.watch", command.watching ? "on" : "off"),
"thread.runtime-mode.set": (command) => use("thread.runtime_mode.set", command.runtimeMode),
"thread.interaction-mode.set": (command) =>
use("thread.interaction_mode.set", command.interactionMode),
"run.interrupt": () => ({ feature: "run.interrupt" }),
"queued-message.promote-to-steer": () => ({ feature: "queue.steer" }),
"queued-run.edit": () => ({ feature: "queue.edit" }),
"thread.guest-share.close": () => ({ feature: "guest_share.close" }),
"thread.guest-message.dismiss": () => ({ feature: "guest_message.dismiss" }),
};
/**
* RPCs that represent a user starting a feature. Variants come only from
* closed enums in the contracts, so no path, id, or free text can reach analytics.
* Thread launches are left out because `client.thread.started` counts them.
*/
const METHOD_FEATURES: Readonly<Record<string, (payload: Payload) => FeatureUsage>> = {
[WS_METHODS.terminalOpen]: () => ({ feature: "terminal.open" }),
[WS_METHODS.previewOpen]: () => ({ feature: "preview.open" }),
[WS_METHODS.deviceOpen]: () => ({ feature: "device.open" }),
[WS_METHODS.shellOpenInEditor]: (payload) => use("editor.open", literal(payload, "editor")),
[WS_METHODS.vcsCreateWorktree]: () => ({ feature: "worktree.create" }),
[WS_METHODS.gitRunStackedAction]: (payload) =>
use("git.stacked_action", literal(payload, "action")),
[WS_METHODS.pullRequestsRunAction]: (payload) =>
use("pull_request.action", literal(payload, "action")),
[WS_METHODS.pullRequestsSubmitReview]: () => ({ feature: "pull_request.review" }),
[WS_METHODS.pullRequestsComment]: () => ({ feature: "pull_request.comment" }),
[WS_METHODS.scheduledTasksUpsert]: () => ({ feature: "scheduled_task.save" }),
[WS_METHODS.scheduledTasksRunNow]: () => ({ feature: "scheduled_task.run_now" }),
[WS_METHODS.agentSessionsImport]: () => ({ feature: "agent_session.import" }),
[WS_METHODS.projectCloneStart]: () => ({ feature: "project.clone" }),
[WS_METHODS.sourceControlPublishRepository]: () => ({ feature: "repository.publish" }),
[WS_METHODS.sourceControlSignInStart]: () => ({ feature: "source_control.sign_in" }),
[WS_METHODS.serverUpsertKeybinding]: () => ({ feature: "keybinding.customize" }),
[WS_METHODS.guestShareCreate]: () => ({ feature: "guest_share.create" }),
};
/** The feature a dispatched command uses, or undefined when it is not a tracked feature. */
export function commandFeatureUsage(command: OrchestrationV2Command): FeatureUsage | undefined {
// TypeScript cannot pair a union's tag with its own entry, so widen once here.
const mapping = COMMAND_FEATURES[command.type] as
| ((command: OrchestrationV2Command) => FeatureUsage | undefined)
| undefined;
return mapping?.(command);
}
/** The feature a WebSocket request uses, or undefined when it is not a tracked feature. */
export function featureUsage(method: string, payload: unknown): FeatureUsage | undefined {
if (typeof payload !== "object" || payload === null) return undefined;
return METHOD_FEATURES[method]?.(payload as Payload);
}