diff --git a/docs/onboarding/llm-analytics/claude-agent-sdk.tsx b/docs/onboarding/llm-analytics/claude-agent-sdk.tsx new file mode 100644 index 000000000000..2749118d3b35 --- /dev/null +++ b/docs/onboarding/llm-analytics/claude-agent-sdk.tsx @@ -0,0 +1,305 @@ +import { OnboardingComponentsContext, createInstallation } from 'scenes/onboarding/OnboardingDocsContentWrapper' + +import { StepDefinition } from '../steps' + +export const getClaudeAgentSDKSteps = (ctx: OnboardingComponentsContext): StepDefinition[] => { + const { CodeBlock, CalloutBox, Markdown, Blockquote, dedent, snippets } = ctx + + const NotableGenerationProperties = snippets?.NotableGenerationProperties + + return [ + { + title: 'Install the PostHog SDK', + badge: 'required', + content: ( + <> + Setting up analytics starts with installing the PostHog Python SDK. + + + + ), + }, + { + title: 'Install the Claude Agent SDK', + badge: 'required', + content: ( + <> + + Install the Claude Agent SDK. PostHog instruments your agent queries by wrapping the `query()` + function. The PostHog SDK **does not** proxy your calls. + + + + + + + These SDKs **do not** proxy your calls. They only fire off an async call to PostHog in the + background to send the data. You can also use LLM analytics with other SDKs or our API, but + you will need to capture the data in the right format. See the schema in the [manual capture + section](https://posthog.com/docs/llm-analytics/installation/manual-capture) for more + details. + + + + ), + }, + { + title: 'Initialize PostHog and run a query', + badge: 'required', + content: ( + <> + + Initialize PostHog with your project token and host from [your project + settings](https://app.posthog.com/settings/project), then use the PostHog `query()` wrapper as a + drop-in replacement for `claude_agent_sdk.query()`. This automatically captures + `$ai_generation`, `$ai_span`, and `$ai_trace` events. + + + ", + host="" + ) + + async def main(): + options = ClaudeAgentOptions( + max_turns=5, + permission_mode="plan", + ) + + async for message in query( + prompt="Tell me a fun fact about hedgehogs", + options=options, + posthog_client=posthog, + posthog_distinct_id="user_123", # optional + posthog_trace_id="trace_123", # optional + posthog_properties={"conversation_id": "abc123"}, # optional + posthog_groups={"company": "company_id_in_your_db"}, # optional + posthog_privacy_mode=False, # optional + ): + print(message) + + asyncio.run(main()) + posthog.shutdown() + `} + /> + +
+ + {dedent` + **Notes:** + - All original messages are yielded unchanged — the wrapper is fully transparent. + - If you want to capture LLM events anonymously, **don't** pass a distinct ID. See our docs on [anonymous vs identified events](https://posthog.com/docs/data/anonymous-vs-identified-events) to learn more. + `} + +
+ + + {dedent` + You can expect captured \`$ai_generation\` events to have the following properties: + `} + + + {NotableGenerationProperties && } + + ), + }, + { + title: 'Reusable configuration with instrument()', + badge: 'optional', + content: ( + <> + + If you make multiple `query()` calls with the same PostHog configuration, use `instrument()` to + configure once and reuse across queries. + + + ", + host="" + ) + + ph = instrument( + client=posthog, + distinct_id="user_123", + properties={"app": "my-agent"}, + ) + + options = ClaudeAgentOptions(max_turns=10) + + async def main(): + # All queries share the same PostHog config + async for msg in ph.query(prompt="Question 1", options=options): + ... + async for msg in ph.query(prompt="Question 2", options=options): + ... + + asyncio.run(main()) + `} + /> + + + {dedent` + You can override any PostHog parameter per-query: + + \`\`\`python + async for msg in ph.query( + prompt="...", + options=options, + posthog_distinct_id="different_user", + posthog_properties={"extra": "data"}, + ): + ... + \`\`\` + `} + + + ), + }, + { + title: 'Tool usage and multi-turn conversations', + badge: 'optional', + content: ( + <> + + PostHog captures the full trace hierarchy for multi-turn agent conversations with tool calls. + Each tool use is captured as an `$ai_span` event linked to its parent generation. + + + ", + host="" + ) + + options = ClaudeAgentOptions( + max_turns=10, + allowed_tools=["Read", "Glob", "Grep", "Bash"], + permission_mode="bypassPermissions", + cwd="/path/to/your/project", + ) + + async def main(): + async for message in query( + prompt="Read the README and summarize this project", + options=options, + posthog_client=posthog, + posthog_distinct_id="user_123", + ): + if isinstance(message, AssistantMessage): + for block in message.content: + if isinstance(block, TextBlock): + print(block.text) + elif isinstance(block, ToolUseBlock): + print(f"Tool: {block.name}") + + asyncio.run(main()) + posthog.shutdown() + `} + /> + + + {dedent` + This captures: + - \`$ai_generation\` events for each LLM turn (with token counts, cost, and cache metrics) + - \`$ai_span\` events for each tool use (Read, Glob, Grep, Bash, etc.) + - An \`$ai_trace\` event grouping the entire conversation with total cost and latency + `} + + + ), + }, + { + title: 'Multi-turn conversations with history', + badge: 'optional', + content: ( + <> + + For stateful, multi-turn conversations where each follow-up has full context from previous + turns, use `PostHogClaudeSDKClient`. This wraps the Claude Agent SDK's `ClaudeSDKClient` and + instruments each turn automatically. All turns share a single trace. + + + ", + host="" + ) + + options = ClaudeAgentOptions(max_turns=5) + + async with PostHogClaudeSDKClient( + options, + posthog_client=posthog, + posthog_distinct_id="user_123", + posthog_properties={"app": "my-agent"}, + ) as client: + # Turn 1 + await client.query("What is the capital of France?") + async for msg in client.receive_response(): + if isinstance(msg, AssistantMessage): + for block in msg.content: + if isinstance(block, TextBlock): + print(block.text) + + # Turn 2 — has full conversation history + await client.query("What language do they speak there?") + async for msg in client.receive_response(): + if isinstance(msg, AssistantMessage): + for block in msg.content: + if isinstance(block, TextBlock): + print(block.text) + `} + /> + + + {dedent` + Each \`receive_response()\` cycle emits \`$ai_generation\` events for that turn. When the client disconnects (exiting the \`async with\` block), a single \`$ai_trace\` event is emitted covering the entire session with aggregate latency. + `} + + + ), + }, + ] +} + +export const ClaudeAgentSDKInstallation = createInstallation(getClaudeAgentSDKSteps)