From 65538b737704851db838fbe26a770af20e3d17f6 Mon Sep 17 00:00:00 2001 From: Andrew Maguire Date: Wed, 1 Apr 2026 12:18:47 +0100 Subject: [PATCH 1/2] feat(llma): use PostHog SDK integration for stamphog LLM analytics Replace direct claude_agent_sdk.query() with posthog.ai.claude_agent_sdk.query() to automatically capture $ai_generation, $ai_span, and $ai_trace events. Falls back to plain claude_agent_sdk.query() if posthoganalytics is not installed, so stamphog continues to work without analytics as before. --- tools/pr-approval-agent/review_pr.py | 1 + tools/pr-approval-agent/reviewer.py | 22 ++++++++++++++++++++-- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/tools/pr-approval-agent/review_pr.py b/tools/pr-approval-agent/review_pr.py index d0ff6a28f43b..a6a698d4e0dd 100644 --- a/tools/pr-approval-agent/review_pr.py +++ b/tools/pr-approval-agent/review_pr.py @@ -4,6 +4,7 @@ # dependencies = [ # "claude-agent-sdk", # "anthropic", +# "posthoganalytics", # ] # /// # ruff: noqa: T201 diff --git a/tools/pr-approval-agent/reviewer.py b/tools/pr-approval-agent/reviewer.py index 865d0b715300..e8b90dbba534 100644 --- a/tools/pr-approval-agent/reviewer.py +++ b/tools/pr-approval-agent/reviewer.py @@ -12,10 +12,15 @@ import subprocess from pathlib import Path -from claude_agent_sdk import ClaudeAgentOptions, ResultMessage, query +from claude_agent_sdk import ClaudeAgentOptions, ResultMessage from claude_agent_sdk.types import AssistantMessage, ToolUseBlock from github import PRData +try: + from posthoganalytics.ai.claude_agent_sdk import query +except ImportError: + from claude_agent_sdk import query + MODEL = "claude-sonnet-4-6" @@ -212,8 +217,21 @@ async def _review(self, pr: PRData, classification: dict, gate_context: dict) -> extra_args={"no-session-persistence": None}, ) + posthog_props = { + "stamphog_pr_number": pr.number, + "stamphog_repo": pr.repo, + "stamphog_author": pr.author, + "stamphog_tier": classification.get("tier", ""), + "stamphog_verdict": gate_context.get("gate_verdict", ""), + } + structured_output = None - async for message in query(prompt=prompt, options=options): + async for message in query( + prompt=prompt, + options=options, + posthog_distinct_id="stamphog", + posthog_properties=posthog_props, + ): if self.verbose: print(f"\033[2m [{type(message).__name__}]\033[0m", flush=True) if isinstance(message, ResultMessage): From af9601790d97d16faea03f7562bda9b4a9674806 Mon Sep 17 00:00:00 2001 From: Andrew Maguire Date: Wed, 1 Apr 2026 12:29:59 +0100 Subject: [PATCH 2/2] fix(llma): only pass posthog kwargs when SDK is available The fallback claude_agent_sdk.query() does not accept posthog_* kwargs, so passing them unconditionally would raise TypeError when posthoganalytics is not installed. --- tools/pr-approval-agent/reviewer.py | 32 ++++++++++++++++------------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/tools/pr-approval-agent/reviewer.py b/tools/pr-approval-agent/reviewer.py index e8b90dbba534..0ef9f405a289 100644 --- a/tools/pr-approval-agent/reviewer.py +++ b/tools/pr-approval-agent/reviewer.py @@ -18,8 +18,12 @@ try: from posthoganalytics.ai.claude_agent_sdk import query + + _POSTHOG_AI_AVAILABLE = True except ImportError: - from claude_agent_sdk import query + from claude_agent_sdk import query # type: ignore[no-redef] + + _POSTHOG_AI_AVAILABLE = False MODEL = "claude-sonnet-4-6" @@ -217,21 +221,21 @@ async def _review(self, pr: PRData, classification: dict, gate_context: dict) -> extra_args={"no-session-persistence": None}, ) - posthog_props = { - "stamphog_pr_number": pr.number, - "stamphog_repo": pr.repo, - "stamphog_author": pr.author, - "stamphog_tier": classification.get("tier", ""), - "stamphog_verdict": gate_context.get("gate_verdict", ""), - } + posthog_kwargs: dict = {} + if _POSTHOG_AI_AVAILABLE: + posthog_kwargs = { + "posthog_distinct_id": "stamphog", + "posthog_properties": { + "stamphog_pr_number": pr.number, + "stamphog_repo": pr.repo, + "stamphog_author": pr.author, + "stamphog_tier": classification.get("tier", ""), + "stamphog_verdict": gate_context.get("gate_verdict", ""), + }, + } structured_output = None - async for message in query( - prompt=prompt, - options=options, - posthog_distinct_id="stamphog", - posthog_properties=posthog_props, - ): + async for message in query(prompt=prompt, options=options, **posthog_kwargs): if self.verbose: print(f"\033[2m [{type(message).__name__}]\033[0m", flush=True) if isinstance(message, ResultMessage):