Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .sampo/changesets/ai-integrations-require-capture-ai.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
pypi/posthog: major
---

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.
2 changes: 1 addition & 1 deletion docs/migration-7.x-to-8.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ These changes follow from this order:

- `capture_ai` and every built-in AI integration send to `/i/v1/ai/events`. The AI endpoint accepts events up to 8 MiB.
- `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.
- 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.
- 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.
- Tests that pass a `Mock` client to an AI integration must assert on `mock.capture_ai`, not `mock.capture`.
- 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.
- 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`.
Expand Down
4 changes: 2 additions & 2 deletions posthog/ai/anthropic/anthropic.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
ToolInProgress as ToolInProgress,
)
from ..utils import (
_resolve_ai_client,
call_llm_and_track_usage as call_llm_and_track_usage,
merge_usage_stats as merge_usage_stats,
)
Expand All @@ -29,7 +30,6 @@
handle_anthropic_tool_delta as handle_anthropic_tool_delta,
)
from posthog.client import Client as PostHogClient
from posthog import setup


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


Expand Down
4 changes: 2 additions & 2 deletions posthog/ai/anthropic/anthropic_async.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,14 @@
import uuid
from typing import Any, Dict, List, Optional

from posthog import setup
from ..stream import AsyncStreamWrapper as AsyncStreamWrapper
from ..types import (
StreamingContentBlock as StreamingContentBlock,
TokenUsage as TokenUsage,
ToolInProgress as ToolInProgress,
)
from ..utils import (
_resolve_ai_client,
call_llm_and_track_usage_async as call_llm_and_track_usage_async,
merge_usage_stats as merge_usage_stats,
)
Expand Down Expand Up @@ -46,7 +46,7 @@ def __init__(self, posthog_client: Optional[PostHogClient] = None, **kwargs):
**kwargs: Additional arguments passed to the Anthropic client
"""
super().__init__(**kwargs)
self._ph_client = posthog_client or setup()
self._ph_client = _resolve_ai_client(posthog_client)
self.messages = AsyncWrappedMessages(self)


Expand Down
10 changes: 5 additions & 5 deletions posthog/ai/anthropic/anthropic_providers.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@

from posthog.ai.anthropic.anthropic import WrappedMessages
from posthog.ai.anthropic.anthropic_async import AsyncWrappedMessages
from posthog.ai.utils import _resolve_ai_client
from posthog.client import Client as PostHogClient
from posthog import setup


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


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


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


Expand All @@ -85,5 +85,5 @@ def __init__(self, posthog_client: Optional[PostHogClient] = None, **kwargs):
**kwargs: Arguments passed to ``anthropic.AsyncAnthropicVertex``.
"""
super().__init__(**kwargs)
self._ph_client = posthog_client or setup()
self._ph_client = _resolve_ai_client(posthog_client)
self.messages = AsyncWrappedMessages(self)
4 changes: 2 additions & 2 deletions posthog/ai/claude_agent_sdk/processor.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,14 @@
"Please install the Claude Agent SDK to use this feature: 'pip install claude-agent-sdk'"
)

from posthog import setup
from posthog.ai.claude_agent_sdk.formatting import (
format_assistant_blocks,
format_tool_result_content,
)
from posthog.ai.media import ensure_serializable as _ensure_serializable
from posthog.ai.utils import (
_capture_processor_event,
_resolve_ai_client,
finalize_ai_content,
with_privacy_mode as _with_privacy_mode,
)
Expand Down Expand Up @@ -167,7 +167,7 @@ def __init__(
groups: Optional PostHog groups to associate with emitted events.
properties: Additional properties included on emitted AI events.
"""
self._client = client or setup()
self._client = _resolve_ai_client(client)
self._distinct_id = distinct_id
self._privacy_mode = privacy_mode
self._groups = groups or {}
Expand Down
13 changes: 2 additions & 11 deletions posthog/ai/gemini/_shared.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@
"Please install the Google Gemini SDK to use this feature: 'pip install google-genai'"
)

from ... import setup
from ...client import Client as PostHogClient
from ..types import StreamingEventData, TokenUsage
from ..utils import (
_capture_ai_event,
_resolve_ai_client,
capture_streaming_event,
finalize_ai_content,
merge_system_prompt,
Expand All @@ -27,15 +27,6 @@
_GEMINI_BASE_URL = "https://generativelanguage.googleapis.com"


def _resolve_posthog_client(
posthog_client: Optional[PostHogClient],
) -> PostHogClient:
client = posthog_client or setup()
if client is None:
raise ValueError("posthog_client is required for PostHog tracking")
return client


def _build_gemini_client_args(
*,
api_key: Optional[str],
Expand Down Expand Up @@ -138,7 +129,7 @@ def _initialize_policy(
posthog_groups: Optional[Dict[str, Any]],
provider_client: Optional[Any] = None,
) -> None:
self._ph_client = _resolve_posthog_client(posthog_client)
self._ph_client = _resolve_ai_client(posthog_client)
self._default_distinct_id = posthog_distinct_id
self._default_properties = posthog_properties or {}
self._default_privacy_mode = posthog_privacy_mode
Expand Down
4 changes: 2 additions & 2 deletions posthog/ai/gemini/gemini.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from ..types import StreamingEventData as StreamingEventData
from ..types import TokenUsage
from ..utils import (
_resolve_ai_client,
call_llm_and_track_usage,
capture_streaming_event as capture_streaming_event,
finalize_ai_content as finalize_ai_content,
Expand All @@ -17,7 +18,6 @@
_GeminiAioNamespace,
_GeminiModelsPolicy,
_build_gemini_client,
_resolve_posthog_client,
)
from .gemini_async import AsyncModels
from .gemini_converter import (
Expand Down Expand Up @@ -92,7 +92,7 @@ def __init__(
**kwargs: Additional arguments (for future compatibility)
"""

self._ph_client = _resolve_posthog_client(posthog_client)
self._ph_client = _resolve_ai_client(posthog_client)

# Built once here and shared by every surface, so a client that uses both
# `models` and `aio.models` still opens a single provider client.
Expand Down
4 changes: 2 additions & 2 deletions posthog/ai/gemini/gemini_async.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from ..types import StreamingEventData as StreamingEventData
from ..types import TokenUsage
from ..utils import (
_resolve_ai_client,
call_llm_and_track_usage_async,
capture_streaming_event as capture_streaming_event,
finalize_ai_content as finalize_ai_content,
Expand All @@ -18,7 +19,6 @@
_GeminiAioNamespace,
_GeminiModelsPolicy,
_build_gemini_client,
_resolve_posthog_client,
)
from .gemini_converter import (
extract_gemini_content_from_chunk,
Expand Down Expand Up @@ -87,7 +87,7 @@ def __init__(
**kwargs: Additional arguments (for future compatibility)
"""

self._ph_client = _resolve_posthog_client(posthog_client)
self._ph_client = _resolve_ai_client(posthog_client)

# Built once here and shared by every surface, so `models`, `aio.models`
# and `files` all go through a single provider client.
Expand Down
4 changes: 2 additions & 2 deletions posthog/ai/langchain/callbacks.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,11 @@
from langchain_core.outputs import ChatGeneration, LLMResult
from pydantic import BaseModel

from posthog import setup
from posthog.ai.gateway import warn_if_posthog_ai_gateway
from posthog.ai.utils import (
_capture_ai_event,
_extract_cache_creation_ttl_breakdown,
_resolve_ai_client,
finalize_ai_content,
get_model_params,
_responses_stop_reason,
Expand Down Expand Up @@ -145,7 +145,7 @@ def __init__(
privacy_mode: Whether to redact the input and output of the trace.
groups: Optional additional PostHog groups to use for the trace.
"""
self._ph_client = client or setup()
self._ph_client = _resolve_ai_client(client)
self._distinct_id = distinct_id
self._trace_id = trace_id
self._properties = properties or {}
Expand Down
2 changes: 1 addition & 1 deletion posthog/ai/openai/_embeddings.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ def _capture_embedding_event(
**(properties or {}),
}

if hasattr(posthog_client, "capture"):
if callable(getattr(posthog_client, "capture_ai", None)):
_capture_ai_event(
posthog_client,
"$ai_embedding",
Expand Down
4 changes: 2 additions & 2 deletions posthog/ai/openai/openai.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
)

from posthog.ai.utils import (
_resolve_ai_client,
call_llm_and_track_usage,
extract_available_tool_calls as extract_available_tool_calls,
finalize_ai_content as finalize_ai_content,
Expand All @@ -27,7 +28,6 @@
format_openai_streaming_output as _format_openai_streaming_output,
)
from posthog.client import Client as PostHogClient
from posthog import setup
from posthog.ai.openai._streaming import (
_ChatCompletionsStreamState,
_ResponsesStreamState,
Expand Down Expand Up @@ -64,7 +64,7 @@ def __init__(self, posthog_client: Optional[PostHogClient] = None, **kwargs):
"""

super().__init__(**kwargs)
self._ph_client = posthog_client or setup()
self._ph_client = _resolve_ai_client(posthog_client)

_wrap_openai_resources(self, _SYNC_RESOURCE_WRAPPERS)

Expand Down
4 changes: 2 additions & 2 deletions posthog/ai/openai/openai_async.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@
"Please install the OpenAI SDK to use this feature: 'pip install openai'"
)

from posthog import setup
from posthog.ai.utils import (
_resolve_ai_client,
call_llm_and_track_usage_async,
extract_available_tool_calls as extract_available_tool_calls,
finalize_ai_content as finalize_ai_content,
Expand Down Expand Up @@ -66,7 +66,7 @@ def __init__(self, posthog_client: Optional[PostHogClient] = None, **kwargs):
"""

super().__init__(**kwargs)
self._ph_client = posthog_client or setup()
self._ph_client = _resolve_ai_client(posthog_client)

_wrap_openai_resources(self, _ASYNC_RESOURCE_WRAPPERS)

Expand Down
6 changes: 3 additions & 3 deletions posthog/ai/openai/openai_providers.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@
)
from .wrapper_utils import _wrap_openai_resources

from posthog.ai.utils import _resolve_ai_client
from posthog.client import Client as PostHogClient
from posthog import setup


class AzureOpenAI(openai.AzureOpenAI):
Expand All @@ -49,7 +49,7 @@ def __init__(self, posthog_client: Optional[PostHogClient] = None, **kwargs):
``api_key``, ``azure_endpoint``, or ``api_version``.
"""
super().__init__(**kwargs)
self._ph_client = posthog_client or setup()
self._ph_client = _resolve_ai_client(posthog_client)

_wrap_openai_resources(self, _SYNC_RESOURCE_WRAPPERS)

Expand All @@ -76,6 +76,6 @@ def __init__(self, posthog_client: Optional[PostHogClient] = None, **kwargs):
``api_key``, ``azure_endpoint``, or ``api_version``.
"""
super().__init__(**kwargs)
self._ph_client = posthog_client or setup()
self._ph_client = _resolve_ai_client(posthog_client)

_wrap_openai_resources(self, _ASYNC_RESOURCE_WRAPPERS)
4 changes: 2 additions & 2 deletions posthog/ai/openai_agents/processor.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,11 @@
TranscriptionSpanData,
)

from posthog import setup
from posthog.ai.media import ensure_serializable as _ensure_serializable
from posthog.ai.sanitization import _full_ai_capture_enabled, _placeholder
from posthog.ai.utils import (
_capture_processor_event,
_resolve_ai_client,
finalize_ai_content,
with_privacy_mode as _with_privacy_mode,
)
Expand Down Expand Up @@ -88,7 +88,7 @@ def __init__(
groups: Optional PostHog groups to associate with all events.
properties: Optional additional properties to include with all events.
"""
self._client = client or setup()
self._client = _resolve_ai_client(client)
self._distinct_id = distinct_id
self._privacy_mode = privacy_mode
self._groups = groups or {}
Expand Down
Loading
Loading