Skip to content

Commit 585ce8b

Browse files
committed
fix(ai): fail at setup when an AI integration's client lacks capture_ai
1 parent 0c99129 commit 585ce8b

23 files changed

Lines changed: 159 additions & 81 deletions
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
pypi/posthog: major
3+
---
4+
5+
The AI integrations no longer fall back to `capture` for a client object without `capture_ai`. Creating an OpenAI, Anthropic or Gemini wrapper, the LangChain `CallbackHandler`, or the OpenAI Agents or Claude Agent SDK processor with such a client now raises `TypeError`. The error shows up when you set the integration up, and not after a provider call. A `Client` or `AsyncPosthog` instance, and the default global client, work as before.

‎docs/migration-7.x-to-8.0.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,7 @@ These changes follow from this order:
176176

177177
- `capture_ai` and every built-in AI integration send to `/i/v1/ai/events`. The AI endpoint accepts events up to 8 MiB.
178178
- `enable_full_ai_capture` now controls only content: string truncation and media redaction. It no longer chooses the endpoint. `_use_ai_lane` and `_enable_multimodal_capture` still work as aliases.
179-
- The AI integrations call your client's `capture_ai`. A client object without `capture_ai` gets `capture` calls instead. A custom client must accept the `options` keyword argument.
179+
- The AI integrations call your client's `capture_ai`, and never `capture`. A custom client object must implement `capture_ai` and accept its `options` keyword argument. If the client has no `capture_ai`, the wrapper, callback handler or tracing processor raises `TypeError` when you create it. Before, such a client got `capture` calls.
180180
- Tests that pass a `Mock` client to an AI integration must assert on `mock.capture_ai`, not `mock.capture`.
181181
- When an AI integration or MCP falls back to a trace, run or session ID, it turns person processing off with a per-event option. This is the only option the integrations pass. It wins over `super_options`, context options and a `$process_person_profile: true` property. `before_send` can still change it.
182182
- The AI integrations have no `options` argument. To set options on their events, use `set_context_option`, `super_options`, or a legacy property such as `$cookieless_mode` in `posthog_properties`.

‎posthog/ai/anthropic/anthropic.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
ToolInProgress as ToolInProgress,
1818
)
1919
from ..utils import (
20+
_resolve_ai_client,
2021
call_llm_and_track_usage as call_llm_and_track_usage,
2122
merge_usage_stats as merge_usage_stats,
2223
)
@@ -29,7 +30,6 @@
2930
handle_anthropic_tool_delta as handle_anthropic_tool_delta,
3031
)
3132
from posthog.client import Client as PostHogClient
32-
from posthog import setup
3333

3434

3535
class Anthropic(anthropic.Anthropic):
@@ -46,7 +46,7 @@ def __init__(self, posthog_client: Optional[PostHogClient] = None, **kwargs):
4646
**kwargs: Additional arguments passed to the Anthropic client
4747
"""
4848
super().__init__(**kwargs)
49-
self._ph_client = posthog_client or setup()
49+
self._ph_client = _resolve_ai_client(posthog_client)
5050
self.messages = WrappedMessages(self)
5151

5252

‎posthog/ai/anthropic/anthropic_async.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,14 @@
1010
import uuid
1111
from typing import Any, Dict, List, Optional
1212

13-
from posthog import setup
1413
from ..stream import AsyncStreamWrapper as AsyncStreamWrapper
1514
from ..types import (
1615
StreamingContentBlock as StreamingContentBlock,
1716
TokenUsage as TokenUsage,
1817
ToolInProgress as ToolInProgress,
1918
)
2019
from ..utils import (
20+
_resolve_ai_client,
2121
call_llm_and_track_usage_async as call_llm_and_track_usage_async,
2222
merge_usage_stats as merge_usage_stats,
2323
)
@@ -46,7 +46,7 @@ def __init__(self, posthog_client: Optional[PostHogClient] = None, **kwargs):
4646
**kwargs: Additional arguments passed to the Anthropic client
4747
"""
4848
super().__init__(**kwargs)
49-
self._ph_client = posthog_client or setup()
49+
self._ph_client = _resolve_ai_client(posthog_client)
5050
self.messages = AsyncWrappedMessages(self)
5151

5252

‎posthog/ai/anthropic/anthropic_providers.py‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@
99

1010
from posthog.ai.anthropic.anthropic import WrappedMessages
1111
from posthog.ai.anthropic.anthropic_async import AsyncWrappedMessages
12+
from posthog.ai.utils import _resolve_ai_client
1213
from posthog.client import Client as PostHogClient
13-
from posthog import setup
1414

1515

1616
class AnthropicBedrock(anthropic.AnthropicBedrock):
@@ -28,7 +28,7 @@ def __init__(self, posthog_client: Optional[PostHogClient] = None, **kwargs):
2828
**kwargs: Arguments passed to ``anthropic.AnthropicBedrock``.
2929
"""
3030
super().__init__(**kwargs)
31-
self._ph_client = posthog_client or setup()
31+
self._ph_client = _resolve_ai_client(posthog_client)
3232
self.messages = WrappedMessages(self)
3333

3434

@@ -47,7 +47,7 @@ def __init__(self, posthog_client: Optional[PostHogClient] = None, **kwargs):
4747
**kwargs: Arguments passed to ``anthropic.AsyncAnthropicBedrock``.
4848
"""
4949
super().__init__(**kwargs)
50-
self._ph_client = posthog_client or setup()
50+
self._ph_client = _resolve_ai_client(posthog_client)
5151
self.messages = AsyncWrappedMessages(self)
5252

5353

@@ -66,7 +66,7 @@ def __init__(self, posthog_client: Optional[PostHogClient] = None, **kwargs):
6666
**kwargs: Arguments passed to ``anthropic.AnthropicVertex``.
6767
"""
6868
super().__init__(**kwargs)
69-
self._ph_client = posthog_client or setup()
69+
self._ph_client = _resolve_ai_client(posthog_client)
7070
self.messages = WrappedMessages(self)
7171

7272

@@ -85,5 +85,5 @@ def __init__(self, posthog_client: Optional[PostHogClient] = None, **kwargs):
8585
**kwargs: Arguments passed to ``anthropic.AsyncAnthropicVertex``.
8686
"""
8787
super().__init__(**kwargs)
88-
self._ph_client = posthog_client or setup()
88+
self._ph_client = _resolve_ai_client(posthog_client)
8989
self.messages = AsyncWrappedMessages(self)

‎posthog/ai/claude_agent_sdk/processor.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,14 @@
2424
"Please install the Claude Agent SDK to use this feature: 'pip install claude-agent-sdk'"
2525
)
2626

27-
from posthog import setup
2827
from posthog.ai.claude_agent_sdk.formatting import (
2928
format_assistant_blocks,
3029
format_tool_result_content,
3130
)
3231
from posthog.ai.media import ensure_serializable as _ensure_serializable
3332
from posthog.ai.utils import (
3433
_capture_processor_event,
34+
_resolve_ai_client,
3535
finalize_ai_content,
3636
with_privacy_mode as _with_privacy_mode,
3737
)
@@ -167,7 +167,7 @@ def __init__(
167167
groups: Optional PostHog groups to associate with emitted events.
168168
properties: Additional properties included on emitted AI events.
169169
"""
170-
self._client = client or setup()
170+
self._client = _resolve_ai_client(client)
171171
self._distinct_id = distinct_id
172172
self._privacy_mode = privacy_mode
173173
self._groups = groups or {}

‎posthog/ai/gemini/_shared.py‎

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,11 @@
99
"Please install the Google Gemini SDK to use this feature: 'pip install google-genai'"
1010
)
1111

12-
from ... import setup
1312
from ...client import Client as PostHogClient
1413
from ..types import StreamingEventData, TokenUsage
1514
from ..utils import (
1615
_capture_ai_event,
16+
_resolve_ai_client,
1717
capture_streaming_event,
1818
finalize_ai_content,
1919
merge_system_prompt,
@@ -27,15 +27,6 @@
2727
_GEMINI_BASE_URL = "https://generativelanguage.googleapis.com"
2828

2929

30-
def _resolve_posthog_client(
31-
posthog_client: Optional[PostHogClient],
32-
) -> PostHogClient:
33-
client = posthog_client or setup()
34-
if client is None:
35-
raise ValueError("posthog_client is required for PostHog tracking")
36-
return client
37-
38-
3930
def _build_gemini_client_args(
4031
*,
4132
api_key: Optional[str],
@@ -138,7 +129,7 @@ def _initialize_policy(
138129
posthog_groups: Optional[Dict[str, Any]],
139130
provider_client: Optional[Any] = None,
140131
) -> None:
141-
self._ph_client = _resolve_posthog_client(posthog_client)
132+
self._ph_client = _resolve_ai_client(posthog_client)
142133
self._default_distinct_id = posthog_distinct_id
143134
self._default_properties = posthog_properties or {}
144135
self._default_privacy_mode = posthog_privacy_mode

‎posthog/ai/gemini/gemini.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from ..types import StreamingEventData as StreamingEventData
77
from ..types import TokenUsage
88
from ..utils import (
9+
_resolve_ai_client,
910
call_llm_and_track_usage,
1011
capture_streaming_event as capture_streaming_event,
1112
finalize_ai_content as finalize_ai_content,
@@ -17,7 +18,6 @@
1718
_GeminiAioNamespace,
1819
_GeminiModelsPolicy,
1920
_build_gemini_client,
20-
_resolve_posthog_client,
2121
)
2222
from .gemini_async import AsyncModels
2323
from .gemini_converter import (
@@ -92,7 +92,7 @@ def __init__(
9292
**kwargs: Additional arguments (for future compatibility)
9393
"""
9494

95-
self._ph_client = _resolve_posthog_client(posthog_client)
95+
self._ph_client = _resolve_ai_client(posthog_client)
9696

9797
# Built once here and shared by every surface, so a client that uses both
9898
# `models` and `aio.models` still opens a single provider client.

‎posthog/ai/gemini/gemini_async.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from ..types import StreamingEventData as StreamingEventData
88
from ..types import TokenUsage
99
from ..utils import (
10+
_resolve_ai_client,
1011
call_llm_and_track_usage_async,
1112
capture_streaming_event as capture_streaming_event,
1213
finalize_ai_content as finalize_ai_content,
@@ -18,7 +19,6 @@
1819
_GeminiAioNamespace,
1920
_GeminiModelsPolicy,
2021
_build_gemini_client,
21-
_resolve_posthog_client,
2222
)
2323
from .gemini_converter import (
2424
extract_gemini_content_from_chunk,
@@ -87,7 +87,7 @@ def __init__(
8787
**kwargs: Additional arguments (for future compatibility)
8888
"""
8989

90-
self._ph_client = _resolve_posthog_client(posthog_client)
90+
self._ph_client = _resolve_ai_client(posthog_client)
9191

9292
# Built once here and shared by every surface, so `models`, `aio.models`
9393
# and `files` all go through a single provider client.

‎posthog/ai/langchain/callbacks.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,11 +41,11 @@
4141
from langchain_core.outputs import ChatGeneration, LLMResult
4242
from pydantic import BaseModel
4343

44-
from posthog import setup
4544
from posthog.ai.gateway import warn_if_posthog_ai_gateway
4645
from posthog.ai.utils import (
4746
_capture_ai_event,
4847
_extract_cache_creation_ttl_breakdown,
48+
_resolve_ai_client,
4949
finalize_ai_content,
5050
get_model_params,
5151
_responses_stop_reason,
@@ -145,7 +145,7 @@ def __init__(
145145
privacy_mode: Whether to redact the input and output of the trace.
146146
groups: Optional additional PostHog groups to use for the trace.
147147
"""
148-
self._ph_client = client or setup()
148+
self._ph_client = _resolve_ai_client(client)
149149
self._distinct_id = distinct_id
150150
self._trace_id = trace_id
151151
self._properties = properties or {}

0 commit comments

Comments
 (0)