From c3cb70c33338d788233bf504c85ba21ceb556868 Mon Sep 17 00:00:00 2001 From: Joshua Snyder Date: Mon, 23 Mar 2026 12:21:10 +0000 Subject: [PATCH] feat: add recursion poem to poetry collection --- apps/code/src/shared/types/analytics.ts | 42 ++ docs/INSTRUMENTATION.md | 428 ++++++++++++++++++ packages/agent/package.json | 1 + packages/agent/src/adapters/base-acp-agent.ts | 3 + .../agent/src/adapters/claude/claude-agent.ts | 44 ++ .../adapters/claude/conversion/sdk-to-acp.ts | 44 ++ packages/agent/src/adapters/claude/hooks.ts | 9 + .../claude/permissions/permission-handlers.ts | 17 + packages/agent/src/adapters/claude/types.ts | 1 + packages/agent/src/analytics.ts | 71 +++ packages/agent/src/server/agent-server.ts | 4 + pnpm-lock.yaml | 13 + poems.txt | 59 +++ 13 files changed, 736 insertions(+) create mode 100644 docs/INSTRUMENTATION.md create mode 100644 packages/agent/src/analytics.ts create mode 100644 poems.txt diff --git a/apps/code/src/shared/types/analytics.ts b/apps/code/src/shared/types/analytics.ts index 526ae1dfe..9005118ee 100644 --- a/apps/code/src/shared/types/analytics.ts +++ b/apps/code/src/shared/types/analytics.ts @@ -142,6 +142,28 @@ export interface SettingChangedProperties { old_value?: string | boolean | number; } +// Terminal events +export interface TerminalOpenedProperties { + task_id: string; +} + +// File tree events +export interface FileTreeToggledProperties { + task_id: string; + visible: boolean; +} + +// Session fork/resume events +export interface SessionForkedProperties { + task_id: string; + original_session_id: string; +} + +export interface SessionResumedProperties { + task_id: string; + original_session_id: string; +} + // Error events export interface TaskCreationFailedProperties { error_type: string; @@ -240,6 +262,16 @@ export const ANALYTICS_EVENTS = { // Feedback events TASK_FEEDBACK: "Task feedback", + // Terminal events + TERMINAL_OPENED: "Terminal opened", + + // File tree events + FILE_TREE_TOGGLED: "File tree toggled", + + // Session fork/resume events + SESSION_FORKED: "Session forked", + SESSION_RESUMED: "Session resumed", + // Error events TASK_CREATION_FAILED: "Task creation failed", AGENT_SESSION_ERROR: "Agent session error", @@ -293,6 +325,16 @@ export type EventPropertyMap = { // Feedback events [ANALYTICS_EVENTS.TASK_FEEDBACK]: TaskFeedbackProperties; + // Terminal events + [ANALYTICS_EVENTS.TERMINAL_OPENED]: TerminalOpenedProperties; + + // File tree events + [ANALYTICS_EVENTS.FILE_TREE_TOGGLED]: FileTreeToggledProperties; + + // Session fork/resume events + [ANALYTICS_EVENTS.SESSION_FORKED]: SessionForkedProperties; + [ANALYTICS_EVENTS.SESSION_RESUMED]: SessionResumedProperties; + // Error events [ANALYTICS_EVENTS.TASK_CREATION_FAILED]: TaskCreationFailedProperties; [ANALYTICS_EVENTS.AGENT_SESSION_ERROR]: AgentSessionErrorProperties; diff --git a/docs/INSTRUMENTATION.md b/docs/INSTRUMENTATION.md new file mode 100644 index 000000000..781e59209 --- /dev/null +++ b/docs/INSTRUMENTATION.md @@ -0,0 +1,428 @@ +# PostHog Code Instrumentation + +All analytics events tracked from the Code app (desktop, agent, mobile). + +## Desktop App — Renderer Events + +Source: `apps/code/src/shared/types/analytics.ts`, tracked via `posthog-js` in the renderer process. + +All renderer events include `team: "posthog-code"` as a default property. + +### App Lifecycle + +#### `App started` + +No properties. + +#### `App quit` + +No properties. + +### Authentication + +#### `User logged in` + +| Property | Type | Description | +|---|---|---| +| `email` | `string?` | User email | +| `uuid` | `string?` | User UUID | +| `project_id` | `string?` | Project ID | +| `region` | `string?` | Region | + +#### `User logged out` + +No properties. + +### Task Management + +#### `Task list viewed` + +| Property | Type | Description | +|---|---|---| +| `filter_type` | `string?` | Active filter | +| `sort_field` | `string?` | Sort field | +| `view_mode` | `string?` | View mode | + +#### `Task created` + +| Property | Type | Description | +|---|---|---| +| `auto_run` | `boolean` | Whether the task auto-runs | +| `created_from` | `string` | `cli` or `command-menu` | +| `repository_provider` | `string?` | `github`, `gitlab`, `local`, or `none` | + +#### `Task viewed` + +| Property | Type | Description | +|---|---|---| +| `task_id` | `string` | Task UUID | + +#### `Task run` + +| Property | Type | Description | +|---|---|---| +| `task_id` | `string` | Task UUID | +| `execution_type` | `string` | `cloud` or `local` | + +#### `Task run started` + +| Property | Type | Description | +|---|---|---| +| `task_id` | `string` | Task UUID | +| `execution_type` | `string` | `cloud` or `local` | +| `model` | `string?` | Model used | +| `initial_mode` | `string?` | Initial mode | +| `adapter` | `string?` | Adapter type | + +#### `Task run completed` + +| Property | Type | Description | +|---|---|---| +| `task_id` | `string` | Task UUID | +| `execution_type` | `string` | `cloud` or `local` | +| `duration_seconds` | `number` | Run duration | +| `prompts_sent` | `number` | Number of prompts sent | +| `stop_reason` | `string` | `user_cancelled`, `completed`, `error`, or `timeout` | + +#### `Task run cancelled` + +| Property | Type | Description | +|---|---|---| +| `task_id` | `string` | Task UUID | +| `execution_type` | `string` | `cloud` or `local` | +| `duration_seconds` | `number` | Run duration | +| `prompts_sent` | `number` | Number of prompts sent | + +#### `Prompt sent` + +| Property | Type | Description | +|---|---|---| +| `task_id` | `string` | Task UUID | +| `is_initial` | `boolean` | Whether it's the first prompt | +| `execution_type` | `string` | `cloud` or `local` | +| `prompt_length_chars` | `number` | Length of the prompt | + +#### `Task creation failed` + +| Property | Type | Description | +|---|---|---| +| `error_type` | `string` | Error classification | +| `failed_step` | `string?` | Step that failed | + +#### `Task feedback` + +| Property | Type | Description | +|---|---|---| +| `task_id` | `string` | Task UUID | +| `task_run_id` | `string?` | Task run UUID | +| `log_url` | `string?` | Log URL | +| `event_count` | `number` | Number of events | +| `feedback_type` | `string` | `good`, `bad`, or `general` | +| `feedback_comment` | `string?` | User comment | + +### Repository + +#### `Repository selected` + +| Property | Type | Description | +|---|---|---| +| `repository_provider` | `string` | `github`, `gitlab`, `local`, or `none` | +| `source` | `string` | `task-creation` or `task-detail` | + +### Git Operations + +#### `Git action executed` + +| Property | Type | Description | +|---|---|---| +| `action_type` | `string` | `push`, `pull`, `sync`, `publish`, `commit`, `commit-push`, `create-pr`, `view-pr`, or `update-pr` | +| `success` | `boolean` | Whether the action succeeded | +| `task_id` | `string?` | Task UUID | + +#### `PR created` + +| Property | Type | Description | +|---|---|---| +| `task_id` | `string?` | Task UUID | +| `success` | `boolean` | Whether PR creation succeeded | + +### File Interactions + +#### `File opened` + +| Property | Type | Description | +|---|---|---| +| `file_extension` | `string` | File extension | +| `source` | `string` | `sidebar`, `agent-suggestion`, `search`, or `diff` | +| `task_id` | `string?` | Task UUID | + +#### `File diff viewed` + +| Property | Type | Description | +|---|---|---| +| `file_extension` | `string` | File extension | +| `change_type` | `string` | `added`, `modified`, or `deleted` | +| `task_id` | `string?` | Task UUID | + +#### `File tree toggled` + +| Property | Type | Description | +|---|---|---| +| `task_id` | `string` | Task UUID | +| `visible` | `boolean` | Whether the file tree is visible | + +### Workspace Events + +#### `Workspace created` + +| Property | Type | Description | +|---|---|---| +| `task_id` | `string` | Task UUID | +| `mode` | `string` | `cloud`, `worktree`, or `local` | + +#### `Workspace scripts started` + +| Property | Type | Description | +|---|---|---| +| `task_id` | `string` | Task UUID | +| `scripts_count` | `number` | Number of scripts | + +#### `Folder registered` + +| Property | Type | Description | +|---|---|---| +| `path_hash` | `string` | Hash of the path | + +### Navigation Events + +#### `Settings viewed` + +No properties. + +#### `Command menu opened` + +No properties. + +#### `Command menu action` + +| Property | Type | Description | +|---|---|---| +| `action_type` | `string` | `home`, `new-task`, `settings`, `logout`, `toggle-theme`, `toggle-left-sidebar`, or `toggle-right-sidebar` | + +#### `Command center viewed` + +No properties. + +### Permission Events + +#### `Permission responded` + +| Property | Type | Description | +|---|---|---| +| `task_id` | `string` | Task UUID | +| `tool_name` | `string?` | Tool name | +| `option_id` | `string?` | Option selected | +| `option_kind` | `string?` | Kind of option | +| `custom_input` | `string?` | Custom input value | + +#### `Permission cancelled` + +| Property | Type | Description | +|---|---|---| +| `task_id` | `string` | Task UUID | +| `tool_name` | `string?` | Tool name | + +### Session Events + +#### `Session config changed` + +| Property | Type | Description | +|---|---|---| +| `task_id` | `string` | Task UUID | +| `category` | `string` | Config category | +| `from_value` | `string` | Previous value | +| `to_value` | `string` | New value | + +#### `Setting changed` + +| Property | Type | Description | +|---|---|---| +| `setting_name` | `string` | Setting name | +| `new_value` | `string \| boolean \| number` | New value | +| `old_value` | `string \| boolean \| number?` | Previous value | + +#### `Session forked` + +| Property | Type | Description | +|---|---|---| +| `task_id` | `string` | Task UUID | +| `original_session_id` | `string` | Original session ID | + +#### `Session resumed` + +| Property | Type | Description | +|---|---|---| +| `task_id` | `string` | Task UUID | +| `original_session_id` | `string` | Original session ID | + +### Terminal Events + +#### `Terminal opened` + +| Property | Type | Description | +|---|---|---| +| `task_id` | `string` | Task UUID | + +### Error Events + +#### `Agent session error` + +| Property | Type | Description | +|---|---|---| +| `task_id` | `string` | Task UUID | +| `error_type` | `string` | Error classification | + +## Agent Events + +Source: `packages/agent/src/analytics.ts` and related files. Tracked via `posthog-node`. + +All agent events include these base properties from the analytics context: + +| Property | Type | Description | +|---|---|---| +| `team` | `string` | Always `posthog-code` | +| `session_id` | `string` | Agent session ID | +| `task_id` | `string?` | Task UUID | +| `task_run_id` | `string?` | Task run UUID | +| `adapter` | `string?` | Adapter type | +| `execution_type` | `string?` | `cloud` or `local` | + +### Session Lifecycle + +#### `Session created` + +Source: `packages/agent/src/adapters/claude/claude-agent.ts` + +| Property | Type | Description | +|---|---|---| +| `model` | `string` | Resolved model ID | +| `permission_mode` | `string` | Permission mode | + +#### `Session resumed` + +Same properties as `Session created`. Tracked when `isResume` is true. + +#### `Session closed` + +Source: `packages/agent/src/adapters/base-acp-agent.ts` + +No additional properties. + +### Prompt & Model + +#### `Prompt completed` + +Source: `packages/agent/src/adapters/claude/claude-agent.ts` + +| Property | Type | Description | +|---|---|---| +| `stop_reason` | `string` | Stop reason from the model | +| `input_tokens` | `number` | Input token count | +| `output_tokens` | `number` | Output token count | +| `tool_calls_count` | `number` | Number of tool calls | + +#### `Mode changed` + +| Property | Type | Description | +|---|---|---| +| `previous_mode` | `string` | Previous mode | +| `new_mode` | `string` | New mode | + +#### `Model changed` + +| Property | Type | Description | +|---|---|---| +| `previous_model` | `string` | Previous model ID | +| `new_model` | `string` | New model ID | + +#### `Effort changed` + +| Property | Type | Description | +|---|---|---| +| `previous_effort` | `string` | Previous effort level | +| `new_effort` | `string` | New effort level | + +#### `Session compacted` + +Source: `packages/agent/src/adapters/claude/conversion/sdk-to-acp.ts` + +| Property | Type | Description | +|---|---|---| +| `pre_tokens` | `number` | Token count before compaction | + +### Tool Execution + +#### `Tool called` + +Source: `packages/agent/src/adapters/claude/conversion/sdk-to-acp.ts` + +| Property | Type | Description | +|---|---|---| +| `tool_name` | `string` | Tool name | +| `tool_kind` | `string` | Tool kind classification | + +#### `Tool completed` + +| Property | Type | Description | +|---|---|---| +| `tool_name` | `string` | Tool name | +| `tool_call_id` | `string` | Tool call ID | + +### Permission Handling + +Source: `packages/agent/src/adapters/claude/permissions/permission-handlers.ts` + +#### `Permission requested` + +| Property | Type | Description | +|---|---|---| +| `tool_name` | `string` | Tool name | +| `permission_mode` | `string` | Current permission mode | + +#### `Permission granted` + +| Property | Type | Description | +|---|---|---| +| `tool_name` | `string` | Tool name | +| `option_id` | `string` | Option ID selected | + +#### `Permission denied` + +| Property | Type | Description | +|---|---|---| +| `tool_name` | `string` | Tool name | + +#### `Permission auto allowed` + +Source: `packages/agent/src/adapters/claude/hooks.ts` + +| Property | Type | Description | +|---|---|---| +| `tool_name` | `string` | Tool name | +| `rule` | `string` | Rule that allowed it | + +#### `Permission auto denied` + +| Property | Type | Description | +|---|---|---| +| `tool_name` | `string` | Tool name | +| `rule` | `string` | Rule that denied it | + +## Mobile App + +Source: `apps/mobile/src/lib/posthog.ts` + +The mobile app uses PostHog React Native SDK with: +- Automatic screen tracking via `posthog.screen()` with `pathname` and `segments` properties +- Session replay enabled (`enableSessionReplay: true`) +- Network telemetry capture (`captureNetworkTelemetry: true`) diff --git a/packages/agent/package.json b/packages/agent/package.json index 6af51889d..c66f948ee 100644 --- a/packages/agent/package.json +++ b/packages/agent/package.json @@ -101,6 +101,7 @@ "hono": "^4.11.7", "jsonwebtoken": "^9.0.2", "minimatch": "^10.0.3", + "posthog-node": "^4.14.0", "tar": "^7.5.0", "uuid": "13.0.0", "yoga-wasm-web": "^0.3.3", diff --git a/packages/agent/src/adapters/base-acp-agent.ts b/packages/agent/src/adapters/base-acp-agent.ts index 93db4e3ee..1236b9716 100644 --- a/packages/agent/src/adapters/base-acp-agent.ts +++ b/packages/agent/src/adapters/base-acp-agent.ts @@ -16,6 +16,7 @@ import type { WriteTextFileRequest, WriteTextFileResponse, } from "@agentclientprotocol/sdk"; +import { shutdownAnalytics, trackEvent } from "../analytics"; import { DEFAULT_GATEWAY_MODEL, fetchGatewayModels, @@ -65,12 +66,14 @@ export abstract class BaseAcpAgent implements Agent { async closeSession(): Promise { try { + trackEvent("Session closed", {}); // Abort first so in-flight HTTP requests are cancelled, // otherwise interrupt() deadlocks waiting for the query to stop // while the query waits on an API call that will never abort. this.session.abortController.abort(); await this.cancel({ sessionId: this.sessionId }); this.session.settingsManager.dispose(); + await shutdownAnalytics(); this.logger.info("Closed session", { sessionId: this.sessionId }); } catch (err) { this.logger.warn("Failed to close session", { diff --git a/packages/agent/src/adapters/claude/claude-agent.ts b/packages/agent/src/adapters/claude/claude-agent.ts index 1b5925e69..7158fac61 100644 --- a/packages/agent/src/adapters/claude/claude-agent.ts +++ b/packages/agent/src/adapters/claude/claude-agent.ts @@ -44,6 +44,7 @@ import { } from "@anthropic-ai/claude-agent-sdk"; import { v7 as uuidv7 } from "uuid"; import packageJson from "../../../package.json" with { type: "json" }; +import { setAnalyticsContext, trackEvent } from "../../analytics"; import { unreachable, withTimeout } from "../../utils/common"; import { Logger } from "../../utils/logger"; import { Pushable } from "../../utils/streams"; @@ -398,6 +399,16 @@ export class ClaudeAcpAgent extends BaseAcpAgent { const result = handleResultMessage(message); if (result.error) throw result.error; + const toolCallsCount = Object.keys(this.toolUseCache).length; + trackEvent("Prompt completed", { + stop_reason: result.stopReason ?? "end_turn", + input_tokens: usage.inputTokens, + output_tokens: usage.outputTokens, + cached_read_tokens: usage.cachedReadTokens, + cached_write_tokens: usage.cachedWriteTokens, + tool_calls_count: toolCallsCount, + }); + return { stopReason: result.stopReason ?? "end_turn", usage }; } @@ -564,6 +575,7 @@ export class ClaudeAcpAgent extends BaseAcpAgent { } if (params.configId === "mode") { + const previousMode = this.session.permissionMode; await this.applySessionMode(params.value); await this.client.sessionUpdate({ sessionId: this.sessionId, @@ -572,15 +584,29 @@ export class ClaudeAcpAgent extends BaseAcpAgent { currentModeId: params.value, }, }); + trackEvent("Mode changed", { + previous_mode: previousMode, + new_mode: params.value, + }); } else if (params.configId === "model") { + const previousModel = this.session.modelId; const sdkModelId = toSdkModelId(params.value); await this.session.query.setModel(sdkModelId); this.session.modelId = params.value; this.rebuildEffortConfigOption(params.value); + trackEvent("Model changed", { + previous_model: previousModel, + new_model: params.value, + }); } else if (params.configId === "effort") { + const previousEffort = this.session.effort; const newEffort = params.value as EffortLevel; this.session.effort = newEffort; this.session.queryOptions.effort = newEffort; + trackEvent("Effort changed", { + previous_effort: previousEffort, + new_effort: newEffort, + }); } this.session.configOptions = this.session.configOptions.map((o) => @@ -783,6 +809,24 @@ export class ClaudeAcpAgent extends BaseAcpAgent { }); } + if (meta?.distinctId) { + setAnalyticsContext({ + distinctId: meta.distinctId, + sessionId, + taskId: taskId, + taskRunId: meta.taskRunId, + adapter: "claude", + executionType: meta.taskRunId ? "cloud" : "local", + }); + + trackEvent(isResume ? "Session resumed" : "Session created", { + model: resolvedModelId, + permission_mode: permissionMode, + is_resume: isResume, + is_fork: !!forkSession, + }); + } + // Resolve model: settings model takes priority, then gateway const settingsModel = settingsManager.getSettings().model; const modelOptions = await this.getModelConfigOptions(); diff --git a/packages/agent/src/adapters/claude/conversion/sdk-to-acp.ts b/packages/agent/src/adapters/claude/conversion/sdk-to-acp.ts index 21186da5c..392b26604 100644 --- a/packages/agent/src/adapters/claude/conversion/sdk-to-acp.ts +++ b/packages/agent/src/adapters/claude/conversion/sdk-to-acp.ts @@ -17,6 +17,7 @@ import type { BetaContentBlock, BetaRawContentBlockDelta, } from "@anthropic-ai/sdk/resources/beta.mjs"; +import { trackEvent } from "../../../analytics"; import { image, text } from "../../../utils/acp-content"; import { unreachable } from "../../../utils/common"; import type { Logger } from "../../../utils/logger"; @@ -71,6 +72,31 @@ export interface MessageHandlerContext { supportsTerminalOutput?: boolean; } +const TOOL_KINDS: Record = { + Read: "read", + NotebookRead: "read", + Edit: "edit", + Write: "edit", + NotebookEdit: "edit", + MultiEdit: "edit", + Bash: "execute", + BashOutput: "execute", + KillShell: "execute", + Glob: "search", + Grep: "search", + LS: "search", + WebFetch: "fetch", + WebSearch: "fetch", + Task: "think", + Agent: "think", + TodoWrite: "think", + ExitPlanMode: "switch_mode", +}; + +function toolKindFromName(name: string): string { + return TOOL_KINDS[name] ?? "other"; +} + function messageUpdateType(role: Role) { return role === "assistant" ? "agent_message_chunk" : "user_message_chunk"; } @@ -157,6 +183,15 @@ function handleToolUseChunk( return null; } + if (!alreadyCached) { + trackEvent("Tool called", { + tool_name: chunk.name, + tool_kind: toolKindFromName(chunk.name), + tool_call_id: chunk.id, + is_subagent: !!ctx.parentToolCallId, + }); + } + if (!alreadyCached && ctx.registerHooks !== false) { registerHookCallback(chunk.id, { onPostToolUseHook: async (toolUseId, _toolInput, toolResponse) => { @@ -306,6 +341,12 @@ function handleToolResultChunk( return []; } + trackEvent("Tool completed", { + tool_name: toolUse.name, + tool_call_id: chunk.tool_use_id, + success: !chunk.is_error, + }); + if (!chunk.is_error) { updateFileContentCache(toolUse, chunk, ctx); } @@ -550,6 +591,9 @@ export async function handleSystemMessage( case "init": break; case "compact_boundary": + trackEvent("Session compacted", { + pre_tokens: message.compact_metadata.pre_tokens, + }); await client.extNotification("_posthog/compact_boundary", { sessionId, trigger: message.compact_metadata.trigger, diff --git a/packages/agent/src/adapters/claude/hooks.ts b/packages/agent/src/adapters/claude/hooks.ts index d1a95e232..e408285ec 100644 --- a/packages/agent/src/adapters/claude/hooks.ts +++ b/packages/agent/src/adapters/claude/hooks.ts @@ -1,4 +1,5 @@ import type { HookCallback, HookInput } from "@anthropic-ai/claude-agent-sdk"; +import { trackEvent } from "../../analytics"; import type { Logger } from "../../utils/logger"; import type { SettingsManager } from "./session/settings"; import type { CodeExecutionMode } from "./tools"; @@ -93,6 +94,10 @@ export const createPreToolUseHook = switch (permissionCheck.decision) { case "allow": + trackEvent("Permission auto allowed", { + tool_name: toolName, + rule: permissionCheck.rule, + }); return { continue: true, hookSpecificOutput: { @@ -102,6 +107,10 @@ export const createPreToolUseHook = }, }; case "deny": + trackEvent("Permission auto denied", { + tool_name: toolName, + rule: permissionCheck.rule, + }); return { continue: true, hookSpecificOutput: { diff --git a/packages/agent/src/adapters/claude/permissions/permission-handlers.ts b/packages/agent/src/adapters/claude/permissions/permission-handlers.ts index a35b68103..cffcaae6d 100644 --- a/packages/agent/src/adapters/claude/permissions/permission-handlers.ts +++ b/packages/agent/src/adapters/claude/permissions/permission-handlers.ts @@ -3,6 +3,7 @@ import type { RequestPermissionResponse, } from "@agentclientprotocol/sdk"; import type { PermissionUpdate } from "@anthropic-ai/claude-agent-sdk"; +import { trackEvent } from "../../../analytics"; import { text } from "../../../utils/acp-content"; import type { Logger } from "../../../utils/logger"; import { toolInfoFromToolUse } from "../conversion/tool-use-to-acp"; @@ -347,6 +348,11 @@ async function handleDefaultPermissionFlow( suggestions, ); + trackEvent("Permission requested", { + tool_name: toolName, + permission_mode: session?.permissionMode, + }); + const response = await client.requestPermission({ options, sessionId, @@ -369,6 +375,14 @@ async function handleDefaultPermissionFlow( (response.outcome.optionId === "allow" || response.outcome.optionId === "allow_always") ) { + trackEvent("Permission granted", { + tool_name: toolName, + option_id: response.outcome.optionId, + option_kind: + response.outcome.optionId === "allow_always" + ? "allow_always" + : "allow_once", + }); if (response.outcome.optionId === "allow_always") { return { behavior: "allow", @@ -388,6 +402,9 @@ async function handleDefaultPermissionFlow( updatedInput: toolInput as Record, }; } else { + trackEvent("Permission denied", { + tool_name: toolName, + }); const message = "User refused permission to run tool"; await emitToolDenial(context, message); return { diff --git a/packages/agent/src/adapters/claude/types.ts b/packages/agent/src/adapters/claude/types.ts index b4ffdd17f..770e7c573 100644 --- a/packages/agent/src/adapters/claude/types.ts +++ b/packages/agent/src/adapters/claude/types.ts @@ -99,6 +99,7 @@ export type ToolUpdateMeta = { export type NewSessionMeta = { taskRunId?: string; + distinctId?: string; disableBuiltInTools?: boolean; systemPrompt?: unknown; sessionId?: string; diff --git a/packages/agent/src/analytics.ts b/packages/agent/src/analytics.ts new file mode 100644 index 000000000..47cf58fee --- /dev/null +++ b/packages/agent/src/analytics.ts @@ -0,0 +1,71 @@ +import { PostHog } from "posthog-node"; + +const POSTHOG_API_KEY = "sTMFPsFhdP1Ssg"; +const POSTHOG_HOST = "https://us.i.posthog.com"; + +let _client: PostHog | undefined; + +function getPostHogClient(): PostHog { + if (!_client) { + const apiKey = process.env.POSTHOG_ANALYTICS_API_KEY ?? POSTHOG_API_KEY; + const host = process.env.POSTHOG_ANALYTICS_HOST ?? POSTHOG_HOST; + _client = new PostHog(apiKey, { + host, + flushAt: 10, + flushInterval: 5000, + }); + } + return _client; +} + +export type AnalyticsContext = { + distinctId: string; + sessionId: string; + taskId?: string; + taskRunId?: string; + adapter?: string; + executionType?: string; +}; + +let _context: AnalyticsContext | undefined; + +export function setAnalyticsContext(context: AnalyticsContext): void { + _context = context; +} + +export function getAnalyticsContext(): AnalyticsContext | undefined { + return _context; +} + +export function trackEvent( + event: string, + properties?: Record, +): void { + const ctx = _context; + if (!ctx) return; + + try { + getPostHogClient().capture({ + distinctId: ctx.distinctId, + event, + properties: { + team: "posthog-code", + session_id: ctx.sessionId, + task_id: ctx.taskId, + task_run_id: ctx.taskRunId, + adapter: ctx.adapter, + execution_type: ctx.executionType, + ...properties, + }, + }); + } catch { + // Analytics failures should never break agent functionality + } +} + +export async function shutdownAnalytics(): Promise { + if (_client) { + await _client.shutdown(); + _client = undefined; + } +} diff --git a/packages/agent/src/server/agent-server.ts b/packages/agent/src/server/agent-server.ts index d04e21152..69473f397 100644 --- a/packages/agent/src/server/agent-server.ts +++ b/packages/agent/src/server/agent-server.ts @@ -706,7 +706,11 @@ export class AgentServer { _meta: { sessionId: payload.run_id, taskRunId: payload.run_id, + distinctId: payload.distinct_id, systemPrompt: this.buildSessionSystemPrompt(prUrl), + persistence: { + taskId: payload.task_id, + }, ...(this.config.claudeCode?.plugins?.length && { claudeCode: { options: { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ca535d348..8cc11ec67 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -622,6 +622,9 @@ importers: minimatch: specifier: ^10.0.3 version: 10.1.2 + posthog-node: + specifier: ^4.14.0 + version: 4.18.0 tar: specifier: ^7.5.0 version: 7.5.7 @@ -8908,6 +8911,10 @@ packages: posthog-js@1.340.0: resolution: {integrity: sha512-9Q/kX302HWRymQqeBSZTARR54jbeGTKxjmlAkaGwi0z3S/EOsPZycNmRq/5iHxliIBbHLl8jcDSVElHzNKUaRg==} + posthog-node@4.18.0: + resolution: {integrity: sha512-XROs1h+DNatgKh/AlIlCtDxWzwrKdYDb2mOs58n4yN8BkGN9ewqeQwG5ApS4/IzwCb7HPttUkOVulkYatd2PIw==} + engines: {node: '>=15.0.0'} + posthog-node@5.24.10: resolution: {integrity: sha512-C4ueZUrifTJMDFngybSWQ+GthcqCqPiCcGg5qnjoh+f6ie3+tdhFROqqshjttpQ6Q4DPM40USPTmU/UBYqgsbA==} engines: {node: ^20.20.0 || >=22.22.0} @@ -20478,6 +20485,12 @@ snapshots: query-selector-shadow-dom: 1.0.1 web-vitals: 5.1.0 + posthog-node@4.18.0: + dependencies: + axios: 1.13.4 + transitivePeerDependencies: + - debug + posthog-node@5.24.10: dependencies: '@posthog/core': 1.20.0 diff --git a/poems.txt b/poems.txt new file mode 100644 index 000000000..753c73e6c --- /dev/null +++ b/poems.txt @@ -0,0 +1,59 @@ +The Code That Runs at Dawn + +In circuits deep where electrons flow, +A function wakes, begins to grow. +It parses through the morning light, +Transforms the data, sets it right. + +No bug shall pass this vigilant gate, +Each test a guard, each lint a fate. +The types are strict, the logic clear, +A well-typed program knows no fear. + +--- + +Ode to a Merge Conflict + +Two branches diverged in a repo of gold, +And sorry I could not merge them both, +I sat there staring, growing old, +At chevrons pointing north and south. + +<<<<<<< mine +I wrote this line with care and grace, +======= +I wrote this line in a different place, +>>>>>>> theirs + +And so I chose, with trembling hand, +To keep them both — as I had planned. + +--- + +Haiku Collection + + Null pointer found — + the program bows gracefully, + then crashes to earth. + + Push to production. + Friday evening, lights go dark. + Monday will be fun. + + One more dependency — + node_modules grows deeper still. + My disk weeps softly. + +--- + +The Recursion + +To understand recursion, +you must first understand recursion. +And so I called myself again, +hoping this time I'd comprehend. + +The stack grew tall, the memory thin, +each frame a mirror looking in. +Until at last, the base case came — +return. And nothing was the same.