diff --git a/posthog/__init__.py b/posthog/__init__.py index 3209de82..a2fbcfda 100644 --- a/posthog/__init__.py +++ b/posthog/__init__.py @@ -185,21 +185,42 @@ def identify_context(distinct_id: str): def set_capture_exception_code_variables_context(enabled: bool): """ - Set whether code variables are captured for the current context. + Override code-variable capture for exceptions in the current context. + + Args: + enabled: Whether exceptions captured in this context should include local + variable values from stack frames. + + Category: + Contexts """ return inner_set_capture_exception_code_variables_context(enabled) def set_code_variables_mask_patterns_context(mask_patterns: list): """ - Variable names matching these patterns will be masked with *** when capturing code variables. + Override code-variable mask patterns for exceptions in the current context. + + Args: + mask_patterns: Variable-name patterns whose values should be replaced + with ``***`` when code variables are captured. + + Category: + Contexts """ return inner_set_code_variables_mask_patterns_context(mask_patterns) def set_code_variables_ignore_patterns_context(ignore_patterns: list): """ - Variable names matching these patterns will be ignored completely when capturing code variables. + Override code-variable ignore patterns for exceptions in the current context. + + Args: + ignore_patterns: Variable-name patterns that should be omitted entirely + when code variables are captured. + + Category: + Contexts """ return inner_set_code_variables_ignore_patterns_context(ignore_patterns) @@ -237,7 +258,50 @@ def get_tags() -> Dict[str, Any]: return inner_get_tags() -"""Settings.""" +"""Settings. + +These module-level settings configure the legacy global PostHog client used by +functions such as ``posthog.capture()``. Set them before your first SDK call. +For new code, prefer creating an explicit ``Posthog``/``Client`` instance with +the corresponding constructor arguments. + +Attributes: + api_key: Project API key/token used by the global client. Required before + calling any global capture or feature flag API. + host: PostHog ingestion host. Defaults to the US ingestion endpoint when not + set. + on_error: Optional callback invoked by background consumers when event upload + fails. + debug: Enable verbose SDK logging and re-raise errors from public APIs. + send: If False, queueing succeeds but events are not sent to PostHog. + sync_mode: If True, send events synchronously instead of using background + worker threads. + disabled: If True, disable captures and API requests. Useful in tests. + personal_api_key: Personal API key used for local feature flag evaluation + and remote config payloads. + poll_interval: Seconds between local feature flag definition refreshes. + disable_geoip: Whether to disable server-side GeoIP enrichment. Defaults to + True. + feature_flags_request_timeout_seconds: Timeout in seconds for feature flag + and remote config requests. + super_properties: Properties merged into every captured event. + enable_exception_autocapture: Automatically capture uncaught exceptions. + log_captured_exceptions: Also log exceptions captured by error tracking. + before_send: Optional callback that can modify or drop events before upload. + Return ``None`` to drop an event. + enable_local_evaluation: Whether to poll feature flag definitions for local + evaluation when a personal API key is configured. + flag_definition_cache_provider: Optional external cache provider for sharing + feature flag definitions across workers. + capture_exception_code_variables: Capture local variable values on exception + stack frames. + code_variables_mask_patterns: Variable-name patterns to mask when capturing + code variables. + code_variables_ignore_patterns: Variable-name patterns to omit when capturing + code variables. + in_app_modules: Module/package prefixes treated as in-app frames in captured + exceptions. +""" api_key = None # type: Optional[str] host = None # type: Optional[str] on_error = None # type: Optional[Callable] @@ -286,7 +350,15 @@ def capture(event: str, **kwargs: Unpack[OptionalCaptureArgs]) -> Optional[str]: distinct_id: Unique identifier for the user properties: Dict of event properties timestamp: When the event occurred + uuid: Unique identifier for this event. If omitted, one is generated + and returned. groups: Dict of group types and IDs + flags: A FeatureFlagEvaluations snapshot from evaluate_flags(). The + exact values from the snapshot are attached with no extra /flags + request. + send_feature_flags: Deprecated. Prefer flags=... from + evaluate_flags(). When truthy, evaluates flags during capture and + attaches them to the event. disable_geoip: Whether to disable GeoIP lookup Details: @@ -343,21 +415,24 @@ def set(**kwargs: Unpack[OptionalSetArgs]) -> Optional[str]: """ Set properties on a user record. + Args: + **kwargs: Optional arguments including: + distinct_id: Unique identifier for the user. Falls back to the + context distinct ID; if none exists, this call does nothing. + properties: Dict of person properties to set. + timestamp: When the properties were set. + uuid: Unique identifier for this operation. If omitted, one is + generated and returned. + disable_geoip: Whether to disable GeoIP lookup. + Details: This will overwrite previous people property values. Generally operates similar to `capture`, with distinct_id being an optional argument, defaulting to the current context's distinct ID. If there is no context-level distinct ID, and no override distinct_id is passed, this function will do nothing. Context tags are folded into $set properties, so tagging the current context and then calling `set` will cause those tags to be set on the user (unlike capture, which causes them to just be set on the event). Examples: ```python # Set person properties - from posthog import capture - capture( - 'distinct_id', - event='event_name', - properties={ - '$set': {'name': 'Max Hedgehog'}, - '$set_once': {'initial_url': '/blog'} - } - ) + from posthog import set + set(distinct_id='distinct_id', properties={'name': 'Max Hedgehog'}) ``` Category: Identification @@ -370,21 +445,24 @@ def set_once(**kwargs: Unpack[OptionalSetArgs]) -> Optional[str]: """ Set properties on a user record, only if they do not yet exist. + Args: + **kwargs: Optional arguments including: + distinct_id: Unique identifier for the user. Falls back to the + context distinct ID; if none exists, this call does nothing. + properties: Dict of person properties to set only once. + timestamp: When the properties were set. + uuid: Unique identifier for this operation. If omitted, one is + generated and returned. + disable_geoip: Whether to disable GeoIP lookup. + Details: This will not overwrite previous people property values, unlike `set`. Otherwise, operates in an identical manner to `set`. Examples: ```python # Set property once - from posthog import capture - capture( - 'distinct_id', - event='event_name', - properties={ - '$set': {'name': 'Max Hedgehog'}, - '$set_once': {'initial_url': '/blog'} - } - ) + from posthog import set_once + set_once(distinct_id='distinct_id', properties={'initial_url': '/blog'}) ``` Category: @@ -490,6 +568,8 @@ def capture_exception( Args: exception: The exception to capture. If not provided, the current exception is captured via `sys.exc_info()` + **kwargs: Optional capture arguments including distinct_id, properties, + timestamp, uuid, groups, flags, send_feature_flags, and disable_geoip. Details: Capture exception is idempotent - if it is called twice with the same exception instance, only a occurrence will be tracked in posthog. This is because, generally, contexts will cause exceptions to be captured automatically. However, to ensure you track an exception, if you catch and do not re-raise it, capturing it manually is recommended, unless you are certain it will have crossed a context boundary (e.g. by existing a `with posthog.new_context():` block already). If the passed exception was raised and caught, the captured stack trace will consist of every frame between where the exception was raised and the point at which it is captured (the "traceback"). If the passed exception was never raised, e.g. if you call `posthog.capture_exception(ValueError("Some Error"))`, the stack trace captured will be the full stack trace at the moment the exception was captured. Note that heavy use of contexts will lead to truncated stack traces, as the exception will be captured by the context entered most recently, which may not be the point you catch the exception for the final time in your code. It's recommended to use contexts sparingly, for this reason. `capture_exception` takes the same set of optional arguments as `capture`. @@ -534,6 +614,7 @@ def feature_enabled( only_evaluate_locally: Whether to evaluate only locally send_feature_flag_events: Whether to send feature flag events disable_geoip: Whether to disable GeoIP lookup + device_id: Optional device ID override for experience-continuity flags Details: You can call `posthog.load_feature_flags()` before to make sure you're not doing unexpected requests. @@ -586,6 +667,7 @@ def get_feature_flag( only_evaluate_locally: Whether to evaluate only locally send_feature_flag_events: Whether to send feature flag events disable_geoip: Whether to disable GeoIP lookup + device_id: Optional device ID override for experience-continuity flags Details: `groups` are a mapping from group type to group key. So, if you have a group type of "organization" and a group key of "5", you would pass groups={"organization": "5"}. `group_properties` take the format: { group_type_name: { group_properties } }. So, for example, if you have the group type "organization" and the group key "5", with the properties name, and employee count, you'll send these as: group_properties={"organization": {"name": "PostHog", "employees": 11}}. @@ -635,6 +717,7 @@ def get_all_flags( group_properties: Group properties only_evaluate_locally: Whether to evaluate only locally disable_geoip: Whether to disable GeoIP lookup + device_id: Optional device ID override for experience-continuity flags flag_keys_to_evaluate: Optional list of flag keys to evaluate (evaluates all if None) Details: @@ -684,6 +767,17 @@ def get_feature_flag_result( - key: The flag key - reason: Why the flag was enabled/disabled + Args: + key: The feature flag key. + distinct_id: The user's distinct ID. + groups: Mapping of group type to group key. + person_properties: Person properties to use for evaluation. + group_properties: Group properties keyed by group type. + only_evaluate_locally: Whether to evaluate only locally. + send_feature_flag_events: Whether to send a $feature_flag_called event. + disable_geoip: Whether to disable GeoIP lookup. + device_id: Optional device ID override for experience-continuity flags. + Example: ```python result = posthog.get_feature_flag_result('beta-feature', 'distinct_id') @@ -719,6 +813,27 @@ def get_feature_flag_payload( disable_geoip=None, # type: Optional[bool] device_id=None, # type: Optional[str] ) -> Optional[str]: + """ + Get the payload associated with a feature flag value. + + Deprecated for new code. Prefer ``evaluate_flags()`` and + ``flags.get_flag_payload(key)`` so flag evaluation happens once per request. + + Args: + key: The feature flag key. + distinct_id: The user's distinct ID. + match_value: Optional flag value to use when selecting a payload. + groups: Mapping of group type to group key. + person_properties: Person properties to use for evaluation. + group_properties: Group properties keyed by group type. + only_evaluate_locally: Whether to evaluate only locally. + send_feature_flag_events: Whether to send a $feature_flag_called event. + disable_geoip: Whether to disable GeoIP lookup. + device_id: Optional device ID override for experience-continuity flags. + + Category: + Feature flags + """ return _proxy( "get_feature_flag_payload", key=key, @@ -743,7 +858,7 @@ def get_remote_config_payload( key: The key of the feature flag Returns: - The payload associated with the feature flag. If payload is encrypted, the return value will decrypted + The payload associated with the feature flag. If payload is encrypted, the return value will be decrypted Note: Requires personal_api_key to be set for authentication @@ -764,6 +879,26 @@ def get_all_flags_and_payloads( device_id=None, # type: Optional[str] flag_keys_to_evaluate=None, # type: Optional[list[str]] ) -> FlagsAndPayloads: + """ + Get all feature flag values and payloads for a user. + + Args: + distinct_id: The user's distinct ID. + groups: Mapping of group type to group key. + person_properties: Person properties to use for evaluation. + group_properties: Group properties keyed by group type. + only_evaluate_locally: Whether to evaluate only locally. + disable_geoip: Whether to disable GeoIP lookup. + device_id: Optional device ID override for experience-continuity flags. + flag_keys_to_evaluate: Optional list of flag keys to evaluate. Evaluates + all flags when omitted. + + Returns: + A dict with ``featureFlags`` and ``featureFlagPayloads`` entries. + + Category: + Feature flags + """ return _proxy( "get_all_flags_and_payloads", distinct_id=distinct_id, @@ -922,6 +1057,22 @@ def shutdown(): def setup() -> Client: + """ + Create or return the global PostHog client configured by module settings. + + Most applications should either instantiate ``Posthog`` directly or set + ``posthog.api_key``/other module settings before calling top-level helpers. + ``setup()`` is called automatically by global APIs such as ``capture()``. + + Returns: + The global ``Client`` instance. + + Raises: + ValueError: If ``api_key`` has not been configured. + + Category: + Initialization + """ global default_client if not default_client: if not api_key: @@ -971,4 +1122,12 @@ def _proxy(method, *args, **kwargs): class Posthog(Client): + """ + Public PostHog SDK client. + + ``Posthog`` is the customer-facing alias for ``Client`` and accepts the same + constructor arguments. Use it to create an explicit SDK instance instead of + relying on module-level global configuration. + """ + pass diff --git a/posthog/ai/anthropic/anthropic.py b/posthog/ai/anthropic/anthropic.py index 2dc28144..288eabac 100644 --- a/posthog/ai/anthropic/anthropic.py +++ b/posthog/ai/anthropic/anthropic.py @@ -104,6 +104,20 @@ def stream( posthog_groups: Optional[Dict[str, Any]] = None, **kwargs: Any, ): + """ + Stream an Anthropic message while tracking usage in PostHog. + + Args: + posthog_distinct_id: Optional distinct ID to associate with the usage event. + posthog_trace_id: Optional trace ID. Generated automatically when omitted. + posthog_properties: Additional properties to include with the usage event. + posthog_privacy_mode: Whether to redact captured input and output. + posthog_groups: Optional PostHog groups to associate with the event. + **kwargs: Arguments passed to Anthropic's ``messages.create`` API. + + Returns: + A streaming iterator yielding Anthropic events. + """ if posthog_trace_id is None: posthog_trace_id = str(uuid.uuid4()) diff --git a/posthog/ai/anthropic/anthropic_async.py b/posthog/ai/anthropic/anthropic_async.py index a7b7bf3b..9b02e35c 100644 --- a/posthog/ai/anthropic/anthropic_async.py +++ b/posthog/ai/anthropic/anthropic_async.py @@ -104,6 +104,20 @@ async def stream( posthog_groups: Optional[Dict[str, Any]] = None, **kwargs: Any, ): + """ + Stream an Anthropic message asynchronously while tracking usage in PostHog. + + Args: + posthog_distinct_id: Optional distinct ID to associate with the usage event. + posthog_trace_id: Optional trace ID. Generated automatically when omitted. + posthog_properties: Additional properties to include with the usage event. + posthog_privacy_mode: Whether to redact captured input and output. + posthog_groups: Optional PostHog groups to associate with the event. + **kwargs: Arguments passed to Anthropic's async ``messages.create`` API. + + Returns: + An async streaming iterator yielding Anthropic events. + """ if posthog_trace_id is None: posthog_trace_id = str(uuid.uuid4()) diff --git a/posthog/ai/anthropic/anthropic_providers.py b/posthog/ai/anthropic/anthropic_providers.py index 97bc7656..8b816b16 100644 --- a/posthog/ai/anthropic/anthropic_providers.py +++ b/posthog/ai/anthropic/anthropic_providers.py @@ -21,6 +21,12 @@ class AnthropicBedrock(anthropic.AnthropicBedrock): _ph_client: PostHogClient def __init__(self, posthog_client: Optional[PostHogClient] = None, **kwargs): + """ + Args: + posthog_client: If provided, events will be captured via this client + instead of the global ``posthog`` client. + **kwargs: Arguments passed to ``anthropic.AnthropicBedrock``. + """ super().__init__(**kwargs) self._ph_client = posthog_client or setup() self.messages = WrappedMessages(self) @@ -34,6 +40,12 @@ class AsyncAnthropicBedrock(anthropic.AsyncAnthropicBedrock): _ph_client: PostHogClient def __init__(self, posthog_client: Optional[PostHogClient] = None, **kwargs): + """ + Args: + posthog_client: If provided, events will be captured via this client + instead of the global ``posthog`` client. + **kwargs: Arguments passed to ``anthropic.AsyncAnthropicBedrock``. + """ super().__init__(**kwargs) self._ph_client = posthog_client or setup() self.messages = AsyncWrappedMessages(self) @@ -47,6 +59,12 @@ class AnthropicVertex(anthropic.AnthropicVertex): _ph_client: PostHogClient def __init__(self, posthog_client: Optional[PostHogClient] = None, **kwargs): + """ + Args: + posthog_client: If provided, events will be captured via this client + instead of the global ``posthog`` client. + **kwargs: Arguments passed to ``anthropic.AnthropicVertex``. + """ super().__init__(**kwargs) self._ph_client = posthog_client or setup() self.messages = WrappedMessages(self) @@ -60,6 +78,12 @@ class AsyncAnthropicVertex(anthropic.AsyncAnthropicVertex): _ph_client: PostHogClient def __init__(self, posthog_client: Optional[PostHogClient] = None, **kwargs): + """ + Args: + posthog_client: If provided, events will be captured via this client + instead of the global ``posthog`` client. + **kwargs: Arguments passed to ``anthropic.AsyncAnthropicVertex``. + """ super().__init__(**kwargs) self._ph_client = posthog_client or setup() self.messages = AsyncWrappedMessages(self) diff --git a/posthog/ai/claude_agent_sdk/client.py b/posthog/ai/claude_agent_sdk/client.py index d9dca8be..689fe3db 100644 --- a/posthog/ai/claude_agent_sdk/client.py +++ b/posthog/ai/claude_agent_sdk/client.py @@ -59,6 +59,22 @@ def __init__( posthog_privacy_mode: bool = False, posthog_groups: Optional[Dict[str, Any]] = None, ): + """ + Initialize a stateful Claude Agent SDK client with PostHog instrumentation. + + Args: + options: Claude Agent SDK options. ``include_partial_messages`` is + enabled automatically so generations can be tracked. + transport: Optional transport passed to ``ClaudeSDKClient``. + posthog_client: Optional PostHog client. Uses the default client when omitted. + posthog_distinct_id: Optional distinct ID, or a callable that resolves + one from a ``ResultMessage``. + posthog_trace_id: Optional trace ID shared across the conversation. + Generated automatically when omitted. + posthog_properties: Additional properties included on emitted AI events. + posthog_privacy_mode: Whether to redact captured inputs, outputs, and tool data. + posthog_groups: Optional PostHog groups to associate with emitted events. + """ from dataclasses import replace as dc_replace # Ensure partial messages for per-generation tracking @@ -91,9 +107,22 @@ def __init__( self._query_start = time.time() async def connect(self, prompt: Any = None) -> None: + """ + Connect the underlying Claude SDK client. + + Args: + prompt: Optional initial prompt passed to ``ClaudeSDKClient.connect``. + """ await self._client.connect(prompt) async def query(self, prompt: str, session_id: str = "default") -> None: + """ + Send a prompt to the Claude Agent SDK conversation. + + Args: + prompt: User prompt to send. + session_id: Claude Agent SDK session ID. Defaults to ``"default"``. + """ # Track the prompt as input for the next generation self._current_input = [{"role": "user", "content": prompt}] await self._client.query(prompt, session_id) @@ -203,6 +232,9 @@ async def receive_response(self): yield message async def disconnect(self) -> None: + """ + Disconnect the underlying client and emit the final PostHog trace event. + """ # Emit the trace event covering the entire session try: latency = time.time() - self._query_start @@ -241,12 +273,25 @@ async def disconnect(self) -> None: # Delegate other methods async def interrupt(self) -> None: + """Interrupt the current Claude Agent SDK operation.""" await self._client.interrupt() async def set_permission_mode(self, mode: str) -> None: + """ + Set the Claude Agent SDK permission mode. + + Args: + mode: Permission mode to pass to the underlying client. + """ await self._client.set_permission_mode(mode) async def set_model(self, model: Optional[str] = None) -> None: + """ + Set the model used by the Claude Agent SDK client. + + Args: + model: Model name, or ``None`` to use the SDK default. + """ await self._client.set_model(model) async def __aenter__(self) -> "PostHogClaudeSDKClient": diff --git a/posthog/ai/claude_agent_sdk/processor.py b/posthog/ai/claude_agent_sdk/processor.py index 38f68eb6..09d8187e 100644 --- a/posthog/ai/claude_agent_sdk/processor.py +++ b/posthog/ai/claude_agent_sdk/processor.py @@ -143,6 +143,17 @@ def __init__( groups: Optional[Dict[str, Any]] = None, properties: Optional[Dict[str, Any]] = None, ): + """ + Initialize a Claude Agent SDK query processor. + + Args: + client: Optional PostHog client. Uses the default client when omitted. + distinct_id: Optional distinct ID for emitted events, or a callable + that receives a ``ResultMessage`` and returns one. + privacy_mode: Whether to redact captured inputs, outputs, and tool data. + groups: Optional PostHog groups to associate with emitted events. + properties: Additional properties included on emitted AI events. + """ self._client = client or setup() self._distinct_id = distinct_id self._privacy_mode = privacy_mode diff --git a/posthog/ai/gemini/gemini.py b/posthog/ai/gemini/gemini.py index 27981163..dd749a27 100644 --- a/posthog/ai/gemini/gemini.py +++ b/posthog/ai/gemini/gemini.py @@ -410,6 +410,23 @@ def generate_content_stream( posthog_groups: Optional[Dict[str, Any]] = None, **kwargs: Any, ): + """ + Stream content from Gemini while tracking usage in PostHog. + + Args: + model: The Gemini model to use. + contents: Input content for generation. + posthog_distinct_id: Optional distinct ID, overriding the client default. + posthog_trace_id: Optional trace ID. Generated automatically when omitted. + posthog_properties: Additional properties merged with client defaults. + posthog_privacy_mode: Whether to redact captured input and output, + overriding the client default. + posthog_groups: Optional PostHog groups, overriding the client default. + **kwargs: Arguments passed to Gemini's ``generate_content_stream`` API. + + Returns: + A streaming iterator yielding Gemini chunks. + """ # Merge PostHog parameters distinct_id, trace_id, properties, privacy_mode, groups = ( self._merge_posthog_params( diff --git a/posthog/ai/gemini/gemini_async.py b/posthog/ai/gemini/gemini_async.py index 4fa1b219..cd2b962f 100644 --- a/posthog/ai/gemini/gemini_async.py +++ b/posthog/ai/gemini/gemini_async.py @@ -413,6 +413,23 @@ async def generate_content_stream( posthog_groups: Optional[Dict[str, Any]] = None, **kwargs: Any, ): + """ + Stream content from Gemini asynchronously while tracking usage in PostHog. + + Args: + model: The Gemini model to use. + contents: Input content for generation. + posthog_distinct_id: Optional distinct ID, overriding the client default. + posthog_trace_id: Optional trace ID. Generated automatically when omitted. + posthog_properties: Additional properties merged with client defaults. + posthog_privacy_mode: Whether to redact captured input and output, + overriding the client default. + posthog_groups: Optional PostHog groups, overriding the client default. + **kwargs: Arguments passed to Gemini's async ``generate_content_stream`` API. + + Returns: + An async streaming iterator yielding Gemini chunks. + """ # Merge PostHog parameters distinct_id, trace_id, properties, privacy_mode, groups = ( self._merge_posthog_params( diff --git a/posthog/ai/langchain/callbacks.py b/posthog/ai/langchain/callbacks.py index f8aacc6d..7e4654cc 100644 --- a/posthog/ai/langchain/callbacks.py +++ b/posthog/ai/langchain/callbacks.py @@ -157,6 +157,7 @@ def on_chain_start( metadata: Optional[Dict[str, Any]] = None, **kwargs, ): + """Record the start of a LangChain chain run for trace/span tracking.""" self._log_debug_event("on_chain_start", run_id, parent_run_id, inputs=inputs) self._set_parent_of_run(run_id, parent_run_id) self._set_trace_or_span_metadata( @@ -171,6 +172,7 @@ def on_chain_end( parent_run_id: Optional[UUID] = None, **kwargs: Any, ): + """Capture a completed LangChain chain run as a trace or span.""" self._log_debug_event("on_chain_end", run_id, parent_run_id, outputs=outputs) self._pop_run_and_capture_trace_or_span(run_id, parent_run_id, outputs) @@ -182,6 +184,7 @@ def on_chain_error( parent_run_id: Optional[UUID] = None, **kwargs: Any, ): + """Capture a failed LangChain chain run as a trace or span.""" self._log_debug_event("on_chain_error", run_id, parent_run_id, error=error) self._pop_run_and_capture_trace_or_span(run_id, parent_run_id, error) @@ -194,6 +197,7 @@ def on_chat_model_start( parent_run_id: Optional[UUID] = None, **kwargs, ): + """Record the start of a chat model run for generation tracking.""" self._log_debug_event( "on_chat_model_start", run_id, parent_run_id, messages=messages ) @@ -212,6 +216,7 @@ def on_llm_start( parent_run_id: Optional[UUID] = None, **kwargs: Any, ): + """Record the start of an LLM run for generation tracking.""" self._log_debug_event("on_llm_start", run_id, parent_run_id, prompts=prompts) self._set_parent_of_run(run_id, parent_run_id) self._set_llm_metadata(serialized, run_id, prompts, **kwargs) @@ -251,6 +256,7 @@ def on_llm_error( parent_run_id: Optional[UUID] = None, **kwargs: Any, ): + """Capture a failed LLM run as a PostHog AI generation event.""" self._log_debug_event("on_llm_error", run_id, parent_run_id, error=error) self._pop_run_and_capture_generation(run_id, parent_run_id, error) @@ -264,6 +270,7 @@ def on_tool_start( metadata: Optional[Dict[str, Any]] = None, **kwargs: Any, ) -> Any: + """Record the start of a LangChain tool run for span tracking.""" self._log_debug_event( "on_tool_start", run_id, parent_run_id, input_str=input_str ) @@ -280,6 +287,7 @@ def on_tool_end( parent_run_id: Optional[UUID] = None, **kwargs: Any, ) -> Any: + """Capture a completed LangChain tool run as a span.""" self._log_debug_event("on_tool_end", run_id, parent_run_id, output=output) self._pop_run_and_capture_trace_or_span(run_id, parent_run_id, output) @@ -292,6 +300,7 @@ def on_tool_error( tags: Optional[list[str]] = None, **kwargs: Any, ) -> Any: + """Capture a failed LangChain tool run as a span.""" self._log_debug_event("on_tool_error", run_id, parent_run_id, error=error) self._pop_run_and_capture_trace_or_span(run_id, parent_run_id, error) @@ -305,6 +314,7 @@ def on_retriever_start( metadata: Optional[Dict[str, Any]] = None, **kwargs: Any, ) -> Any: + """Record the start of a LangChain retriever run for span tracking.""" self._log_debug_event("on_retriever_start", run_id, parent_run_id, query=query) self._set_parent_of_run(run_id, parent_run_id) self._set_trace_or_span_metadata( @@ -319,6 +329,7 @@ def on_retriever_end( parent_run_id: Optional[UUID] = None, **kwargs: Any, ): + """Capture a completed LangChain retriever run as a span.""" self._log_debug_event( "on_retriever_end", run_id, parent_run_id, documents=documents ) @@ -358,6 +369,7 @@ def on_agent_finish( parent_run_id: Optional[UUID] = None, **kwargs: Any, ) -> Any: + """Capture a completed LangChain agent action as a span.""" self._log_debug_event("on_agent_finish", run_id, parent_run_id, finish=finish) self._pop_run_and_capture_trace_or_span(run_id, parent_run_id, finish) diff --git a/posthog/ai/openai/openai.py b/posthog/ai/openai/openai.py index 98657d20..90fd5399 100644 --- a/posthog/ai/openai/openai.py +++ b/posthog/ai/openai/openai.py @@ -38,9 +38,10 @@ class OpenAI(openai.OpenAI): def __init__(self, posthog_client: Optional[PostHogClient] = None, **kwargs): """ Args: - api_key: OpenAI API key. - posthog_client: If provided, events will be captured via this client instead of the global `posthog`. - **openai_config: Any additional keyword args to set on openai (e.g. organization="xxx"). + posthog_client: If provided, events will be captured via this client + instead of the global ``posthog`` client. + **kwargs: Arguments passed to ``openai.OpenAI`` such as ``api_key`` + or ``organization``. """ super().__init__(**kwargs) @@ -86,6 +87,20 @@ def create( posthog_groups: Optional[Dict[str, Any]] = None, **kwargs: Any, ): + """ + Create an OpenAI Responses API response while tracking usage in PostHog. + + Args: + posthog_distinct_id: Optional distinct ID to associate with the usage event. + posthog_trace_id: Optional trace ID. Generated automatically when omitted. + posthog_properties: Additional properties to include with the usage event. + posthog_privacy_mode: Whether to redact captured input and output. + posthog_groups: Optional PostHog groups to associate with the event. + **kwargs: Arguments passed to OpenAI's ``responses.create`` API. + + Returns: + The OpenAI response, or a streaming iterator when ``stream=True``. + """ if posthog_trace_id is None: posthog_trace_id = str(uuid.uuid4()) @@ -288,6 +303,7 @@ def __getattr__(self, name): @property def completions(self): + """Access chat completions with PostHog usage tracking.""" return WrappedCompletions(self._client, self._original.completions) @@ -311,6 +327,20 @@ def create( posthog_groups: Optional[Dict[str, Any]] = None, **kwargs: Any, ): + """ + Create an OpenAI chat completion while tracking usage in PostHog. + + Args: + posthog_distinct_id: Optional distinct ID to associate with the usage event. + posthog_trace_id: Optional trace ID. Generated automatically when omitted. + posthog_properties: Additional properties to include with the usage event. + posthog_privacy_mode: Whether to redact captured input and output. + posthog_groups: Optional PostHog groups to associate with the event. + **kwargs: Arguments passed to OpenAI's ``chat.completions.create`` API. + + Returns: + The OpenAI chat completion, or a streaming iterator when ``stream=True``. + """ if posthog_trace_id is None: posthog_trace_id = str(uuid.uuid4()) @@ -576,6 +606,7 @@ def __getattr__(self, name): @property def chat(self): + """Access beta chat APIs with PostHog usage tracking.""" return WrappedBetaChat(self._client, self._original.chat) @@ -592,6 +623,7 @@ def __getattr__(self, name): @property def completions(self): + """Access beta chat completions with PostHog usage tracking.""" return WrappedBetaCompletions(self._client, self._original.completions) @@ -615,6 +647,20 @@ def parse( posthog_groups: Optional[Dict[str, Any]] = None, **kwargs: Any, ): + """ + Parse an OpenAI beta chat completion while tracking usage in PostHog. + + Args: + posthog_distinct_id: Optional distinct ID to associate with the usage event. + posthog_trace_id: Optional trace ID. Generated automatically when omitted. + posthog_properties: Additional properties to include with the usage event. + posthog_privacy_mode: Whether to redact captured input and output. + posthog_groups: Optional PostHog groups to associate with the event. + **kwargs: Arguments passed to OpenAI's beta ``chat.completions.parse`` API. + + Returns: + The parsed response from OpenAI. + """ return call_llm_and_track_usage( posthog_distinct_id, self._client._ph_client, diff --git a/posthog/ai/openai/openai_async.py b/posthog/ai/openai/openai_async.py index 99b7e410..cb25e138 100644 --- a/posthog/ai/openai/openai_async.py +++ b/posthog/ai/openai/openai_async.py @@ -40,10 +40,10 @@ class AsyncOpenAI(openai.AsyncOpenAI): def __init__(self, posthog_client: Optional[PostHogClient] = None, **kwargs): """ Args: - api_key: OpenAI API key. - posthog_client: If provided, events will be captured via this client instead - of the global posthog. - **openai_config: Any additional keyword args to set on openai (e.g. organization="xxx"). + posthog_client: If provided, events will be captured via this client + instead of the global ``posthog`` client. + **kwargs: Arguments passed to ``openai.AsyncOpenAI`` such as + ``api_key`` or ``organization``. """ super().__init__(**kwargs) @@ -90,6 +90,20 @@ async def create( posthog_groups: Optional[Dict[str, Any]] = None, **kwargs: Any, ): + """ + Create an OpenAI Responses API response while tracking usage in PostHog. + + Args: + posthog_distinct_id: Optional distinct ID to associate with the usage event. + posthog_trace_id: Optional trace ID. Generated automatically when omitted. + posthog_properties: Additional properties to include with the usage event. + posthog_privacy_mode: Whether to redact captured input and output. + posthog_groups: Optional PostHog groups to associate with the event. + **kwargs: Arguments passed to OpenAI's async ``responses.create`` API. + + Returns: + The OpenAI response, or an async streaming iterator when ``stream=True``. + """ if posthog_trace_id is None: posthog_trace_id = str(uuid.uuid4()) @@ -318,6 +332,7 @@ def __getattr__(self, name): @property def completions(self): + """Access async chat completions with PostHog usage tracking.""" return WrappedCompletions(self._client, self._original.completions) @@ -341,6 +356,20 @@ async def create( posthog_groups: Optional[Dict[str, Any]] = None, **kwargs: Any, ): + """ + Create an OpenAI chat completion while tracking usage in PostHog. + + Args: + posthog_distinct_id: Optional distinct ID to associate with the usage event. + posthog_trace_id: Optional trace ID. Generated automatically when omitted. + posthog_properties: Additional properties to include with the usage event. + posthog_privacy_mode: Whether to redact captured input and output. + posthog_groups: Optional PostHog groups to associate with the event. + **kwargs: Arguments passed to OpenAI's async ``chat.completions.create`` API. + + Returns: + The OpenAI chat completion, or an async streaming iterator when ``stream=True``. + """ if posthog_trace_id is None: posthog_trace_id = str(uuid.uuid4()) @@ -636,6 +665,7 @@ def __getattr__(self, name): @property def chat(self): + """Access async beta chat APIs with PostHog usage tracking.""" return WrappedBetaChat(self._client, self._original.chat) @@ -653,6 +683,7 @@ def __getattr__(self, name): @property def completions(self): + """Access async beta chat completions with PostHog usage tracking.""" return WrappedBetaCompletions(self._client, self._original.completions) @@ -677,6 +708,20 @@ async def parse( posthog_groups: Optional[Dict[str, Any]] = None, **kwargs: Any, ): + """ + Parse an OpenAI beta chat completion while tracking usage in PostHog. + + Args: + posthog_distinct_id: Optional distinct ID to associate with the usage event. + posthog_trace_id: Optional trace ID. Generated automatically when omitted. + posthog_properties: Additional properties to include with the usage event. + posthog_privacy_mode: Whether to redact captured input and output. + posthog_groups: Optional PostHog groups to associate with the event. + **kwargs: Arguments passed to OpenAI's async beta ``chat.completions.parse`` API. + + Returns: + The parsed response from OpenAI. + """ return await call_llm_and_track_usage_async( posthog_distinct_id, self._client._ph_client, diff --git a/posthog/ai/openai/openai_providers.py b/posthog/ai/openai/openai_providers.py index 9d1d1a6d..d86d4998 100644 --- a/posthog/ai/openai/openai_providers.py +++ b/posthog/ai/openai/openai_providers.py @@ -31,10 +31,10 @@ class AzureOpenAI(openai.AzureOpenAI): def __init__(self, posthog_client: Optional[PostHogClient] = None, **kwargs): """ Args: - api_key: Azure OpenAI API key. - posthog_client: If provided, events will be captured via this client instead - of the global posthog. - **openai_config: Any additional keyword args to set on Azure OpenAI (e.g. azure_endpoint="xxx"). + posthog_client: If provided, events will be captured via this client + instead of the global ``posthog`` client. + **kwargs: Arguments passed to ``openai.AzureOpenAI`` such as + ``api_key``, ``azure_endpoint``, or ``api_version``. """ super().__init__(**kwargs) self._ph_client = posthog_client or setup() @@ -69,10 +69,10 @@ class AsyncAzureOpenAI(openai.AsyncAzureOpenAI): def __init__(self, posthog_client: Optional[PostHogClient] = None, **kwargs): """ Args: - api_key: Azure OpenAI API key. - posthog_client: If provided, events will be captured via this client instead - of the global posthog. - **openai_config: Any additional keyword args to set on Azure OpenAI (e.g. azure_endpoint="xxx"). + posthog_client: If provided, events will be captured via this client + instead of the global ``posthog`` client. + **kwargs: Arguments passed to ``openai.AsyncAzureOpenAI`` such as + ``api_key``, ``azure_endpoint``, or ``api_version``. """ super().__init__(**kwargs) self._ph_client = posthog_client or setup() diff --git a/posthog/ai/otel/exporter.py b/posthog/ai/otel/exporter.py index d25e0981..bd2bd996 100644 --- a/posthog/ai/otel/exporter.py +++ b/posthog/ai/otel/exporter.py @@ -53,15 +53,34 @@ def __init__( ) def export(self, spans: Sequence[ReadableSpan]) -> SpanExportResult: + """ + Export AI-related spans to PostHog and drop non-AI spans. + + Args: + spans: Readable OpenTelemetry spans to filter and export. + + Returns: + The OpenTelemetry export result. + """ ai_spans = [span for span in spans if is_ai_span(span)] if not ai_spans: return SpanExportResult.SUCCESS return self._exporter.export(ai_spans) def shutdown(self) -> None: + """Shut down the underlying OTLP exporter.""" self._exporter.shutdown() def force_flush(self, timeout_millis: Optional[int] = None) -> bool: + """ + Flush pending spans from the underlying OTLP exporter. + + Args: + timeout_millis: Optional flush timeout in milliseconds. + + Returns: + True if the flush succeeded within the timeout, False otherwise. + """ if timeout_millis is not None: return self._exporter.force_flush(timeout_millis) return self._exporter.force_flush() diff --git a/posthog/ai/otel/processor.py b/posthog/ai/otel/processor.py index bcad8715..1a1f7bb7 100644 --- a/posthog/ai/otel/processor.py +++ b/posthog/ai/otel/processor.py @@ -53,17 +53,39 @@ def __init__( self._processor = BatchSpanProcessor(exporter) def on_start(self, span: Span, parent_context: Optional[Context] = None) -> None: + """ + Handle span start notifications. + + This processor does not need to do work at span start; filtering happens + in ``on_end``. + """ pass def on_end(self, span: ReadableSpan) -> None: + """ + Export an ended span if it is AI-related. + + Args: + span: The ended OpenTelemetry span. + """ if not is_ai_span(span): return self._processor.on_end(span) def shutdown(self) -> None: + """Shut down the underlying batch span processor.""" self._processor.shutdown() def force_flush(self, timeout_millis: Optional[int] = None) -> bool: + """ + Flush pending spans from the underlying batch span processor. + + Args: + timeout_millis: Optional flush timeout in milliseconds. + + Returns: + True if the flush succeeded within the timeout, False otherwise. + """ if timeout_millis is not None: return self._processor.force_flush(timeout_millis) return self._processor.force_flush() diff --git a/posthog/client.py b/posthog/client.py index dbf95fb9..dc1c6f8c 100644 --- a/posthog/client.py +++ b/posthog/client.py @@ -211,9 +211,59 @@ def __init__( Initialize a new PostHog client instance. Args: - project_api_key: The project API key. - host: The host to use for the client. - debug: Whether to enable debug mode. + project_api_key: PostHog project API key/token. + host: PostHog host. Defaults to the US ingestion endpoint when not + set. App hosts such as ``https://us.posthog.com`` are mapped to + the corresponding ingestion host. + debug: Enable verbose SDK logging and re-raise errors from public + API methods. + max_queue_size: Maximum number of events buffered before upload. + send: If False, queueing succeeds but events are not sent. + on_error: Optional callback invoked by background consumers when an + upload fails. + flush_at: Number of queued events that triggers a batch upload. + flush_interval: Maximum seconds a background consumer waits before + flushing a partial batch. + gzip: Whether to gzip event upload payloads. + max_retries: Number of upload retries for background consumers. + sync_mode: If True, send each event synchronously instead of using + background worker threads. + timeout: HTTP request timeout in seconds for event uploads. + thread: Number of background consumer threads. + poll_interval: Seconds between local feature flag definition refreshes. + personal_api_key: Personal API key used for local feature flag + evaluation and remote config payloads. + disabled: If True, disable captures and API requests. Useful in tests. + disable_geoip: Whether to disable server-side GeoIP enrichment. + Defaults to True. + historical_migration: Mark events as historical migration imports. + feature_flags_request_timeout_seconds: Timeout in seconds for feature + flag and remote config requests. + super_properties: Properties merged into every captured event. + enable_exception_autocapture: Automatically capture uncaught + exceptions. + log_captured_exceptions: Also log exceptions captured by error + tracking. + project_root: Root path used to determine in-app stack frames for + captured exceptions. Defaults to the current working directory. + privacy_mode: For AI observability, capture usage metadata without + prompt inputs or outputs. + before_send: Optional callback that can modify or drop events before + upload. Return ``None`` to drop an event. + flag_fallback_cache_url: Optional feature flag fallback cache URL, + such as ``memory://local/?ttl=300&size=10000`` or a Redis URL. + enable_local_evaluation: Whether to poll feature flag definitions for + local evaluation when a personal API key is configured. + flag_definition_cache_provider: Optional external cache provider for + sharing feature flag definitions across workers. + capture_exception_code_variables: Capture local variable values on + exception stack frames. + code_variables_mask_patterns: Variable-name patterns to mask when + capturing code variables. + code_variables_ignore_patterns: Variable-name patterns to omit when + capturing code variables. + in_app_modules: Module/package prefixes treated as in-app frames in + captured exceptions. Examples: ```python @@ -608,7 +658,12 @@ def capture( timestamp: The timestamp of the event. uuid: A unique identifier for the event. groups: A dictionary of group information. - send_feature_flags: Whether to send feature flags with the event. + flags: A FeatureFlagEvaluations snapshot from evaluate_flags(). The + exact values from the snapshot are attached with no extra /flags + request. + send_feature_flags: Deprecated. Prefer flags=... from + evaluate_flags(). When truthy, evaluates flags during capture and + attaches them to the event. disable_geoip: Whether to disable GeoIP for this event. Examples: @@ -2186,6 +2241,24 @@ def _capture_feature_flag_called_if_needed( reported_flags.add(feature_flag_reported_key) def get_remote_config_payload(self, key: str): + """ + Get the payload for a remote config feature flag. + + Args: + key: The remote config feature flag key. + + Returns: + The payload associated with the feature flag, or ``None`` if the + client is disabled, no personal API key is configured, or the request + fails. Encrypted payloads are decrypted by PostHog before being + returned. + + Note: + Requires ``personal_api_key`` for authentication. + + Category: + Feature flags + """ if self.disabled: return None @@ -2717,6 +2790,16 @@ def _initialize_flag_cache(self, cache_url): return None def feature_flag_definitions(self): + """ + Return feature flag definitions loaded for local evaluation. + + Returns: + The currently loaded feature flag definitions, or ``None`` before + local evaluation has loaded definitions. + + Category: + Feature flags + """ return self.feature_flags def _add_local_person_and_group_properties( diff --git a/posthog/integrations/django.py b/posthog/integrations/django.py index d0cca181..98dc8faa 100644 --- a/posthog/integrations/django.py +++ b/posthog/integrations/django.py @@ -55,11 +55,15 @@ def _get_sanitized_tracing_header(request, header_name) -> Optional[str]: class PosthogContextMiddleware: """Middleware to automatically track Django requests. - This middleware wraps all calls with a posthog context. It attempts to extract the following from the request headers: + This middleware wraps all calls with a posthog context. It attempts to extract the following from the request: - Session ID, (extracted from `X-POSTHOG-SESSION-ID`) - - Distinct ID, (extracted from `X-POSTHOG-DISTINCT-ID`) - - Request URL as $current_url - - Request Method as $request_method + - Distinct ID, (extracted from `X-POSTHOG-DISTINCT-ID`, falling back to the authenticated request user ID) + - Authenticated user email as `email` + - Request URL as `$current_url` + - Request method as `$request_method` + - Request path as `$request_path` + - Forwarded IP address as `$ip` + - User agent as `$user_agent` The context will also auto-capture exceptions and send them to PostHog, unless you disable it by setting `POSTHOG_MW_CAPTURE_EXCEPTIONS` to `False` in your Django settings. The exceptions are captured using the @@ -88,6 +92,13 @@ class PosthogContextMiddleware: def __init__(self, get_response): # type: (Union[Callable[[HttpRequest], HttpResponse], Callable[[HttpRequest], Awaitable[HttpResponse]]]) -> None + """ + Initialize the middleware with Django's next handler. + + Args: + get_response: The next middleware or view handler in Django's + middleware chain. May be synchronous or asynchronous. + """ self.get_response = get_response self._is_coroutine = iscoroutinefunction(get_response) diff --git a/posthog/request.py b/posthog/request.py index 37dd70e2..76f0a9fe 100644 --- a/posthog/request.py +++ b/posthog/request.py @@ -147,7 +147,14 @@ def reset_sessions() -> None: def set_socket_options(socket_options: Optional[SocketOptions]) -> None: """ - Configure socket options for all HTTP connections. + Configure socket options for all SDK HTTP connections. + + Call this during initialization, before making API requests. Pass ``None`` + to reset to the default socket behavior. + + Args: + socket_options: A list of ``(level, option, value)`` tuples accepted by + urllib3/``socket.setsockopt()``, or ``None`` to reset defaults. Example: from posthog import set_socket_options @@ -162,12 +169,23 @@ def set_socket_options(socket_options: Optional[SocketOptions]) -> None: def enable_keep_alive() -> None: - """Enable TCP keepalive to prevent idle connections from being dropped.""" + """ + Enable TCP keepalive for SDK HTTP connections. + + This helps prevent idle pooled connections from being dropped by network + infrastructure. Call during initialization, before making API requests. + """ set_socket_options(KEEP_ALIVE_SOCKET_OPTIONS) def disable_connection_reuse() -> None: - """Disable connection reuse, creating a fresh connection for each request.""" + """ + Disable HTTP connection reuse for SDK requests. + + Each request will create a fresh connection. This can avoid issues with + environments that terminate pooled connections, but adds per-request + overhead. Call during initialization, before making API requests. + """ global _pooling_enabled _pooling_enabled = False diff --git a/posthog/types.py b/posthog/types.py index 3b6a392d..1906fe9b 100644 --- a/posthog/types.py +++ b/posthog/types.py @@ -11,16 +11,24 @@ # Type alias for the send_feature_flags parameter class SendFeatureFlagsOptions(TypedDict, total=False): - """Options for sending feature flags with capture events. + """Options for deprecated ``capture(send_feature_flags=...)`` behavior. + + Prefer passing ``flags=posthog.evaluate_flags(...)`` to ``capture()`` for new + code. Args: - only_evaluate_locally: Whether to only use local evaluation for feature flags. - If True, only flags that can be evaluated locally will be included. - If False, remote evaluation via /flags API will be used when needed. - person_properties: Properties to use for feature flag evaluation specific to this event. - These properties will be merged with any existing person properties. - group_properties: Group properties to use for feature flag evaluation specific to this event. - Format: { group_type_name: { group_properties } } + should_send: Whether feature flags should be evaluated and attached to + the event. + only_evaluate_locally: Whether to only use local evaluation for feature + flags. If True, only flags that can be evaluated locally will be + included. If False, remote evaluation via /flags API will be used + when needed. + person_properties: Properties to use for feature flag evaluation specific + to this event. These properties will be merged with any existing + person properties. + group_properties: Group properties to use for feature flag evaluation + specific to this event. Format: { group_type_name: { group_properties } } + flag_keys_filter: Optional list of flag keys to evaluate and attach. """ should_send: bool @@ -32,6 +40,14 @@ class SendFeatureFlagsOptions(TypedDict, total=False): @dataclass(frozen=True) class FlagReason: + """Reason metadata returned by the feature flag API. + + Attributes: + code: Machine-readable reason code. + condition_index: Matching condition index, when available. + description: Human-readable reason description. + """ + code: str condition_index: Optional[int] description: str @@ -49,11 +65,22 @@ def from_json(cls, resp: Any) -> Optional["FlagReason"]: @dataclass(frozen=True) class LegacyFlagMetadata: + """Legacy feature flag metadata containing only a payload.""" + payload: Any @dataclass(frozen=True) class FlagMetadata: + """Feature flag metadata returned by the feature flag API. + + Attributes: + id: Numeric feature flag ID. + payload: Payload configured for the matched flag value, if any. + version: Feature flag version. + description: Feature flag description. + """ + id: int payload: Optional[str] version: int @@ -73,6 +100,16 @@ def from_json(cls, resp: Any) -> Union["FlagMetadata", LegacyFlagMetadata]: @dataclass(frozen=True) class FeatureFlag: + """Detailed feature flag evaluation returned by the flags API. + + Attributes: + key: Feature flag key. + enabled: Whether the flag is enabled for the evaluated user or group. + variant: Variant key for multivariate flags, otherwise ``None``. + reason: Optional reason metadata explaining the result. + metadata: Payload and other metadata returned by the API. + """ + key: str enabled: bool variant: Optional[str] @@ -119,6 +156,8 @@ def from_value_and_payload( class FlagsResponse(TypedDict, total=False): + """Normalized response from the PostHog feature flags API.""" + flags: dict[str, FeatureFlag] errorsWhileComputingFlags: bool requestId: str @@ -127,6 +166,8 @@ class FlagsResponse(TypedDict, total=False): class FlagsAndPayloads(TypedDict, total=True): + """Feature flag values and payloads keyed by feature flag key.""" + featureFlags: Optional[dict[str, FlagValue]] featureFlagPayloads: Optional[dict[str, Any]]