Skip to content

Commit 5a8383a

Browse files
committed
feat(ai): add Claude Agent SDK integration for LLM analytics
Add posthog.ai.claude_agent_sdk module that wraps claude_agent_sdk.query() to automatically emit $ai_generation, $ai_span, and $ai_trace events. - PostHogClaudeAgentProcessor with _GenerationTracker that reconstructs per-turn generation metrics from Anthropic StreamEvents - Two entry points: query() drop-in replacement and instrument() for configure-once reuse - Two-slot input tracking to correctly associate tool results with subsequent generations despite SDK message ordering - All instrumentation wrapped in try/except so PostHog errors never interrupt the underlying Claude Agent SDK query - 16 unit tests covering generation, multi-turn, fallback, tool spans, traces, privacy mode, personless mode, custom properties - Example scripts (simple_query.py, instrument_reuse.py)
1 parent 795ee41 commit 5a8383a

13 files changed

Lines changed: 1599 additions & 0 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
pypi/posthog: minor
3+
---
4+
5+
feat(ai): add Claude Agent SDK integration for LLM analytics
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
POSTHOG_API_KEY=phc_your_project_api_key
2+
POSTHOG_HOST=https://us.i.posthog.com
3+
ANTHROPIC_API_KEY=sk-ant-your_api_key
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# Claude Agent SDK + PostHog AI Examples
2+
3+
Track Claude Agent SDK calls with PostHog.
4+
5+
## Setup
6+
7+
```bash
8+
pip install -r requirements.txt
9+
cp .env.example .env
10+
# Fill in your API keys in .env
11+
```
12+
13+
## Examples
14+
15+
- **simple_query.py** - Single query using the `query()` drop-in replacement
16+
- **instrument_reuse.py** - Configure-once with `instrument()`, reuse across multiple queries
17+
18+
## Run
19+
20+
```bash
21+
source .env
22+
python simple_query.py
23+
python instrument_reuse.py
24+
```
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
"""Claude Agent SDK with instrument() for reusable config, tracked by PostHog."""
2+
3+
import asyncio
4+
import os
5+
6+
from claude_agent_sdk import ClaudeAgentOptions, AssistantMessage, TextBlock
7+
from posthog import Posthog
8+
from posthog.ai.claude_agent_sdk import instrument
9+
10+
posthog = Posthog(
11+
os.environ["POSTHOG_API_KEY"],
12+
host=os.environ.get("POSTHOG_HOST", "https://us.i.posthog.com"),
13+
)
14+
15+
# Configure once, reuse for multiple queries
16+
ph = instrument(
17+
client=posthog,
18+
distinct_id="example-user",
19+
properties={"app": "demo", "environment": "development"},
20+
)
21+
22+
23+
async def ask(prompt: str) -> None:
24+
print(f"\n> {prompt}")
25+
options = ClaudeAgentOptions(
26+
max_turns=2,
27+
permission_mode="plan",
28+
)
29+
30+
async for message in ph.query(prompt=prompt, options=options):
31+
if isinstance(message, AssistantMessage):
32+
for block in message.content:
33+
if isinstance(block, TextBlock):
34+
print(f" {block.text}")
35+
36+
37+
async def main():
38+
await ask("What is the capital of France? Reply in one sentence.")
39+
await ask("What is 15% of 280? Reply in one sentence.")
40+
41+
42+
asyncio.run(main())
43+
posthog.shutdown()
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
posthog>=7.9.12
2+
claude-agent-sdk
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
"""Claude Agent SDK simple query, tracked by PostHog."""
2+
3+
import asyncio
4+
import os
5+
6+
from claude_agent_sdk import ClaudeAgentOptions
7+
from posthog import Posthog
8+
from posthog.ai.claude_agent_sdk import query
9+
10+
posthog = Posthog(
11+
os.environ["POSTHOG_API_KEY"],
12+
host=os.environ.get("POSTHOG_HOST", "https://us.i.posthog.com"),
13+
)
14+
15+
16+
async def main():
17+
options = ClaudeAgentOptions(
18+
max_turns=2,
19+
permission_mode="plan",
20+
)
21+
22+
async for message in query(
23+
prompt="What is 2 + 2? Reply in one sentence.",
24+
options=options,
25+
posthog_client=posthog,
26+
posthog_distinct_id="example-user",
27+
posthog_properties={"example": "simple_query"},
28+
):
29+
print(f"[{type(message).__name__}]")
30+
31+
32+
asyncio.run(main())
33+
posthog.shutdown()
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
exclude-newer = "7 days"
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
from __future__ import annotations
2+
3+
from typing import TYPE_CHECKING, Any, Callable, Dict, Optional, Union
4+
5+
if TYPE_CHECKING:
6+
from claude_agent_sdk.types import ClaudeAgentOptions, ResultMessage
7+
8+
from posthog.client import Client
9+
10+
try:
11+
import claude_agent_sdk # noqa: F401
12+
except ImportError:
13+
raise ModuleNotFoundError(
14+
"Please install the Claude Agent SDK to use this feature: 'pip install claude-agent-sdk'"
15+
)
16+
17+
from posthog.ai.claude_agent_sdk.processor import PostHogClaudeAgentProcessor
18+
19+
__all__ = ["PostHogClaudeAgentProcessor", "instrument", "query"]
20+
21+
22+
def instrument(
23+
client: Optional[Client] = None,
24+
distinct_id: Optional[Union[str, Callable[[ResultMessage], Optional[str]]]] = None,
25+
privacy_mode: bool = False,
26+
groups: Optional[Dict[str, Any]] = None,
27+
properties: Optional[Dict[str, Any]] = None,
28+
) -> PostHogClaudeAgentProcessor:
29+
"""
30+
Create a PostHog-instrumented query wrapper for the Claude Agent SDK.
31+
32+
Returns a PostHogClaudeAgentProcessor whose .query() method is a drop-in
33+
replacement for claude_agent_sdk.query() that automatically emits
34+
$ai_generation, $ai_span, and $ai_trace events.
35+
36+
Args:
37+
client: Optional PostHog client instance. If not provided, uses the default client.
38+
distinct_id: Optional distinct ID to associate with all events.
39+
Can also be a callable that takes a ResultMessage and returns a distinct ID.
40+
privacy_mode: If True, redacts sensitive information in tracking.
41+
groups: Optional PostHog groups to associate with events.
42+
properties: Optional additional properties to include with all events.
43+
44+
Returns:
45+
PostHogClaudeAgentProcessor: A processor whose .query() method wraps claude_agent_sdk.query().
46+
47+
Example:
48+
```python
49+
from posthog.ai.claude_agent_sdk import instrument
50+
51+
ph = instrument(distinct_id="my-app", properties={"env": "prod"})
52+
53+
async for message in ph.query(prompt="Hello", options=options):
54+
print(message)
55+
```
56+
"""
57+
return PostHogClaudeAgentProcessor(
58+
client=client,
59+
distinct_id=distinct_id,
60+
privacy_mode=privacy_mode,
61+
groups=groups,
62+
properties=properties,
63+
)
64+
65+
66+
async def query(
67+
*,
68+
prompt: Any,
69+
options: Optional[ClaudeAgentOptions] = None,
70+
transport: Any = None,
71+
posthog_client: Optional[Client] = None,
72+
posthog_distinct_id: Optional[Union[str, Callable[[ResultMessage], Optional[str]]]] = None,
73+
posthog_trace_id: Optional[str] = None,
74+
posthog_properties: Optional[Dict[str, Any]] = None,
75+
posthog_privacy_mode: bool = False,
76+
posthog_groups: Optional[Dict[str, Any]] = None,
77+
):
78+
"""
79+
Drop-in replacement for claude_agent_sdk.query() with PostHog instrumentation.
80+
81+
All original messages are yielded unchanged. PostHog events ($ai_generation,
82+
$ai_span, $ai_trace) are emitted automatically.
83+
84+
Args:
85+
prompt: The prompt (same as claude_agent_sdk.query)
86+
options: ClaudeAgentOptions (same as claude_agent_sdk.query)
87+
transport: Optional transport (same as claude_agent_sdk.query)
88+
posthog_client: Optional PostHog client instance.
89+
posthog_distinct_id: Optional distinct ID for this query.
90+
posthog_trace_id: Optional trace ID (auto-generated if not provided).
91+
posthog_properties: Extra properties to include with all events.
92+
posthog_privacy_mode: If True, redacts sensitive content.
93+
posthog_groups: Optional PostHog groups.
94+
95+
Example:
96+
```python
97+
from posthog.ai.claude_agent_sdk import query
98+
99+
async for message in query(
100+
prompt="Hello",
101+
options=options,
102+
posthog_distinct_id="my-app",
103+
posthog_properties={"pr_number": 123},
104+
):
105+
print(message)
106+
```
107+
"""
108+
processor = PostHogClaudeAgentProcessor(
109+
client=posthog_client,
110+
distinct_id=posthog_distinct_id,
111+
privacy_mode=posthog_privacy_mode,
112+
groups=posthog_groups,
113+
properties={},
114+
)
115+
116+
async for message in processor.query(
117+
prompt=prompt,
118+
options=options,
119+
transport=transport,
120+
posthog_trace_id=posthog_trace_id,
121+
posthog_properties=posthog_properties,
122+
posthog_privacy_mode=posthog_privacy_mode,
123+
posthog_groups=posthog_groups,
124+
):
125+
yield message

0 commit comments

Comments
 (0)