From 851cbd42bf5cb89e9a2cff9723be983fb666e479 Mon Sep 17 00:00:00 2001 From: Carlos Marchal Date: Fri, 24 Jul 2026 10:45:12 +0200 Subject: [PATCH 1/5] chore: posthoganalytics 7.29 --- ee/hogai/core/runner.py | 2 - .../core/test/test_base_callback_handlers.py | 5 -- posthog/apps.py | 14 ++-- posthog/llm/completions.py | 13 +++- posthog/ph_client.py | 44 +---------- posthog/settings/ingestion.py | 27 ------- posthog/test/test_ph_client.py | 76 ++++--------------- .../subscriptions/llm_change_summary.py | 2 +- .../temporal/vision_actions/synthesis.py | 2 +- pyproject.toml | 2 +- uv.lock | 10 +-- 11 files changed, 40 insertions(+), 157 deletions(-) diff --git a/ee/hogai/core/runner.py b/ee/hogai/core/runner.py index 7c858afcc282..9cd0771e3e09 100644 --- a/ee/hogai/core/runner.py +++ b/ee/hogai/core/runner.py @@ -37,7 +37,6 @@ from posthog.event_usage import report_user_action from posthog.models import Team, User from posthog.ph_client import get_client -from posthog.settings.ingestion import DedicatedAIEndpointRollout from posthog.sync import database_sync_to_async from posthog.utils import get_instance_region @@ -206,7 +205,6 @@ def make_client(region: str): region, flush_at=1, before_send=ai_event_truncator, - dedicated_ai_endpoint_stage=DedicatedAIEndpointRollout.RUNNER, ) # Local deployment or hobby diff --git a/ee/hogai/core/test/test_base_callback_handlers.py b/ee/hogai/core/test/test_base_callback_handlers.py index 37c6a2523756..bbfe8f9f30cb 100644 --- a/ee/hogai/core/test/test_base_callback_handlers.py +++ b/ee/hogai/core/test/test_base_callback_handlers.py @@ -6,8 +6,6 @@ import posthoganalytics from posthoganalytics.ai.langchain.callbacks import CallbackHandler -from posthog.settings.ingestion import DedicatedAIEndpointRollout - from products.posthog_ai.backend.models.assistant import Conversation from ee.hogai.chat_agent.runner import ChatAgentRunner @@ -73,7 +71,6 @@ def test_callback_handler_cloud_us_region(self, mock_get_client, mock_get_region "US", flush_at=1, before_send=ai_event_truncator, - dedicated_ai_endpoint_stage=DedicatedAIEndpointRollout.RUNNER, ) @patch("ee.hogai.core.runner.is_cloud") @@ -107,13 +104,11 @@ def get_client_side_effect(region, **kwargs): "EU", flush_at=1, before_send=ai_event_truncator, - dedicated_ai_endpoint_stage=DedicatedAIEndpointRollout.RUNNER, ) mock_get_client.assert_any_call( "US", flush_at=1, before_send=ai_event_truncator, - dedicated_ai_endpoint_stage=DedicatedAIEndpointRollout.RUNNER, ) @patch("ee.hogai.core.runner.is_cloud") diff --git a/posthog/apps.py b/posthog/apps.py index 1696bc61f83c..a450184cb6c0 100644 --- a/posthog/apps.py +++ b/posthog/apps.py @@ -9,7 +9,6 @@ from posthoganalytics.client import Client from posthog.git import get_git_branch, get_git_commit_short -from posthog.ph_client import enable_dedicated_ai_endpoint_for_default_client from posthog.utils import ( _build_flag_provider, get_available_timezones_with_offsets, @@ -73,10 +72,11 @@ def ready(self): "service": settings.OTEL_SERVICE_NAME, "environment": os.getenv("OTEL_SERVICE_ENVIRONMENT"), } - # Config for the SDK's `client.metrics` API. The pinned SDK version predates - # the metrics API and ignores this attr; once posthoganalytics is bumped to - # >=7.23 it's picked up by setup(), so metrics get a real service.name - # instead of 'unknown_service'. + # Internal, unstable SDK switch: our AI SDK wrapper events ride the dedicated + # AI capture lane instead of /batch/. setup() syncs this onto the lazily + # auto-instantiated default client, whenever it gets constructed. + posthoganalytics._use_ai_lane = True # ty: ignore[invalid-assignment] + # Config for the SDK's `client.metrics` API, picked up by setup(). posthoganalytics.metrics = { # type: ignore[attr-defined] # Same fallback as the OTel trace resource (otel_instrumentation.py) — # metrics and traces from one process must share a service identity. @@ -138,10 +138,6 @@ def ready(self): if not posthoganalytics.disabled and posthoganalytics.feature_flag_definitions() is None: posthoganalytics.load_feature_flags() - # The feature_flag_definitions() call above constructs the default client, so - # the dedicated-AI-endpoint flag can only be applied from this point on. - enable_dedicated_ai_endpoint_for_default_client() - from posthog.async_migrations.setup import setup_async_migrations if settings.SKIP_ASYNC_MIGRATIONS_SETUP: diff --git a/posthog/llm/completions.py b/posthog/llm/completions.py index 8d8a9f62d034..ae5133ca89a2 100644 --- a/posthog/llm/completions.py +++ b/posthog/llm/completions.py @@ -1,4 +1,5 @@ import os +from functools import cache from typing import Any, Optional from django.conf import settings @@ -7,9 +8,14 @@ import posthoganalytics from posthoganalytics.ai.openai import OpenAI -openai_client = ( - OpenAI(posthog_client=posthoganalytics, base_url=settings.OPENAI_BASE_URL) if os.getenv("OPENAI_API_KEY") else None # type: ignore -) + +@cache +def _get_openai_client() -> Optional[OpenAI]: + # Lazy so importing this module never constructs the SDK's default client + # before apps.py has configured the posthoganalytics module attributes. + if not os.getenv("OPENAI_API_KEY"): + return None + return OpenAI(posthog_client=posthoganalytics.setup(), base_url=settings.OPENAI_BASE_URL) def hit_openai( @@ -20,6 +26,7 @@ def hit_openai( timeout: float | None = None, response_format: dict[str, Any] | None = None, ) -> tuple[str, int, int]: + openai_client = _get_openai_client() if not openai_client: raise ValueError("OPENAI_API_KEY environment variable not set") diff --git a/posthog/ph_client.py b/posthog/ph_client.py index b491f52fce57..3869d48a6089 100644 --- a/posthog/ph_client.py +++ b/posthog/ph_client.py @@ -4,13 +4,10 @@ from typing import Any from uuid import UUID -from django.conf import settings - import structlog import posthoganalytics from posthog.cloud_utils import is_cloud -from posthog.settings.ingestion import DedicatedAIEndpointRollout from posthog.utils import get_instance_region PH_US_API_KEY = "sTMFPsFhdP1Ssg" @@ -21,36 +18,6 @@ logger = structlog.get_logger(__name__) -_DEDICATED_AI_ENDPOINT_STAGES = (DedicatedAIEndpointRollout.RUNNER, DedicatedAIEndpointRollout.ALL) - - -def _use_dedicated_ai_endpoint(caller_stage: DedicatedAIEndpointRollout) -> bool: - rollout = settings.POSTHOG_DEDICATED_AI_ENDPOINT_ROLLOUT - if rollout is DedicatedAIEndpointRollout.OFF: - return False - return _DEDICATED_AI_ENDPOINT_STAGES.index(rollout) >= _DEDICATED_AI_ENDPOINT_STAGES.index(caller_stage) - - -def enable_dedicated_ai_endpoint_for_default_client() -> None: - """Route the module-level default client's `$ai_*` events to the dedicated AI - endpoint at the `all` rollout stage. - - Deliberate workaround: the SDK's lazy `setup()` doesn't accept - `_dedicated_ai_endpoint`, and we want to finish testing the endpoint on our own - traffic before rethinking the flag as a public option threaded through the - SDK's normal construction paths. Mutating the constructed client is safe: it - and its consumers read the flag per batch, and the SDK's post-fork consumer - rebuild copies it from the old consumers. - """ - if not _use_dedicated_ai_endpoint(DedicatedAIEndpointRollout.ALL): - return - client = posthoganalytics.default_client - if client is None: - return - client._dedicated_ai_endpoint = True - for consumer in client.consumers or []: - consumer.dedicated_ai_endpoint = True - def feature_enabled_or_false( key: str, @@ -117,12 +84,7 @@ def capture_ph_event(*args: Any, **kwargs: Any) -> None: ph_client.shutdown() -def get_client( - region: str = "US", - *, - dedicated_ai_endpoint_stage: DedicatedAIEndpointRollout = DedicatedAIEndpointRollout.ALL, - **kwargs: Any, -): +def get_client(region: str = "US", **kwargs: Any): from posthoganalytics import Posthog api_key = None @@ -140,6 +102,8 @@ def get_client( api_key, host=host, super_properties={"region": region}, - _dedicated_ai_endpoint=_use_dedicated_ai_endpoint(dedicated_ai_endpoint_stage), + # Internal, unstable SDK switch: AI SDK wrapper events ride the dedicated + # AI capture lane instead of /batch/. + _use_ai_lane=True, **kwargs, ) diff --git a/posthog/settings/ingestion.py b/posthog/settings/ingestion.py index 0a389058c0a8..b5ae6200bc47 100644 --- a/posthog/settings/ingestion.py +++ b/posthog/settings/ingestion.py @@ -1,13 +1,8 @@ import os -from enum import StrEnum - -import structlog from posthog.settings.utils import get_from_env, get_list, get_set from posthog.utils import str_to_bool -logger = structlog.get_logger(__name__) - INGESTION_LAG_METRIC_TEAM_IDS = get_list(os.getenv("INGESTION_LAG_METRIC_TEAM_IDS", "")) # KEEP IN SYNC WITH plugin-server/src/config/config.ts @@ -80,28 +75,6 @@ NEW_ANALYTICS_CAPTURE_ENDPOINT = os.getenv("NEW_CAPTURE_ENDPOINT", "/i/v0/e/") -# Cumulative rollout of the dedicated AI ingestion pipeline for our own `$ai_*` events: each stage -# also routes the stages before it. Chart-toggled so we can advance or roll back without a deploy. -class DedicatedAIEndpointRollout(StrEnum): - OFF = "off" - RUNNER = "runner" - ALL = "all" - - -def _parse_dedicated_ai_rollout(value: str) -> "DedicatedAIEndpointRollout": - try: - return DedicatedAIEndpointRollout(value.strip().lower()) - except ValueError: - logger.warning("invalid_dedicated_ai_endpoint_rollout", value=value) - return DedicatedAIEndpointRollout.OFF - - -POSTHOG_DEDICATED_AI_ENDPOINT_ROLLOUT = get_from_env( - "POSTHOG_DEDICATED_AI_ENDPOINT_ROLLOUT", - DedicatedAIEndpointRollout.OFF, - type_cast=_parse_dedicated_ai_rollout, -) - CAPTURE_V1_INTERNAL_ENDPOINT = os.getenv("CAPTURE_V1_INTERNAL_ENDPOINT", "/i/v1/analytics/events") CAPTURE_V1_INTERNAL_MAX_ATTEMPTS = get_from_env("CAPTURE_V1_INTERNAL_MAX_ATTEMPTS", type_cast=int, default=4) CAPTURE_V1_INTERNAL_RETRY_AFTER_CAP_SECONDS = get_from_env( diff --git a/posthog/test/test_ph_client.py b/posthog/test/test_ph_client.py index 6e2ef0346bff..77ed6392494e 100644 --- a/posthog/test/test_ph_client.py +++ b/posthog/test/test_ph_client.py @@ -1,69 +1,19 @@ -from django.test import SimpleTestCase, override_settings +from django.test import SimpleTestCase import posthoganalytics -from parameterized import parameterized -from posthoganalytics import Posthog -from posthog.ph_client import enable_dedicated_ai_endpoint_for_default_client, get_client -from posthog.settings.ingestion import ( - DedicatedAIEndpointRollout as Rollout, - _parse_dedicated_ai_rollout, -) +from posthog.ph_client import get_client -class TestDedicatedAIEndpointRollout(SimpleTestCase): - @parameterized.expand( - [ - (Rollout.OFF, Rollout.RUNNER, False), - (Rollout.OFF, Rollout.ALL, False), - (Rollout.RUNNER, Rollout.RUNNER, True), - (Rollout.RUNNER, Rollout.ALL, False), - (Rollout.ALL, Rollout.RUNNER, True), - (Rollout.ALL, Rollout.ALL, True), - ] - ) - def test_dedicated_ai_endpoint_gated_by_rollout_stage(self, rollout, caller_stage, expected): - with override_settings(POSTHOG_DEDICATED_AI_ENDPOINT_ROLLOUT=rollout): - client = get_client( - "US", dedicated_ai_endpoint_stage=caller_stage, send=False, enable_local_evaluation=False - ) - self.assertEqual(client._dedicated_ai_endpoint, expected) +class TestAILaneOptIn(SimpleTestCase): + def test_get_client_opts_into_ai_lane(self): + for region in ("US", "EU"): + client = get_client(region, send=False, enable_local_evaluation=False) + self.assertTrue(client._use_ai_lane) - def test_general_callers_only_opt_in_at_full_rollout(self): - with override_settings(POSTHOG_DEDICATED_AI_ENDPOINT_ROLLOUT=Rollout.RUNNER): - self.assertFalse(get_client("US", send=False, enable_local_evaluation=False)._dedicated_ai_endpoint) - with override_settings(POSTHOG_DEDICATED_AI_ENDPOINT_ROLLOUT=Rollout.ALL): - self.assertTrue(get_client("US", send=False, enable_local_evaluation=False)._dedicated_ai_endpoint) - - @parameterized.expand( - [ - ("off", Rollout.OFF), - ("runner", Rollout.RUNNER), - ("all", Rollout.ALL), - (" RUNNER ", Rollout.RUNNER), - ("bogus", Rollout.OFF), - ] - ) - def test_parse_rollout_falls_back_to_off_on_invalid(self, value, expected): - self.assertEqual(_parse_dedicated_ai_rollout(value), expected) - - @parameterized.expand( - [ - (Rollout.OFF, False), - (Rollout.RUNNER, False), - (Rollout.ALL, True), - ] - ) - def test_default_client_routes_ai_events_only_at_full_rollout(self, rollout, expected): - client = Posthog("test-key", send=False, enable_local_evaluation=False) - original = posthoganalytics.default_client - posthoganalytics.default_client = client # ty: ignore[invalid-assignment] - try: - with override_settings(POSTHOG_DEDICATED_AI_ENDPOINT_ROLLOUT=rollout): - enable_dedicated_ai_endpoint_for_default_client() - finally: - posthoganalytics.default_client = original - self.assertEqual(client._dedicated_ai_endpoint, expected) - self.assertTrue(client.consumers) - for consumer in client.consumers: - self.assertEqual(consumer.dedicated_ai_endpoint, expected) + def test_module_attribute_opts_default_client_into_ai_lane(self): + # apps.py sets the module attribute; setup() syncs it onto the lazily + # auto-instantiated default client whenever it gets constructed. + self.assertTrue(posthoganalytics._use_ai_lane) + client = posthoganalytics.setup() + self.assertTrue(client._use_ai_lane) diff --git a/products/exports/backend/temporal/subscriptions/llm_change_summary.py b/products/exports/backend/temporal/subscriptions/llm_change_summary.py index c9910f40a608..53ee87aafac2 100644 --- a/products/exports/backend/temporal/subscriptions/llm_change_summary.py +++ b/products/exports/backend/temporal/subscriptions/llm_change_summary.py @@ -362,7 +362,7 @@ def _attach_images_to_user_message( def _get_openai_client() -> OpenAI: if not os.environ.get("OPENAI_API_KEY"): raise ValueError("OPENAI_API_KEY environment variable not set") - return OpenAI(posthog_client=posthoganalytics, base_url=settings.OPENAI_BASE_URL, max_retries=3) # type: ignore[arg-type] + return OpenAI(posthog_client=posthoganalytics.setup(), base_url=settings.OPENAI_BASE_URL, max_retries=3) def generate_change_summary( diff --git a/products/replay_vision/backend/temporal/vision_actions/synthesis.py b/products/replay_vision/backend/temporal/vision_actions/synthesis.py index 7477930e7771..f5c21750dacc 100644 --- a/products/replay_vision/backend/temporal/vision_actions/synthesis.py +++ b/products/replay_vision/backend/temporal/vision_actions/synthesis.py @@ -422,7 +422,7 @@ def _run_synthesis(team: Team, action: VisionAction, lines: list[str]) -> str: # the LLM gateway (settings.OPENAI_BASE_URL), so the generation lands in LLM analytics tagged to # Replay Vision AND bills the team's AI credits ($ai_billable) — the same budget # is_team_over_ai_credit_budget gates on above. - client = OpenAI(posthog_client=posthoganalytics, base_url=settings.OPENAI_BASE_URL, max_retries=3) # type: ignore[arg-type] + client = OpenAI(posthog_client=posthoganalytics.setup(), base_url=settings.OPENAI_BASE_URL, max_retries=3) distinct_id = replay_vision_distinct_id(team.id) response = client.chat.completions.create( # type: ignore[call-overload] model=SYNTHESIS_MODEL, diff --git a/pyproject.toml b/pyproject.toml index 07db233ab128..5b86fe1b40d1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -100,7 +100,7 @@ dependencies = [ "paramiko~=3.5.0", "pillow==12.2.0", "protobuf~=5.29.6", - "posthoganalytics==7.27.0", + "posthoganalytics==7.29.0", "polars==1.37.1", "psycopg2-binary==2.9.10", "psycopg[binary]==3.2.4", diff --git a/uv.lock b/uv.lock index 4471a6c87ec1..986fa0469047 100644 --- a/uv.lock +++ b/uv.lock @@ -11,7 +11,7 @@ resolution-markers = [ ] [options] -exclude-newer = "2026-07-13T23:30:06.062618Z" +exclude-newer = "0001-01-01T00:00:00Z" # This has no effect and is included for backwards compatibility when using relative exclude-newer values. exclude-newer-span = "P7D" [options.exclude-newer-package] @@ -5933,7 +5933,7 @@ requires-dist = [ { name = "pixelhog", specifier = "~=1.2.0" }, { name = "playwright", specifier = "~=1.60.0" }, { name = "polars", specifier = "==1.37.1" }, - { name = "posthoganalytics", specifier = "==7.27.0" }, + { name = "posthoganalytics", specifier = "==7.29.0" }, { name = "protobuf", specifier = "~=5.29.6" }, { name = "psycopg", extras = ["binary"], specifier = "==3.2.4" }, { name = "psycopg2-binary", specifier = "==2.9.10" }, @@ -6094,7 +6094,7 @@ provides-extras = ["dev"] [[package]] name = "posthoganalytics" -version = "7.27.0" +version = "7.29.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "backoff" }, @@ -6102,9 +6102,9 @@ dependencies = [ { name = "requests" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a4/52/b2c043125c35abbfcaaebf581497d97227051625bc9bee9b235dbed5e807/posthoganalytics-7.27.0.tar.gz", hash = "sha256:5fdabb0e58616a766daeedb040a0868142581d259416d20d84fe5147db8488bd", size = 352566, upload-time = "2026-07-18T16:40:21.412Z" } +sdist = { url = "https://files.pythonhosted.org/packages/87/60/614e96eb7700c5e9f2deba7950fd559ca91b5a1e6b1e2d4288d987248086/posthoganalytics-7.29.0.tar.gz", hash = "sha256:2a5c697514333958b81e237004ad068b8dd6fb4bd6c1fea410186e19c818adf6", size = 360552, upload-time = "2026-07-23T15:27:33.829Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4e/75/9e6f0d3c55c2476d0fef27e8650c233b8afdd2848fab4674bdb71371b084/posthoganalytics-7.27.0-py3-none-any.whl", hash = "sha256:8fb2dd8f0238d2a8a61bed3e7f54536ad4d3dcfe1a45e113d67f4c6814c022f5", size = 424440, upload-time = "2026-07-18T16:40:20.013Z" }, + { url = "https://files.pythonhosted.org/packages/7b/7d/06adde30a2f05214ca233b28b5bee0970bbc750a8910cfa1e8c10fa40098/posthoganalytics-7.29.0-py3-none-any.whl", hash = "sha256:dab170775c738ad02af516ee0545e9d4857d6e06ac2ef1edf05ef55a99aeb7ab", size = 432594, upload-time = "2026-07-23T15:27:32.179Z" }, ] [[package]] From 102336c0874e4d7a9397a7abdd828bc900d9c69f Mon Sep 17 00:00:00 2001 From: Carlos Marchal Date: Fri, 24 Jul 2026 10:52:55 +0200 Subject: [PATCH 2/5] Update posthog/apps.py Co-authored-by: graphite-app[bot] <96075541+graphite-app[bot]@users.noreply.github.com> --- posthog/apps.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/posthog/apps.py b/posthog/apps.py index a450184cb6c0..401d0d0c12c9 100644 --- a/posthog/apps.py +++ b/posthog/apps.py @@ -75,7 +75,8 @@ def ready(self): # Internal, unstable SDK switch: our AI SDK wrapper events ride the dedicated # AI capture lane instead of /batch/. setup() syncs this onto the lazily # auto-instantiated default client, whenever it gets constructed. - posthoganalytics._use_ai_lane = True # ty: ignore[invalid-assignment] + posthoganalytics._use_ai_lane = True # type: ignore[invalid-assignment] + # Config for the SDK's `client.metrics` API, picked up by setup(). posthoganalytics.metrics = { # type: ignore[attr-defined] # Same fallback as the OTel trace resource (otel_instrumentation.py) — From 79319937996e1990f27a9681ccc17e1de5291d08 Mon Sep 17 00:00:00 2001 From: Carlos Marchal Date: Fri, 24 Jul 2026 11:24:23 +0200 Subject: [PATCH 3/5] fix: ty suppressions and comment churn --- posthog/apps.py | 12 ++++++------ posthog/llm/completions.py | 2 -- posthog/ph_client.py | 2 -- posthog/test/test_ph_client.py | 2 -- 4 files changed, 6 insertions(+), 12 deletions(-) diff --git a/posthog/apps.py b/posthog/apps.py index 401d0d0c12c9..285eaf4073e8 100644 --- a/posthog/apps.py +++ b/posthog/apps.py @@ -72,13 +72,13 @@ def ready(self): "service": settings.OTEL_SERVICE_NAME, "environment": os.getenv("OTEL_SERVICE_ENVIRONMENT"), } - # Internal, unstable SDK switch: our AI SDK wrapper events ride the dedicated - # AI capture lane instead of /batch/. setup() syncs this onto the lazily - # auto-instantiated default client, whenever it gets constructed. - posthoganalytics._use_ai_lane = True # type: ignore[invalid-assignment] + posthoganalytics._use_ai_lane = True # ty: ignore[invalid-assignment] - # Config for the SDK's `client.metrics` API, picked up by setup(). - posthoganalytics.metrics = { # type: ignore[attr-defined] + # Config for the SDK's `client.metrics` API. The pinned SDK version predates + # the metrics API and ignores this attr; once posthoganalytics is bumped to + # >=7.23 it's picked up by setup(), so metrics get a real service.name + # instead of 'unknown_service'. + posthoganalytics.metrics = { # type: ignore[attr-defined] # ty: ignore[invalid-assignment] # Same fallback as the OTel trace resource (otel_instrumentation.py) — # metrics and traces from one process must share a service identity. "service_name": settings.OTEL_SERVICE_NAME or "posthog-django-default", diff --git a/posthog/llm/completions.py b/posthog/llm/completions.py index ae5133ca89a2..baeb90269e26 100644 --- a/posthog/llm/completions.py +++ b/posthog/llm/completions.py @@ -11,8 +11,6 @@ @cache def _get_openai_client() -> Optional[OpenAI]: - # Lazy so importing this module never constructs the SDK's default client - # before apps.py has configured the posthoganalytics module attributes. if not os.getenv("OPENAI_API_KEY"): return None return OpenAI(posthog_client=posthoganalytics.setup(), base_url=settings.OPENAI_BASE_URL) diff --git a/posthog/ph_client.py b/posthog/ph_client.py index 3869d48a6089..97f3161cc1ab 100644 --- a/posthog/ph_client.py +++ b/posthog/ph_client.py @@ -102,8 +102,6 @@ def get_client(region: str = "US", **kwargs: Any): api_key, host=host, super_properties={"region": region}, - # Internal, unstable SDK switch: AI SDK wrapper events ride the dedicated - # AI capture lane instead of /batch/. _use_ai_lane=True, **kwargs, ) diff --git a/posthog/test/test_ph_client.py b/posthog/test/test_ph_client.py index 77ed6392494e..9a8f6d642c17 100644 --- a/posthog/test/test_ph_client.py +++ b/posthog/test/test_ph_client.py @@ -12,8 +12,6 @@ def test_get_client_opts_into_ai_lane(self): self.assertTrue(client._use_ai_lane) def test_module_attribute_opts_default_client_into_ai_lane(self): - # apps.py sets the module attribute; setup() syncs it onto the lazily - # auto-instantiated default client whenever it gets constructed. self.assertTrue(posthoganalytics._use_ai_lane) client = posthoganalytics.setup() self.assertTrue(client._use_ai_lane) From 145c4c11bb15f2c3a14a11040680ab2d729d675a Mon Sep 17 00:00:00 2001 From: Carlos Marchal Date: Fri, 24 Jul 2026 16:22:27 +0200 Subject: [PATCH 4/5] fix: mypy unused ignore and exports test interception point --- posthog/apps.py | 2 +- .../subscriptions/test_llm_change_summary.py | 13 +++++++------ 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/posthog/apps.py b/posthog/apps.py index 285eaf4073e8..ca22326a136f 100644 --- a/posthog/apps.py +++ b/posthog/apps.py @@ -78,7 +78,7 @@ def ready(self): # the metrics API and ignores this attr; once posthoganalytics is bumped to # >=7.23 it's picked up by setup(), so metrics get a real service.name # instead of 'unknown_service'. - posthoganalytics.metrics = { # type: ignore[attr-defined] # ty: ignore[invalid-assignment] + posthoganalytics.metrics = { # ty: ignore[invalid-assignment] # Same fallback as the OTel trace resource (otel_instrumentation.py) — # metrics and traces from one process must share a service identity. "service_name": settings.OTEL_SERVICE_NAME or "posthog-django-default", diff --git a/products/exports/backend/temporal/subscriptions/test_llm_change_summary.py b/products/exports/backend/temporal/subscriptions/test_llm_change_summary.py index 649a9adb0464..f8628e1aa387 100644 --- a/products/exports/backend/temporal/subscriptions/test_llm_change_summary.py +++ b/products/exports/backend/temporal/subscriptions/test_llm_change_summary.py @@ -691,12 +691,8 @@ def test_emits_ai_generation_event_with_billing_properties(self, monkeypatch): monkeypatch.setenv("OPENAI_API_KEY", "test-fake-key") monkeypatch.setattr("posthog.event_usage.SITE_URL", "https://us.posthog.com") - captured_calls: list[dict] = [] - - def fake_capture(*args, **kwargs): - captured_calls.append(kwargs) - - monkeypatch.setattr("posthoganalytics.capture", fake_capture) + fake_client = MagicMock() + monkeypatch.setattr("posthoganalytics.default_client", fake_client) usage_details = MagicMock() usage_details.cached_tokens = 0 @@ -721,6 +717,11 @@ def fake_capture(*args, **kwargs): with patch("openai.resources.chat.completions.Completions.create", return_value=fake_response): generate_change_summary(None, current, team=team, delivery_id="abc-123") # type: ignore[arg-type] + captured_calls = [ + c.kwargs + for method in (fake_client.capture, fake_client._capture_ai) + for c in method.call_args_list + ] ai_generation_calls = [c for c in captured_calls if c.get("event") == "$ai_generation"] assert len(ai_generation_calls) == 1, ( f"expected exactly one $ai_generation capture, got events: {[c.get('event') for c in captured_calls]}" From 7d3ff7551b0da32f872c5c710476a394eb2fe675 Mon Sep 17 00:00:00 2001 From: Carlos Marchal Date: Fri, 24 Jul 2026 16:58:33 +0200 Subject: [PATCH 5/5] chore: format exports test --- .../backend/temporal/subscriptions/test_llm_change_summary.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/products/exports/backend/temporal/subscriptions/test_llm_change_summary.py b/products/exports/backend/temporal/subscriptions/test_llm_change_summary.py index f8628e1aa387..0db06456a0fc 100644 --- a/products/exports/backend/temporal/subscriptions/test_llm_change_summary.py +++ b/products/exports/backend/temporal/subscriptions/test_llm_change_summary.py @@ -718,9 +718,7 @@ def test_emits_ai_generation_event_with_billing_properties(self, monkeypatch): generate_change_summary(None, current, team=team, delivery_id="abc-123") # type: ignore[arg-type] captured_calls = [ - c.kwargs - for method in (fake_client.capture, fake_client._capture_ai) - for c in method.call_args_list + c.kwargs for method in (fake_client.capture, fake_client._capture_ai) for c in method.call_args_list ] ai_generation_calls = [c for c in captured_calls if c.get("event") == "$ai_generation"] assert len(ai_generation_calls) == 1, (