Andrew MaguireandGitHub 1875b712d2 feat(ai): add OpenAI Agents SDK integration (#408)
* feat(ai): add OpenAI Agents SDK integration

Add PostHogTracingProcessor that implements the OpenAI Agents SDK
TracingProcessor interface to capture agent traces in PostHog.

- Maps GenerationSpanData to $ai_generation events
- Maps FunctionSpanData, AgentSpanData, HandoffSpanData, GuardrailSpanData
  to $ai_span events with appropriate types
- Supports privacy mode, groups, and custom properties
- Includes instrument() helper for one-liner setup
- 22 unit tests covering all span types

* feat(openai-agents): add $ai_group_id support for linking conversation traces

- Capture group_id from trace and include as $ai_group_id on all events
- Add _get_group_id() helper to retrieve group_id from trace metadata
- Pass group_id through all span handlers (generation, function, agent, handoff, guardrail, response, custom, audio, mcp, generic)
- Enables linking multiple traces in the same conversation thread

* feat(openai-agents): add enhanced span properties

- Add $ai_total_tokens to generation and response spans (required by PostHog cost reporting)
- Add $ai_error_type for cross-provider error categorization (model_behavior_error, user_error, input_guardrail_triggered, output_guardrail_triggered, max_turns_exceeded)
- Add $ai_output_choices to response spans for output content capture
- Add audio pass-through properties for voice spans:
  - first_content_at (time to first audio byte)
  - audio_input_format / audio_output_format
  - model_config
  - $ai_input for TTS text input
- Add comprehensive tests for all new properties

* Add $ai_framework property and standardize $ai_provider for OpenAI Agents

- Add $ai_framework="openai-agents" to all events for framework identification
- Standardize $ai_provider="openai" on all events (previously some used "openai_agents")
- Follows pattern from posthog-js where $ai_provider is the underlying LLM provider

* chore: bump version to 7.7.0 for OpenAI Agents SDK integration

* fix: add openai_agents package to setuptools config

Without this, the module is not included in the distribution
and users get an ImportError after pip install.

* fix: correct indentation in on_trace_start properties dict

* fix: prevent unbounded growth of span/trace tracking dicts

Add max entry limit and eviction for _span_start_times and
_trace_metadata dicts. If on_span_end or on_trace_end is never
called (e.g., due to an SDK exception), these dicts could grow
indefinitely in long-running processes.

* fix: resolve distinct_id from trace metadata in on_span_end

Previously on_span_end always called _get_distinct_id(None), which
meant callable distinct_id resolvers never received the trace object
for spans. Now the resolved distinct_id is stored at trace start and
looked up by trace_id during span end.

* refactor: extract _base_properties helper to reduce duplication

All span handlers repeated the same 6 base fields (trace_id, span_id,
parent_id, provider, framework, latency) plus the group_id conditional.
Extract into a shared helper to reduce ~100 lines of boilerplate.

* test: add missing edge case tests for openai agents processor

- test_generation_span_with_no_usage: zero tokens when usage is None
- test_generation_span_with_partial_usage: only input_tokens present
- test_error_type_categorization_by_type_field_only: type field without
  matching message content
- test_distinct_id_resolved_from_trace_for_spans: callable resolver
  uses trace context for span events
- test_eviction_of_stale_entries: memory leak prevention works

* fix: handle non-dict error_info in span error parsing

If span.error is a string instead of a dict, calling .get() would
raise AttributeError. Now falls back to str() for non-dict errors.

* style: apply ruff formatting

* style: replace lambda assignments with def (ruff E731)

* fix: restore full CHANGELOG.md history

The rebase conflict resolution accidentally truncated the changelog
to only the most recent entries. Restored all historical entries.

* fix: preserve personless mode for trace-id fallback distinct IDs

When no distinct_id is provided, _get_distinct_id falls back to
trace_id or "unknown". Since these are non-None strings, the
$process_person_profile=False check in _capture_event never fired,
creating unwanted person profiles keyed by trace IDs.

Track whether the user explicitly provided a distinct_id and use
that flag to control personless mode, matching the pattern used
by the langchain and openai integrations.

* fix: restore changelog history and fix personless mode edge cases

Two fixes from bot review:

1. CHANGELOG.md was accidentally truncated to 38 lines during rebase
   conflict resolution. Restored all 767 lines of history.

2. Personless mode now follows the same pattern as langchain/openai
   integrations: _get_distinct_id returns None when no user-provided
   ID is available, and callers set $process_person_profile=False
   before falling back to trace_id. This covers the edge case where
   a callable distinct_id returns None.

* fix: handle None token counts in generation span

Guard against input_tokens or output_tokens being None when computing
$ai_total_tokens to avoid TypeError.

* fix: check error_type_raw for all error categories

Check both error_type_raw and error_message for guardrail and
max_turns errors, consistent with how ModelBehaviorError and
UserError are already checked.

* fix: add type hints to instrument() function

* refactor: rename _safe_json to _ensure_serializable for clarity

The function validates JSON serializability and falls back to str(),
not serializes. Rename and update docstring to make the contract clear.

* refactor: emit $ai_trace at trace end instead of start

Move the $ai_trace event from on_trace_start to on_trace_end to
capture full metadata including latency, matching the LangChain
integration approach. on_trace_start now only stores metadata for
use by spans.

* style: fix ruff formatting

* fix: add TYPE_CHECKING imports for type hints in instrument()
2026-01-27 21:15:16 +00:00
2025-11-15 12:44:50 +00:00
2025-01-02 12:52:51 -05:00
2020-02-09 21:11:22 -08:00
2025-04-10 15:30:54 +01:00
2020-02-09 21:11:22 -08:00
2025-06-06 15:16:31 +03:00
2025-11-11 18:12:33 +00:00
2025-11-25 16:43:34 +01:00

PostHog Python

posthoglogo

pypi installs GitHub contributors GitHub commit activity GitHub closed issues

Please see the Python integration docs for details.

Python Version Support

SDK Version Python Versions Supported Notes
7.3.1+ 3.10, 3.11, 3.12, 3.13, 3.14 Added Python 3.14 support
7.0.0 - 7.0.1 3.10, 3.11, 3.12, 3.13 Dropped Python 3.9 support
4.0.1 - 6.x 3.9, 3.10, 3.11, 3.12, 3.13 Python 3.9+ required

Development

Testing Locally

We recommend using uv. It's super fast.

  1. Run uv venv env (creates virtual environment called "env")
    • or python3 -m venv env
  2. Run source env/bin/activate (activates the virtual environment)
  3. Run uv sync --extra dev --extra test (installs the package in develop mode, along with test dependencies)
    • or pip install -e ".[dev,test]"
  4. you have to run pre-commit install to have auto linting pre commit
  5. Run make test
  6. To run a specific test do pytest -k test_no_api_key

PostHog recommends uv so...

uv python install 3.12
uv python pin 3.12
uv venv
source env/bin/activate
uv sync --extra dev --extra test
pre-commit install
make test

Running Locally

Assuming you have a local version of PostHog running, you can run python3 example.py to see the library in action.

Releasing Versions

Updates are released automatically using GitHub Actions when version.py is updated on master. After bumping version.py in master and adding to CHANGELOG.md, the release workflow will automatically trigger and deploy the new version.

If you need to check the latest runs or manually trigger a release, you can go to our release workflow's page and dispatch it manually, using workflow from master.

Testing changes locally with the PostHog app

You can run make prep_local, and it'll create a new folder alongside the SDK repo one called posthog-python-local, which you can then import into the posthog project by changing pyproject.toml to look like this:

dependencies = [
    ...
    "posthoganalytics" #NOTE: no version number
    ...
]
...
[tools.uv.sources]
posthoganalytics = { path = "../posthog-python-local" }

This'll let you build and test SDK changes fully locally, incorporating them into your local posthog app stack. It mainly takes care of the posthog -> posthoganalytics module renaming. You'll need to re-run make prep_local each time you make a change, and re-run uv sync --active in the posthog app project.

S
Description
Hanzo tenant service — sourced from hanzoai/insights-python
Readme MIT
3.2 MiB
Languages
Python 99.6%
Makefile 0.2%
Shell 0.1%