- Remove Posthog = Insights alias from __init__.py
- Remove PosthogContextMiddleware alias from django.py
- Remove PostHogTracingProcessor alias from processor.py
- Change $lib from "posthog-python" to "insights-python"
- Change ingestion URLs from posthog.com to insights.hanzo.ai
- Rename all posthog_* kwargs to insights_* across AI wrappers
- Rename __posthog_exception_captured to __insights_exception_captured
- Rename posthog_context_stack contextvar to insights_context_stack
- Rename posthog🎏 Redis prefix to insights🎏
- Rename $$_posthog_redacted_* sentinels to $$_insights_redacted_*
- Remove POSTHOG_MW_* Django settings fallback, X-POSTHOG-* headers
- Rename Prompts(posthog=) param to Prompts(client=)
- Update APP_ENDPOINT to us.insights.hanzo.ai
- Update all tests, examples, docs, mypy config
* Add warning log when local flag evaluation called before flags loaded
When feature_enabled() is called with only_evaluate_locally=True before
flag definitions are fetched, the SDK silently returns None. This adds a
warning log so users can diagnose the issue immediately.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Move cold start warning to only fire for only_evaluate_locally=True
The warning was in _locally_evaluate_flag which runs for all flag
evaluations, including those that fall back to server-side evaluation.
Move it to the caller where only_evaluate_locally is known, so it only
fires when the caller explicitly opted out of the server fallback.
* Narrow cold start warning to only fire when flags were never fetched
Use `is None` instead of `not` to avoid firing when flags are loaded
but empty (401, 402, no personal_api_key), which already have their
own specific error logs.
* add changeset
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* fix(llma): use distinct_id from outer context if not provided
* fix(llma): distinct_id from context is now explicitly passed to capture method
* fix(llma): fix $process_person_profile with outer context distinct_id, add tests
- Fix personless check to consider outer context distinct_id (not just the
explicit param), so events from users who set distinct_id via outer context
are not incorrectly marked as personless.
- Fix typo: "district_id" -> "distinct_id" in comments.
- Add test coverage for distinct_id resolution: no id (personless), explicit
param, outer context, and explicit overriding outer context.
* chore: add sampo changeset for distinct_id context fix
* style: ruff format
---------
Co-authored-by: Andrew Maguire <andrewm4894@gmail.com>
* feat: add semver targeting support to local flag evaluation
Implement 9 semver comparison operators (semver_eq, semver_neq, semver_gt, semver_gte, semver_lt, semver_lte, semver_tilde, semver_caret, semver_wildcard) for feature flag local evaluation. Uses regex-based parsing that matches the server-side sortableSemver behavior to handle v-prefix, whitespace, pre-release suffixes, and non-standard version formats.
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
* fix: guard against ReDoS in semver regex parsing
Add input length limit before regex search to prevent polynomial
backtracking on adversarial input (CodeQL py/polynomial-redos).
* fix: replace regex with string parsing to resolve ReDoS warning
Replace SEMVER_EXTRACT_RE regex with simple string splitting to
eliminate nested quantifiers that CodeQL flagged as polynomial-redos.
* refactor: inline semver operator tuple to match existing patterns
* refactor: add PROPERTY_OPERATORS constant for match_property
Extract all operator strings into a single source-of-truth tuple and
validate against it early in match_property, replacing the fallthrough
at the end of the function.
* refactor: split PROPERTY_OPERATORS into composable sub-groups
Break the flat tuple into category-specific tuples (EQUALITY_OPERATORS,
STRING_OPERATORS, etc.) that compose into PROPERTY_OPERATORS via
concatenation. The semver dispatch code now references
SEMVER_OPERATORS and SEMVER_COMPARISON_OPERATORS instead of
repeating the full operator lists inline.
* fix: add unreachable fallthrough to satisfy mypy return check
* add release
---------
Co-authored-by: Claude Haiku 4.5 <noreply@anthropic.com>
* feat: add semver targeting support to local flag evaluation
Implement 9 semver comparison operators (semver_eq, semver_neq, semver_gt, semver_gte, semver_lt, semver_lte, semver_tilde, semver_caret, semver_wildcard) for feature flag local evaluation. Uses regex-based parsing that matches the server-side sortableSemver behavior to handle v-prefix, whitespace, pre-release suffixes, and non-standard version formats.
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
* fix: guard against ReDoS in semver regex parsing
Add input length limit before regex search to prevent polynomial
backtracking on adversarial input (CodeQL py/polynomial-redos).
* fix: replace regex with string parsing to resolve ReDoS warning
Replace SEMVER_EXTRACT_RE regex with simple string splitting to
eliminate nested quantifiers that CodeQL flagged as polynomial-redos.
* refactor: inline semver operator tuple to match existing patterns
* add changeset
---------
Co-authored-by: Claude Haiku 4.5 <noreply@anthropic.com>
* feat: add $ai_tokens_source property to detect token value overrides
When users pass token properties (e.g. $ai_input_tokens) via
posthog_properties, these override the SDK-computed values. This new
$ai_tokens_source property ("sdk" or "passthrough") lets us distinguish
whether token values came from the SDK or were externally injected,
which is critical for diagnosing cost calculation discrepancies.
* chore: bump version to 7.9.4
* chore: add changelog entry for 7.9.4
* chore: fix ruff formatting
* chore: remove unused pytest import
* fix: Avoid setting dynamic version
Version is now fixed because of sampo, so we can get rid of this
* feat: add changeset
* docs: Add new RELEASING section to README
* chore: Migrate releases to `sampo`
This is much closer to what we have in `posthog-js`, let's see if it's a good thing!
There's still a lot to do before deploying this:
- updating CI
- updating README with instructions
* Add sampo changeset
* chore: Update to relase Python via Slack + sampo
* Update release.yml
* fix: Use pyproject.toml version as source of truth
* feat: Support device_id as bucketing identifier for local evaluation
Add support for `bucketing_identifier` field on feature flags to allow
using `device_id` instead of `distinct_id` for hashing/bucketing in
local evaluation.
* feat: limit max number of items in collection to scan
* feat: changelog
* fix: format
* feat: test
* feat: replace entire collection instead of truncating
* fix: Retry on 408 and respect Retry-After header
408 (Request Timeout) was incorrectly treated as a non-retryable client
error. Retry-After response headers were ignored during backoff. Replace
backoff library usage with a manual retry loop that honours Retry-After
when present and falls back to exponential backoff otherwise.
* fix: Parse HTTP-date Retry-After values
Retry-After can be seconds or an HTTP-date per RFC 7231. Fall back to
email.utils.parsedate_to_datetime when the numeric parse fails.
* fix: Don't retry on unclassifiable APIError status
When APIError.status is "N/A" (no HTTP status), treat it as
non-retryable to avoid unexpected retry loops on errors the SDK
cannot classify.
* test: Add retry delay tests for Retry-After and exponential backoff
Verify time.sleep is called with the Retry-After value when present,
uses exponential backoff (2^attempt) when absent, and that 408 is
retried.
* feat(llma): add prompt management
* chore(llma): bump version
* fix(llma): use SDK session with retry logic for prompt fetching
Use _get_session() from posthog/request.py instead of raw requests.get()
to benefit from the SDK's existing retry configuration on transient
network failures.
* feat: pass raw provider usage metadata for backend cost calculations
Add raw_usage field to TokenUsage type to capture raw provider usage metadata (OpenAI, Anthropic, Gemini). This enables the backend to extract modality-specific token counts (text vs image vs audio) for accurate cost calculations.
- Add raw_usage field to TokenUsage TypedDict
- Update all provider converters to capture raw usage:
- OpenAI: capture response.usage and chunk usage
- Anthropic: capture usage from message_start and message_delta events
- Gemini: capture usage_metadata from responses and chunks
- Pass raw usage as $ai_usage property in PostHog events
- Update merge_usage_stats to handle raw_usage in both modes
- Add tests verifying $ai_usage is captured for all providers
Backend will extract provider-specific details and delete $ai_usage after processing to avoid bloating properties.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* fix: add serialize_raw_usage helper to ensure JSON serializability
Address PR review feedback from @andrewm4894:
1. **Serialization**: Add serialize_raw_usage() helper with fallback chain:
- .model_dump() for Pydantic models (OpenAI/Anthropic)
- .to_dict() for protobuf-like objects
- vars() for simple objects
- str() as last resort
This ensures we never pass unserializable objects to PostHog client.
2. **Data loss prevention**: Change from replacing to merging raw_usage in
incremental mode. For Anthropic streaming, message_start has input token
details and message_delta has output token details - merging preserves
both instead of losing input data.
3. **Test coverage**: Enhanced tests to verify:
- JSON serializability with json.dumps()
- Expected structure of raw_usage dicts
- Coverage for both non-streaming and streaming modes
- Fixed Gemini test mocks to return proper dicts from model_dump()
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* refactor: move raw_usage serialization from utils to converters
Address PR feedback from @andrewm4894 - serialize in converters, not utils.
**Problem:**
Utils was receiving raw Pydantic/protobuf objects and serializing them,
which meant provider-specific knowledge leaked into generic code.
**Solution:**
Move serialization into converters where provider context exists:
Converters (NEW):
- OpenAI: serialize_raw_usage(response.usage) → dict
- Anthropic: serialize_raw_usage(event.usage) → dict
- Gemini: serialize_raw_usage(metadata) → dict
Utils (SIMPLIFIED):
- Just passes dicts through, no serialization needed
- Merge operations work with dicts only
**Benefits:**
1. Type correctness: raw_usage is always Dict[str, Any]
2. Separation of concerns: converters handle provider formats
3. Fail fast: serialization errors in converters with context
4. Cleaner abstraction: utils doesn't know about Pydantic/protobuf
**Flow:**
Provider object → Converter serializes → dict → Utils → PostHog
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* fix: add type annotation for current_raw to satisfy mypy
Fix mypy error: "Need type annotation for 'current_raw'"
Extract value first, then apply explicit type annotation with ternary
conditional to satisfy mypy's type checker.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
* 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()
* feat: add device_id to flags request payload
Add device_id parameter to all feature flag methods, similar to how
distinct_id is handled. The device_id is included in the flags request
payload sent to the server.
- Add device_id parameter to Client methods and module-level functions
- Add context support via set_context_device_id() for automatic fallback
- Add tests for explicit device_id and context-based device_id
- Bump version to 7.6.0
* docs: add Python version support table to README
Add a table documenting which SDK versions introduced or dropped
support for different Python versions, based on CHANGELOG.md entries.
* fix: correct Python 3.14 support version to 7.4.3
* Apply suggestions from code review
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>