diff --git a/backend/apps/agents/core/ws_manager.py b/backend/apps/agents/core/ws_manager.py index 3e24da9f..f804904a 100644 --- a/backend/apps/agents/core/ws_manager.py +++ b/backend/apps/agents/core/ws_manager.py @@ -90,12 +90,10 @@ class ConnectionManager: if event == "agent:status" and data.get("status") in TERMINAL_STATUSES: seq_log.persist_terminal(session_id, payload_str) - # Mirror every discrete agent message into swarm-analytics. Outside the - # stamp lock (fire-and-forget; must not gate the broadcast). Replays use - # ws.send_text directly, not this path, so reconnects don't double-count. + # Outside the stamp lock so analytics can't gate the broadcast; replays go via ws.send_text, so reconnects don't double-count. if event == "agent:message": try: - from backend.apps.service.analytics_agent_bridge import bridge_agent_message, BroadcastMessage + from backend.apps.service.analytics.agent_bridge import bridge_agent_message, BroadcastMessage bridge_agent_message(session_id, BroadcastMessage.model_validate(data.get("message") or {})) except Exception: logger.debug("agent:message analytics bridge failed", exc_info=True) diff --git a/backend/apps/agents/manager/AgentLaunchMixin.py b/backend/apps/agents/manager/AgentLaunchMixin.py index 8370f26b..3154b3d4 100644 --- a/backend/apps/agents/manager/AgentLaunchMixin.py +++ b/backend/apps/agents/manager/AgentLaunchMixin.py @@ -147,7 +147,7 @@ class AgentLaunchMixin: }) try: - from backend.apps.service.analytics import track_agent_created + from backend.apps.service.analytics.client import track_agent_created track_agent_created(id=session.id, dashboard_id=session.dashboard_id) except Exception: pass diff --git a/backend/apps/agents/manager/metadata.py b/backend/apps/agents/manager/metadata.py index 85b306ef..0f3d5c7f 100644 --- a/backend/apps/agents/manager/metadata.py +++ b/backend/apps/agents/manager/metadata.py @@ -95,7 +95,7 @@ async def generate_title(session: Optional[AgentSession], session_id: str, first "name": title, }) try: - from backend.apps.service.analytics import track_agent_title + from backend.apps.service.analytics.client import track_agent_title track_agent_title(id=session_id, title=title) except Exception: pass diff --git a/backend/apps/auth/router.py b/backend/apps/auth/router.py index 3745b919..42153976 100644 --- a/backend/apps/auth/router.py +++ b/backend/apps/auth/router.py @@ -78,7 +78,7 @@ def p_sync_identity_to_service(settings_obj) -> None: logger.debug("identify sync failed: %s", e) if email: try: - from backend.apps.service.analytics import track_link_email + from backend.apps.service.analytics.client import track_link_email track_link_email(email) except Exception as e: logger.debug("analytics link_email sync failed: %s", e) diff --git a/backend/apps/dashboards/dashboards.py b/backend/apps/dashboards/dashboards.py index bbf29e91..94c492b3 100644 --- a/backend/apps/dashboards/dashboards.py +++ b/backend/apps/dashboards/dashboards.py @@ -137,7 +137,7 @@ async def create_dashboard(body: DashboardCreate): dashboard = Dashboard(name=body.name) save(dashboard) try: - from backend.apps.service.analytics import track_dashboard_event + from backend.apps.service.analytics.client import track_dashboard_event track_dashboard_event(dashboard_id=dashboard.id, action="create") except Exception: pass @@ -458,7 +458,7 @@ async def delete_dashboard(dashboard_id: str): p_delete(dashboard_id) try: - from backend.apps.service.analytics import track_dashboard_event + from backend.apps.service.analytics.client import track_dashboard_event track_dashboard_event(dashboard_id=dashboard_id, action="delete") except Exception: pass @@ -565,7 +565,7 @@ async def duplicate_dashboard(dashboard_id: str): atomic_write_json(os.path.join(DATA_DIR, f"{new_id}.json"), new_dashboard) try: - from backend.apps.service.analytics import track_dashboard_event + from backend.apps.service.analytics.client import track_dashboard_event track_dashboard_event(dashboard_id=new_id, action="create") except Exception: pass diff --git a/backend/apps/service/analytics/__init__.py b/backend/apps/service/analytics/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/backend/apps/service/analytics_agent_bridge.py b/backend/apps/service/analytics/agent_bridge.py similarity index 97% rename from backend/apps/service/analytics_agent_bridge.py rename to backend/apps/service/analytics/agent_bridge.py index f3081cd6..60a31d20 100644 --- a/backend/apps/service/analytics_agent_bridge.py +++ b/backend/apps/service/analytics/agent_bridge.py @@ -13,7 +13,7 @@ from pydantic import BaseModel, ConfigDict from typeguard import typechecked from backend.apps.agents.core.models import AgentSession -from backend.apps.service.analytics import track_agent_message +from backend.apps.service.analytics.client import track_agent_message logger = logging.getLogger(__name__) diff --git a/backend/apps/service/analytics.py b/backend/apps/service/analytics/client.py similarity index 100% rename from backend/apps/service/analytics.py rename to backend/apps/service/analytics/client.py diff --git a/backend/apps/service/analytics_frontend_bridge.py b/backend/apps/service/analytics/frontend_bridge.py similarity index 97% rename from backend/apps/service/analytics_frontend_bridge.py rename to backend/apps/service/analytics/frontend_bridge.py index 64d446e7..685a5f57 100644 --- a/backend/apps/service/analytics_frontend_bridge.py +++ b/backend/apps/service/analytics/frontend_bridge.py @@ -11,7 +11,7 @@ from typing import Optional from pydantic import BaseModel, ConfigDict from typeguard import typechecked -from backend.apps.service.analytics import ( +from backend.apps.service.analytics.client import ( persist_client_env, track_app_opened, track_dashboard_event, diff --git a/backend/apps/service/service.py b/backend/apps/service/service.py index b5831a9c..fa17f48f 100644 --- a/backend/apps/service/service.py +++ b/backend/apps/service/service.py @@ -191,7 +191,7 @@ async def service_lifespan(): svc.sync({"identity": id_props}) # First-boot log write doubles as the token-registration trigger. - from backend.apps.service.analytics import get_analytics_client, track_link_email + from backend.apps.service.analytics.client import get_analytics_client, track_link_email analytics_client = get_analytics_client() if analytics_client is not None: try: @@ -251,7 +251,7 @@ async def service_lifespan(): # Flush before the process exits or buffered events are lost. try: - from backend.apps.service.analytics import track_app_closed, shutdown_analytics + from backend.apps.service.analytics.client import track_app_closed, shutdown_analytics track_app_closed() shutdown_analytics() except Exception: @@ -444,7 +444,7 @@ async def service_status(): def p_bridge_to_analytics(item: dict) -> None: # Boundary adapter: validate the raw report() envelope into a typed event, hand it to the analytics bridge. - from backend.apps.service.analytics_frontend_bridge import bridge_frontend_event, FrontendEvent + from backend.apps.service.analytics.frontend_bridge import bridge_frontend_event, FrontendEvent try: bridge_frontend_event(FrontendEvent.model_validate(item)) except Exception: diff --git a/backend/apps/settings/settings.py b/backend/apps/settings/settings.py index cd85c2c2..bf47a77f 100644 --- a/backend/apps/settings/settings.py +++ b/backend/apps/settings/settings.py @@ -266,7 +266,7 @@ async def apply_settings_update(body: AppSettings, protect_fields: set[str] | No if id_props: p_identify(id_props) if body.user_email: - from backend.apps.service.analytics import track_link_email + from backend.apps.service.analytics.client import track_link_email track_link_email(body.user_email) await save_settings_async(body) diff --git a/linter/config/config.json b/linter/config/config.json index 1c52f44e..6c014103 100644 --- a/linter/config/config.json +++ b/linter/config/config.json @@ -140,6 +140,7 @@ "backend/apps/agents/manager/streaming", "backend/apps/outputs", "backend/apps/tools_lib", + "backend/apps/service", "backend/tests", "frontend/src/shared", "frontend/src/shared/state",