- 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>
* feat: llma / error tracking integration
* capture all metadata in llm event
* instrument with contexts
* bump version
* indentation
* linting
* tests
* raise
* test: add exception capture integration tests for langchain
Add 6 tests covering the new LLMA + error tracking integration:
- capture_exception called on span/generation errors
- $exception_event_id added to AI events
- No capture when autocapture disabled
- AI properties passed to exception event
- Handles None return from capture_exception
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix: pass context tags to capture() for test compatibility
- Export get_tags() from posthog module
- Explicitly pass context tags to capture() in AI utils
- Fix $ai_model fallback to extract from response.model
- Fix ruff formatting in langchain test_callbacks.py
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix: disable auto-capture exceptions in LLM context
The new_context() defaults to capture_exceptions=True which would
auto-capture any exception regardless of enable_exception_autocapture
setting. This was inconsistent with LangChain callbacks which
explicitly check the setting.
Pass capture_exceptions=False to let exception handling be controlled
explicitly by the enable_exception_autocapture setting.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix: isolate LLM context with fresh=True to avoid tag inheritance
Use fresh=True to start with a clean context for each LLM call.
This avoids inheriting $ai_* tags from parent contexts which could
cause mismatched AI metadata due to the tag merge order bug in
contexts.py (parent tags incorrectly override child tags).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix: correct tag merge order so child tags take precedence
The collect_tags() method had a bug where parent tags would overwrite
child tags, despite the comment saying the opposite. This fix ensures
child context tags properly override parent tags.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* refactor: remove fresh=True now that tag merge order is fixed
With the collect_tags() bug fixed, child tags properly override parent
tags. LLM events can now inherit useful parent context tags (request_id,
user info, etc.) while still having their $ai_* tags take precedence.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* test: add test for child tags overriding parent tags
Verifies that in non-fresh contexts, child tags properly override
parent tags with the same key while still inheriting other parent tags.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* chore: add TODO for OpenAI/Anthropic/Gemini exception capture
Document that exception capture needs to be added for the direct SDK
wrappers, similar to how it's implemented in LangChain callbacks.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: David Newell <david@Mac.communityfibre.co.uk>
Co-authored-by: Andrew Maguire <andrewm4894@gmail.com>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The "Create GitHub release" step was broken in PR #386, which removed
the GITHUB_TOKEN env var from the actions/create-release action. The
action requires the token to be passed explicitly, so releases were
being published to PyPI but GitHub tags/releases were not created.
This replaces the archived actions/create-release@v1 with the gh CLI,
which is already used elsewhere in this workflow. The gh CLI properly
uses GH_TOKEN for authentication.
* fix: extract model from response for OpenAI stored prompts
When using OpenAI stored prompts, the model is defined in the OpenAI
dashboard rather than passed in the API request. This change adds a
fallback to extract the model from the response object when not
provided in kwargs.
FixesPostHog/posthog#42861
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Apply suggestion from @greptile-apps[bot]
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
* Apply suggestion from @greptile-apps[bot]
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
* test: add tests for model extraction fallback and bump to 7.4.1
- Add 8 tests covering model extraction from response for stored prompts
- Fix utils.py to add 'unknown' fallback for consistency
- Bump version to 7.4.1
- Update CHANGELOG.md
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* style: format utils.py with ruff
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix: remove 'unknown' fallback from non-streaming to match original behavior
Non-streaming originally returned None when model wasn't in kwargs.
Streaming keeps "unknown" fallback as that was the original behavior.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* test: add test for None model fallback in non-streaming
Verifies that non-streaming returns None (not "unknown") when model
is not available in kwargs or response, matching original behavior.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
* Add urllib3-based retry for feature flag requests
Use urllib3's built-in Retry mechanism for feature flag POST requests
instead of application-level retry logic. This is simpler and leverages
well-tested library code.
Key changes:
- Add `RETRY_STATUS_FORCELIST` = [408, 500, 502, 503, 504]
- Add `_build_flags_session()` with POST retries and `status_forcelist`
- Update `flags()` to use dedicated flags session
- Add tests for retry configuration and session usage
The flags session retries on:
- Network failures (connect/read errors)
- Transient server errors (408, 500, 502, 503, 504)
It does NOT retry on:
- 429 (rate limit) - need to wait, not hammer
- 402 (quota limit) - won't resolve with retries
* Make examples run without requiring personal api key
* Add integration tests for network retry behavior
Add tests that verify actual retry behavior, not just configuration:
- test_retries_on_503_then_succeeds: Spins up a local HTTP server that
returns 503 twice then 200, verifying 3 requests are made
- test_connection_errors_are_retried: Verifies connection errors trigger
retries by measuring elapsed time with backoff
Both tests use dynamically allocated ports for CI safety.
* Bump version to 7.4.0
* Add $feature_flag_error property to track flag evaluation failures
Track errors in feature flag evaluation by adding a `$feature_flag_error` property to the `$feature_flag_called` event.
* Refactor requests exception imports through request.py
Export RequestsTimeout and RequestsConnectionError from posthog/request.py
to keep all requests library imports in one place and avoid mypy issues.
* Address PR review feedback
- Fix fallback logic to only trigger on actual exceptions, not when
errors_while_computing or flag_missing is set from a successful API response
- Change log.exception() to log.warning() for expected operational errors
(quota limits, timeouts, connection errors, API errors) to reduce log noise
- Keep log.exception() only for truly unexpected errors (unknown_error)
- Extract stale cache fallback into _get_stale_flag_fallback() helper method
* Add tests for stale cache fallback and error absence
- Add TestFeatureFlagErrorWithStaleCacheFallback test class with 4 tests:
- test_timeout_error_returns_stale_cached_value
- test_connection_error_returns_stale_cached_value
- test_api_error_returns_stale_cached_value
- test_error_without_cache_returns_none
- Add negative assertions to verify $feature_flag_error is absent on success:
- test_get_feature_flag_result_boolean_local_evaluation
- test_get_feature_flag_result_variant_local_evaluation
- test_get_feature_flag_result_boolean_decide
- test_get_feature_flag_result_variant_decide
* Report combined errors when both errors_while_computing and flag_missing
When the server returns errorsWhileComputingFlags=true AND the requested
flag is not in the response, report both conditions as a comma-separated
string: "errors_while_computing_flags,flag_missing"
This provides better debugging context when both conditions occur.
* Add FeatureFlagError constants class for error type values
- Add FeatureFlagError class to types.py with constants:
- ERRORS_WHILE_COMPUTING, FLAG_MISSING, QUOTA_LIMITED
- TIMEOUT, CONNECTION_ERROR, UNKNOWN_ERROR
- api_error(status) static method for dynamic error strings
- Update client.py to use FeatureFlagError constants instead of
magic strings
- Update all tests to use constants for maintainability
This improves maintainability by:
- Single source of truth for error values
- IDE autocomplete and typo detection
- Documentation of analytics-stable values
* Remove print statements from test failure handlers
* Fix mypy type error in FeatureFlagError.api_error method
Accept Union[int, str] to match APIError.status type.
* feat: Add FlagDefinitionCacheProvider interface
* feat: Add a Redis example for FlagDefinitionCacheProvider
* style: ruff format
* style: ruff format
* refactor: clean up mypy errors
* chore: mypy-baseline sync
* fix: Type Redis as Redis[str]
* fix: Adhere to strict typing
The defined types don't leave room for missing or optional keys. We'll
use the types as they're defined.
* Add ETag support for local evaluation polling
Add support for HTTP conditional requests using ETags to reduce bandwidth
when polling for feature flag definitions. When flag definitions haven't
changed, the server returns 304 Not Modified and the SDK skips processing.
- Add GetResponse dataclass to encapsulate response data, ETag, and status
- Update get() to send If-None-Match header and handle 304 responses
- Store ETag in client and pass it on subsequent polling requests
- Skip flag processing when 304 Not Modified is received
* Use _session rather than requests
Benefits:
1. Reuses TCP connections via keep-alive
2. 2 retries on connect/read errors
3. Faster handshakes
* Add unit tests for get() function
Test HTTP-level behavior including:
- ETag extraction from response headers
- If-None-Match header sent when etag provided
- 304 Not Modified response handling
- Fallback when 304 has no ETag header
- Error response handling (APIError)
- Authorization and User-Agent headers
- Timeout and URL construction
* Add defensive null check for response.data
Guard against unexpected None data in non-304 responses to prevent
TypeError when accessing dictionary keys.
* Clear stored ETag when server stops sending one
If the server stops including ETag headers in responses, clear the
stored ETag so we don't keep sending a stale If-None-Match header.
* Mask API tokens in log messages
Keep first 10 chars visible for identification while hiding the rest.
Addresses CodeQL security warning about logging sensitive data.
* Ran ruff format
- Add 'Lint with ruff' step to CI workflow (was only running format check)
- Fix F401: Add explicit re-exports for public API types in __init__.py
- Fix F811: Rename duplicate test_openai_reasoning_tokens to test_openai_reasoning_tokens_o4_mini
- Fix E731: Convert lambda to def function in test_middleware.py
- Fix unused imports in client.py and test files (auto-fixed)
Without ruff check in CI, lint errors were accumulating on master undetected.
* fix(llma): cache cost calculation in the LangChain callback
* fix: format
* Update posthog/ai/langchain/callbacks.py
Co-authored-by: Radu Raicea <radu@raicea.com>
* Bump version to 6.7.13
Master has already released 6.7.12 with other fixes, so this PR will be 6.7.13
---------
Co-authored-by: Radu Raicea <radu@raicea.com>
Co-authored-by: Andrew Maguire <andrewm4894@gmail.com>
* fix: Add LangChain 1.0+ compatibility for CallbackHandler imports
- Use try/except to import from langchain_core first (LangChain 1.0+)
- Fall back to legacy langchain imports for older versions
- Maintains backward compatibility with LangChain 0.x
- All existing tests pass (45 passed)
Fixes#362
* test: Add regression test for AgentAction/AgentFinish imports
- Tests that AgentAction and AgentFinish can be imported
- Tests on_agent_action and on_agent_finish callbacks with mock data
- Ensures compatibility with both LangChain 0.x and 1.0+
- Catches the import issue that was previously only tested with API keys
This addresses a test coverage gap identified during code review.
* chore: Add CHANGELOG entry for LangChain 1.0+ compatibility fix
* fix: Remove unused type: ignore comments for mypy
The type: ignore comments were only needed when the except block
executes, but CI runs with LangChain 1.0+ so the try block succeeds.
Mypy flags these as unused-ignore errors.
* chore: bump version to 6.7.12 for langchain 1.0 compatibility
Restores the process_exception method that was removed in v6.7.5 (PR #328),
which broke exception capture from Django views and downstream middleware.
Django converts view exceptions into responses before they propagate through
the middleware stack's __call__ method, so the context manager's exception
handler never sees them. Django provides these exceptions via the
process_exception hook instead.
Changes:
- Add process_exception method to capture exceptions from views and downstream
middleware with proper request context and tags
- Add tests verifying process_exception behavior and settings (capture_exceptions,
request_filter)
* Add $ai_lib_metadata to AI integrations
Adds framework identification metadata to all AI events for easier filtering
and analytics. Each integration now includes a $ai_lib_metadata property with
schema version and framework name.
- LangChain: Hardcoded to "langchain"
- Native wrappers (Anthropic, OpenAI, Gemini): Uses provider name
- Ready for future frameworks (pydantic-ai, crewai, llamaindex)
This enables PostHog queries to easily distinguish between:
- Direct SDK wrapper usage
- Framework-mediated usage (LangChain, etc.)
- Different framework types
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Add \$ai_lib_metadata to sync/async paths and tests
- Added \$ai_lib_metadata to call_llm_and_track_usage (sync)
- Added \$ai_lib_metadata to call_llm_and_track_usage_async (async)
- Added test assertion in test_basic_completion
- Placed metadata at end of properties for consistency
All tests pass successfully.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Refactor: use unified utility function for $ai_lib_metadata
Creates a single `get_ai_lib_metadata(framework)` utility function to generate
the $ai_lib_metadata object, replacing inline implementations across the
codebase.
Changes:
- Add get_ai_lib_metadata() utility to utils.py
- Update LangChain callbacks to use utility function
- Update call_llm_and_track_usage() to use utility function
- Update call_llm_and_track_usage_async() to use utility function
- Update capture_streaming_event() to use utility function
Benefits:
- Consistency across all integrations
- Single source of truth for metadata structure
- Easier to extend with version detection later
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Add $ai_lib_metadata assertions to provider tests
Add missing $ai_lib_metadata assertions to Anthropic, Gemini, and LangChain tests to match the validation already present in OpenAI tests. Each test now verifies the metadata field contains the correct schema version and framework name.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Simplify to $ai_framework property, only for actual frameworks
Changes:
- Replace complex $ai_lib_metadata object with simple $ai_framework string
- Only include $ai_framework when using actual framework (LangChain)
- Remove $ai_framework from direct provider calls (OpenAI, Anthropic, Gemini)
- Update all tests to reflect new behavior
Before: {"schema": "v1", "frameworks": [{"name": "langchain"}]}
After: "langchain" (only when using LangChain framework)
This eliminates wasteful redundancy where framework=provider for direct calls.
---------
Co-authored-by: Claude <noreply@anthropic.com>
Rename workflow files from .yaml to .yml for consistency with existing
workflows (ci.yml, call-flags-project-board.yml).
This resolves naming confusion and standardizes all GitHub Actions
workflow files to use the .yml extension.
Address code review feedback and critical issues from PR #328.
Changes:
- Keep __call__ as sync method that conditionally routes to __acall__ for async paths
- Use markcoroutinefunction() to properly mark instances when async is detected
- Detect async/sync at init time via iscoroutinefunction(get_response)
- Remove process_exception method - it was non-functional (Django doesn't call it on new-style middleware without MiddlewareMixin)
- Fix markcoroutinefunction fallback to be a simple no-op instead of accessing private API
- Exception capture works correctly via contexts.new_context() which has built-in exception handling
- Add comprehensive test coverage for sync, async, and hybrid middleware behavior
- Add async exception capture tests
- Refactor tests to use proper middleware initialization
This implementation follows Django's recommended hybrid middleware pattern where
both sync_capable and async_capable are True, allowing Django to pass requests
without conversion while the middleware adapts based on the detected mode.
The sync path behavior is identical to version 6.7.4 (pre-async), ensuring perfect
backward compatibility for WSGI deployments.
Addresses #329
Related to #328
* Fix multi-condition flags with static cohorts returning wrong variants
When a feature flag has multiple conditions and one contains a static
cohort, the SDK now correctly falls back to the API instead of
evaluating subsequent conditions locally and returning incorrect variants.
Introduce RequiresServerEvaluation exception to distinguish between:
- Missing server-side data (static cohorts) → immediate API fallback
- Evaluation errors (bad regex, missing properties) → try next condition
Changes:
- Add RequiresServerEvaluation exception class
- Update match_cohort() to throw RequiresServerEvaluation for static cohorts
- Update match_property_group() to propagate RequiresServerEvaluation
- Update match_feature_flag_properties() to handle both exception types
- Update client.py to catch both exceptions for API fallback
- Export RequiresServerEvaluation in __init__.py
- Add test for multi-condition static cohort scenario
All 84 feature flag tests pass.
* Add unit test for payloads
* ruff format
* Updates script to persist references
* Workflow to generate references to a folder
* Get rid of references, to be generated
* Update .github/workflows/generate-references.yaml
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
* Update .github/workflows/generate-references.yaml
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
* Review comments
* Update .github/workflows/generate-references.yaml
* Pin hashes and only run on releases
* Pin uv
---------
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
* fix: Prevent core Client methods from raising exceptions
The goal is to ensure that our client doesn't cause a panic in an
end-user application. This change updates
capture/set/set_once/group_identify/alias to swallow and log any
exceptions that occur. Note that this won't prevent errors from
propagating via the `on_error` callback if an error occurs while
processing the queue.
* test: Remove assertions that capture raises
These tests were broken anyways. Capture would only raise because it was
being called with no arguments, not because api_key or host are None.
* fix: always capture system prompt
* chore: bump version
* fix: gemini system prompt capture
* chore: imports at top
* fix: test
The mock we were passing from this test
reporetd that it had a `system instruction` field,
breaking assumptions
* chore: lint
* fix: better code organization
* chore: lint
* fix(llmo): set the $ai_tools properly for all providers
* fix(llmo): remove privacy mode from $ai_tools
* chore(llmo): bump version
* chore(llmo): run formatter
* fix(llmo): properly set tool calls in $ai_output_choices
* chore(llmo): bump version
* chore(llmo): run formatter
* fix(llmo): fix types error
* feat(llmo): change $ai_output_choices to have an array of content
* chore(llmo): run formatter
* feat(llmo): create text type object
* chore(llmo): update CHANGELOG.md
2025-08-05 14:02:37 -04:00
Phil HaackGitHubgreptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
* session and identity in context
* bump version
* make django integration use context distinct id and session functions
* don't use self
* fix comments
* fix middleware tests
* clarify fresh and distinct id's
* Fix exported modules, add makefile command to test changes locally
* tiny fix
* add context maanager and tag functions
* Update posthog/scopes.py
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
* ran black
* black locally disagrees with ci. python is awful
* bleh
* isort
* fix mypyp thing
* Revert "fix mypyp thing"
This reverts commit 21ad8733610967cad0bbf8451508ca1189be543c.
* update baseline
* lets try again
* alright lets try again
* revert to baseline
* try ignoring it i guess
* black
* try supporting async too
* black
* mypy
* formatting
* fix changelog
* fix comment
* we only support python 3.9+
* change decorator name
* fix example
* isort
* auto-capture in with blocks
* fix tests
* black
* inherit tags by default
* assert swap
* add tags to all events
* rm comment
* Update example.py
Co-authored-by: David Newell <d.newell1@outlook.com>
---------
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
Co-authored-by: David Newell <d.newell1@outlook.com>
* init
* moved the constants
* formatting
* mypy
* fr do some damn formatting
* don't exclude posthog
* make it a set
* differentiate
* fix AI test
* use the same type everywhere
* ready to release
in e.g. lambda environments the connection can time out between invocations,
this comes through to the client as a "RemoteDisconnected" error, which it
turns out urllib classifies as a "read" error not a connection error (as
it's possible to get this error after data has been sent)
* Flesh out Decide response types
* Ensure we normalize get_decide
In a back compat manner.
* Populate feature_flags_by_key when setting feature_flags
Since `self.feature_flags_by_key` is derived from `self.feature_flags`, and we often set the latter in unit tests, but forget to set the former, our tests can be wonky.
This ensures that when we set `self.feature_flags`, we always set `self. feature_flags_by_key`
* Annotate types
* Lookup local flag by key
Fixes#121
* Refactor local flag evaluation into its own method
* Include extra details in `$feature_flag_called` events
* Fix up type annotations, tests, and formatting
* Update lib to decide v4
* Bump version and add changelog
* feat: cached tokens
* feat: add tool support
* chore: local test
* chore: isort black
* chore: bump v
* chore: remove import
* fix: types
* fix: black
* fix: mypy unpacking of None
* chore: mypy baseline
* feat: mypy fix
* fix: did things and stuff
* fix: mypy yourpy whos py?
* fix: things can be None
* fix: move test
* fix remove exampels from package
* fix: losing my py
* haha okay
* tests workin
* format
* use case-sensitive comparisons
* omg LOL
* fix tests
* jeez
* this will probably work
* now do local eval
* okay
* yo
* formatting
* fix import order
* type check
* ai yi yi
* code review
* format
* do it
* merge conflict UGH
* black formatting
* bump version
* correct changelog
2025-03-03 14:00:52 -05:00
Peter KirkhamGitHubgreptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
* haha okay
* tests workin
* format
* use case-sensitive comparisons
* omg LOL
* fix tests
* jeez
* this will probably work
* now do local eval
* okay
* yo
* formatting
* fix import order
* type check
* ai yi yi
* code review
* format
* automatically retry connection errors
from the docs for max_retries: this applies only to failed DNS lookups,
socket connections and connection timeouts
* run tests on multiple python versions
* update freezegun
* fix: Move code under mypy type
This is incorrect, we should've added these slightly lower in the method definition to avoid mypy from breaking
* feat: Bump to 3.12.1
* Move accessing variants outside of loop
`flag_variants` doesn't depend on condition so it doesn't make sense to declare it in the loop.
* Fix assertion
* Remove incorrect comment
Comment seems superfluous anyways.
* Break out of the loop when the key is found
The purpose of the loop is to loop through the flag keys and evaluate the one where `flag["key"] == key`. Once that key is found, there's no need to continue the loop.
* Complete the test
Looks like the test was missing an assert.
* Precompute valid variant keys outside loop
* feat(ai): LangChain integration v0.1
* test: langchain integration tests
* test: langchain-openai for v2 and v3
* chore: reorganize imports
* fix: ci
* fix: set python on ci to 3.9
* fix: upgrade ci for python 3.9
* fix: fallback for distinct_id
* fix: personless events for omitted distinct_ids
* fix: review comments
* feat: base url retrieval
* [FEATURE]Add distinct_id to group_identify
* [TESTS]Updated test cases for adding distinct_id to group_identify
* [LINT-FIX]client.py and test_client.py
* [CHORE]Verion bump and changelog update
* test CI
* heck it, upgrade python
* okay don't do anything silly with the cache hits i guess
* more CI upgrades :crossedfingers
* upgrade all CI to latest versions, then
* jk this is how python works
* whackamole
* what even
* yeesh
* this can't be it
* if this breaks ill kms
* dark magic dark MAGIC
* im giving up on my dreams
* this is the fix, needs tests
* fix test
* tests
* yeah
* please work
* ran the formatter
* code review feedback
* how'd this get here
* bump version add changelog
* Update CHANGELOG.md
Co-authored-by: David Newell <d.newell1@outlook.com>
---------
Co-authored-by: David Newell <d.newell1@outlook.com>
* initial try
* change init file to a class
* remove commas
* format
* sort
* restore original and add renamed client
* format
* move disabled
* format
* handle changing var for global instance
* add test
* change api error to log
* change implementation
* sort
* format
* raise when debug is true
* Update posthog/test/test_feature_flags.py
* format
---------
Co-authored-by: Neil Kakkar <neilkakkar@gmail.com>
* implementation with test
* format
* v=3 and get_all_payloads
* format
* fix bug
* format
* address comments
* format
* add tests
* format
* add resiliency
* fix lookup
* remove check
* address comments
* example.py
* format
* format
* get feature flag method
* remove groups param from method
* use personal api key
* add groups
* add capture
* black reformat
* black?
* Update posthog/client.py
Co-authored-by: Neil Kakkar <neilkakkar@gmail.com>
* add to init
* formatting
Co-authored-by: Neil Kakkar <neilkakkar@gmail.com>
* Capturing $feature_flag_called at the end of client.feature_enabled method
* Enabling multi-variants for feature flags
Co-authored-by: Utku Zihnioglu <utku@webshare.io>
Co-authored-by: Tim Glaser <tim@posthog.com>
* feat(uuid): add support for custom uuids
* fix deprecated assert
* do not send "none" uuid
* you can have it in any color you want, as long as it's black
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.