mirror of
https://github.com/OpenHands/OpenHands.git
synced 2025-12-26 05:48:36 +08:00
Revert "chore(backend): Add better PostHog tracking" (#11749)
This commit is contained in:
parent
d9731b6850
commit
96f13b15e7
@ -31,7 +31,6 @@ from openhands.server.services.conversation_service import create_provider_token
|
||||
from openhands.server.shared import config
|
||||
from openhands.server.user_auth import get_access_token
|
||||
from openhands.server.user_auth.user_auth import get_user_auth
|
||||
from openhands.utils.posthog_tracker import track_user_signup_completed
|
||||
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter('ignore')
|
||||
@ -370,12 +369,6 @@ async def accept_tos(request: Request):
|
||||
|
||||
logger.info(f'User {user_id} accepted TOS')
|
||||
|
||||
# Track user signup completion in PostHog
|
||||
track_user_signup_completed(
|
||||
user_id=user_id,
|
||||
signup_timestamp=user_settings.accepted_tos.isoformat(),
|
||||
)
|
||||
|
||||
response = JSONResponse(
|
||||
status_code=status.HTTP_200_OK, content={'redirect_url': redirect_url}
|
||||
)
|
||||
|
||||
@ -28,7 +28,6 @@ from storage.subscription_access import SubscriptionAccess
|
||||
|
||||
from openhands.server.user_auth import get_user_id
|
||||
from openhands.utils.http_session import httpx_verify_option
|
||||
from openhands.utils.posthog_tracker import track_credits_purchased
|
||||
|
||||
stripe.api_key = STRIPE_API_KEY
|
||||
billing_router = APIRouter(prefix='/api/billing')
|
||||
@ -458,20 +457,6 @@ async def success_callback(session_id: str, request: Request):
|
||||
)
|
||||
session.commit()
|
||||
|
||||
# Track credits purchased in PostHog
|
||||
try:
|
||||
track_credits_purchased(
|
||||
user_id=billing_session.user_id,
|
||||
amount_usd=amount_subtotal / 100, # Convert cents to dollars
|
||||
credits_added=add_credits,
|
||||
stripe_session_id=session_id,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
f'Failed to track credits purchase: {e}',
|
||||
extra={'user_id': billing_session.user_id, 'error': str(e)},
|
||||
)
|
||||
|
||||
return RedirectResponse(
|
||||
f'{request.base_url}settings/billing?checkout=success', status_code=302
|
||||
)
|
||||
|
||||
@ -42,10 +42,6 @@ from openhands.core.exceptions import (
|
||||
from openhands.core.logger import LOG_ALL_EVENTS
|
||||
from openhands.core.logger import openhands_logger as logger
|
||||
from openhands.core.schema import AgentState
|
||||
from openhands.utils.posthog_tracker import (
|
||||
track_agent_task_completed,
|
||||
track_credit_limit_reached,
|
||||
)
|
||||
from openhands.events import (
|
||||
EventSource,
|
||||
EventStream,
|
||||
@ -713,20 +709,6 @@ class AgentController:
|
||||
EventSource.ENVIRONMENT,
|
||||
)
|
||||
|
||||
# Track agent task completion in PostHog
|
||||
if new_state == AgentState.FINISHED:
|
||||
try:
|
||||
# Get app_mode from environment, default to 'oss'
|
||||
app_mode = os.environ.get('APP_MODE', 'oss')
|
||||
track_agent_task_completed(
|
||||
conversation_id=self.id,
|
||||
user_id=self.user_id,
|
||||
app_mode=app_mode,
|
||||
)
|
||||
except Exception as e:
|
||||
# Don't let tracking errors interrupt the agent
|
||||
self.log('warning', f'Failed to track agent completion: {e}')
|
||||
|
||||
# Save state whenever agent state changes to ensure we don't lose state
|
||||
# in case of crashes or unexpected circumstances
|
||||
self.save_state()
|
||||
@ -905,18 +887,6 @@ class AgentController:
|
||||
self.state_tracker.run_control_flags()
|
||||
except Exception as e:
|
||||
logger.warning('Control flag limits hit')
|
||||
# Track credit limit reached if it's a budget exception
|
||||
if 'budget' in str(e).lower() and self.state.budget_flag:
|
||||
try:
|
||||
track_credit_limit_reached(
|
||||
conversation_id=self.id,
|
||||
user_id=self.user_id,
|
||||
current_budget=self.state.budget_flag.current_value,
|
||||
max_budget=self.state.budget_flag.max_value,
|
||||
)
|
||||
except Exception as track_error:
|
||||
# Don't let tracking errors interrupt the agent
|
||||
self.log('warning', f'Failed to track credit limit: {track_error}')
|
||||
await self._react_to_exception(e)
|
||||
return
|
||||
|
||||
|
||||
@ -26,13 +26,11 @@ from openhands.microagent.types import (
|
||||
)
|
||||
from openhands.server.dependencies import get_dependencies
|
||||
from openhands.server.shared import server_config
|
||||
from openhands.server.types import AppMode
|
||||
from openhands.server.user_auth import (
|
||||
get_access_token,
|
||||
get_provider_tokens,
|
||||
get_user_id,
|
||||
)
|
||||
from openhands.utils.posthog_tracker import alias_user_identities
|
||||
|
||||
app = APIRouter(prefix='/api/user', dependencies=get_dependencies())
|
||||
|
||||
@ -119,14 +117,6 @@ async def get_user(
|
||||
|
||||
try:
|
||||
user: User = await client.get_user()
|
||||
|
||||
# Alias git provider login with Keycloak user ID in PostHog (SaaS mode only)
|
||||
if user_id and user.login and server_config.app_mode == AppMode.SAAS:
|
||||
alias_user_identities(
|
||||
keycloak_user_id=user_id,
|
||||
git_login=user.login,
|
||||
)
|
||||
|
||||
return user
|
||||
|
||||
except UnknownException as e:
|
||||
|
||||
@ -1,270 +0,0 @@
|
||||
"""PostHog tracking utilities for OpenHands events."""
|
||||
|
||||
import os
|
||||
|
||||
from openhands.core.logger import openhands_logger as logger
|
||||
|
||||
# Lazy import posthog to avoid import errors in environments where it's not installed
|
||||
posthog = None
|
||||
|
||||
|
||||
def _init_posthog():
|
||||
"""Initialize PostHog client lazily."""
|
||||
global posthog
|
||||
if posthog is None:
|
||||
try:
|
||||
import posthog as ph
|
||||
|
||||
posthog = ph
|
||||
posthog.api_key = os.environ.get(
|
||||
'POSTHOG_CLIENT_KEY', 'phc_3ESMmY9SgqEAGBB6sMGK5ayYHkeUuknH2vP6FmWH9RA'
|
||||
)
|
||||
posthog.host = os.environ.get('POSTHOG_HOST', 'https://us.i.posthog.com')
|
||||
except ImportError:
|
||||
logger.warning(
|
||||
'PostHog not installed. Analytics tracking will be disabled.'
|
||||
)
|
||||
posthog = None
|
||||
|
||||
|
||||
def track_agent_task_completed(
|
||||
conversation_id: str,
|
||||
user_id: str | None = None,
|
||||
app_mode: str | None = None,
|
||||
) -> None:
|
||||
"""Track when an agent completes a task.
|
||||
|
||||
Args:
|
||||
conversation_id: The ID of the conversation/session
|
||||
user_id: The ID of the user (optional, may be None for unauthenticated users)
|
||||
app_mode: The application mode (saas/oss), optional
|
||||
"""
|
||||
_init_posthog()
|
||||
|
||||
if posthog is None:
|
||||
return
|
||||
|
||||
# Use conversation_id as distinct_id if user_id is not available
|
||||
# This ensures we can track completions even for anonymous users
|
||||
distinct_id = user_id if user_id else f'conversation_{conversation_id}'
|
||||
|
||||
try:
|
||||
posthog.capture(
|
||||
distinct_id=distinct_id,
|
||||
event='agent_task_completed',
|
||||
properties={
|
||||
'conversation_id': conversation_id,
|
||||
'user_id': user_id,
|
||||
'app_mode': app_mode or 'unknown',
|
||||
},
|
||||
)
|
||||
logger.debug(
|
||||
'posthog_track',
|
||||
extra={
|
||||
'event': 'agent_task_completed',
|
||||
'conversation_id': conversation_id,
|
||||
'user_id': user_id,
|
||||
},
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
f'Failed to track agent_task_completed to PostHog: {e}',
|
||||
extra={
|
||||
'conversation_id': conversation_id,
|
||||
'error': str(e),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def track_user_signup_completed(
|
||||
user_id: str,
|
||||
signup_timestamp: str,
|
||||
) -> None:
|
||||
"""Track when a user completes signup by accepting TOS.
|
||||
|
||||
Args:
|
||||
user_id: The ID of the user (Keycloak user ID)
|
||||
signup_timestamp: ISO format timestamp of when TOS was accepted
|
||||
"""
|
||||
_init_posthog()
|
||||
|
||||
if posthog is None:
|
||||
return
|
||||
|
||||
try:
|
||||
posthog.capture(
|
||||
distinct_id=user_id,
|
||||
event='user_signup_completed',
|
||||
properties={
|
||||
'user_id': user_id,
|
||||
'signup_timestamp': signup_timestamp,
|
||||
},
|
||||
)
|
||||
logger.debug(
|
||||
'posthog_track',
|
||||
extra={
|
||||
'event': 'user_signup_completed',
|
||||
'user_id': user_id,
|
||||
},
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
f'Failed to track user_signup_completed to PostHog: {e}',
|
||||
extra={
|
||||
'user_id': user_id,
|
||||
'error': str(e),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def track_credit_limit_reached(
|
||||
conversation_id: str,
|
||||
user_id: str | None = None,
|
||||
current_budget: float = 0.0,
|
||||
max_budget: float = 0.0,
|
||||
) -> None:
|
||||
"""Track when a user reaches their credit limit during a conversation.
|
||||
|
||||
Args:
|
||||
conversation_id: The ID of the conversation/session
|
||||
user_id: The ID of the user (optional, may be None for unauthenticated users)
|
||||
current_budget: The current budget spent
|
||||
max_budget: The maximum budget allowed
|
||||
"""
|
||||
_init_posthog()
|
||||
|
||||
if posthog is None:
|
||||
return
|
||||
|
||||
distinct_id = user_id if user_id else f'conversation_{conversation_id}'
|
||||
|
||||
try:
|
||||
posthog.capture(
|
||||
distinct_id=distinct_id,
|
||||
event='credit_limit_reached',
|
||||
properties={
|
||||
'conversation_id': conversation_id,
|
||||
'user_id': user_id,
|
||||
'current_budget': current_budget,
|
||||
'max_budget': max_budget,
|
||||
},
|
||||
)
|
||||
logger.debug(
|
||||
'posthog_track',
|
||||
extra={
|
||||
'event': 'credit_limit_reached',
|
||||
'conversation_id': conversation_id,
|
||||
'user_id': user_id,
|
||||
'current_budget': current_budget,
|
||||
'max_budget': max_budget,
|
||||
},
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
f'Failed to track credit_limit_reached to PostHog: {e}',
|
||||
extra={
|
||||
'conversation_id': conversation_id,
|
||||
'error': str(e),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def track_credits_purchased(
|
||||
user_id: str,
|
||||
amount_usd: float,
|
||||
credits_added: float,
|
||||
stripe_session_id: str,
|
||||
) -> None:
|
||||
"""Track when a user successfully purchases credits.
|
||||
|
||||
Args:
|
||||
user_id: The ID of the user (Keycloak user ID)
|
||||
amount_usd: The amount paid in USD (cents converted to dollars)
|
||||
credits_added: The number of credits added to the user's account
|
||||
stripe_session_id: The Stripe checkout session ID
|
||||
"""
|
||||
_init_posthog()
|
||||
|
||||
if posthog is None:
|
||||
return
|
||||
|
||||
try:
|
||||
posthog.capture(
|
||||
distinct_id=user_id,
|
||||
event='credits_purchased',
|
||||
properties={
|
||||
'user_id': user_id,
|
||||
'amount_usd': amount_usd,
|
||||
'credits_added': credits_added,
|
||||
'stripe_session_id': stripe_session_id,
|
||||
},
|
||||
)
|
||||
logger.debug(
|
||||
'posthog_track',
|
||||
extra={
|
||||
'event': 'credits_purchased',
|
||||
'user_id': user_id,
|
||||
'amount_usd': amount_usd,
|
||||
'credits_added': credits_added,
|
||||
},
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
f'Failed to track credits_purchased to PostHog: {e}',
|
||||
extra={
|
||||
'user_id': user_id,
|
||||
'error': str(e),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def alias_user_identities(
|
||||
keycloak_user_id: str,
|
||||
git_login: str,
|
||||
) -> None:
|
||||
"""Alias a user's Keycloak ID with their git provider login for unified tracking.
|
||||
|
||||
This allows PostHog to link events tracked from the frontend (using git provider login)
|
||||
with events tracked from the backend (using Keycloak user ID).
|
||||
|
||||
PostHog Python alias syntax: alias(previous_id, distinct_id)
|
||||
- previous_id: The old/previous distinct ID that will be merged
|
||||
- distinct_id: The new/canonical distinct ID to merge into
|
||||
|
||||
For our use case:
|
||||
- Git provider login is the previous_id (first used in frontend, before backend auth)
|
||||
- Keycloak user ID is the distinct_id (canonical backend ID)
|
||||
- Result: All events with git login will be merged into Keycloak user ID
|
||||
|
||||
Args:
|
||||
keycloak_user_id: The Keycloak user ID (canonical distinct_id)
|
||||
git_login: The git provider username (GitHub/GitLab/Bitbucket) to merge
|
||||
|
||||
Reference:
|
||||
https://github.com/PostHog/posthog-python/blob/master/posthog/client.py
|
||||
"""
|
||||
_init_posthog()
|
||||
|
||||
if posthog is None:
|
||||
return
|
||||
|
||||
try:
|
||||
# Merge git provider login into Keycloak user ID
|
||||
# posthog.alias(previous_id, distinct_id) - official Python SDK signature
|
||||
posthog.alias(git_login, keycloak_user_id)
|
||||
logger.debug(
|
||||
'posthog_alias',
|
||||
extra={
|
||||
'previous_id': git_login,
|
||||
'distinct_id': keycloak_user_id,
|
||||
},
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
f'Failed to alias user identities in PostHog: {e}',
|
||||
extra={
|
||||
'keycloak_user_id': keycloak_user_id,
|
||||
'git_login': git_login,
|
||||
'error': str(e),
|
||||
},
|
||||
)
|
||||
@ -1,243 +0,0 @@
|
||||
"""Integration tests for PostHog tracking in AgentController."""
|
||||
|
||||
import asyncio
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from openhands.controller.agent import Agent
|
||||
from openhands.controller.agent_controller import AgentController
|
||||
from openhands.core.config import OpenHandsConfig
|
||||
from openhands.core.config.agent_config import AgentConfig
|
||||
from openhands.core.config.llm_config import LLMConfig
|
||||
from openhands.core.schema import AgentState
|
||||
from openhands.events import EventSource, EventStream
|
||||
from openhands.events.action.message import SystemMessageAction
|
||||
from openhands.llm.llm_registry import LLMRegistry
|
||||
from openhands.server.services.conversation_stats import ConversationStats
|
||||
from openhands.storage.memory import InMemoryFileStore
|
||||
|
||||
|
||||
@pytest.fixture(scope='function')
|
||||
def event_loop():
|
||||
"""Create event loop for async tests."""
|
||||
loop = asyncio.get_event_loop_policy().new_event_loop()
|
||||
yield loop
|
||||
loop.close()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_agent_with_stats():
|
||||
"""Create a mock agent with properly connected LLM registry and conversation stats."""
|
||||
import uuid
|
||||
|
||||
# Create LLM registry
|
||||
config = OpenHandsConfig()
|
||||
llm_registry = LLMRegistry(config=config)
|
||||
|
||||
# Create conversation stats
|
||||
file_store = InMemoryFileStore({})
|
||||
conversation_id = f'test-conversation-{uuid.uuid4()}'
|
||||
conversation_stats = ConversationStats(
|
||||
file_store=file_store, conversation_id=conversation_id, user_id='test-user'
|
||||
)
|
||||
|
||||
# Connect registry to stats
|
||||
llm_registry.subscribe(conversation_stats.register_llm)
|
||||
|
||||
# Create mock agent
|
||||
agent = MagicMock(spec=Agent)
|
||||
agent_config = MagicMock(spec=AgentConfig)
|
||||
llm_config = LLMConfig(
|
||||
model='gpt-4o',
|
||||
api_key='test_key',
|
||||
num_retries=2,
|
||||
retry_min_wait=1,
|
||||
retry_max_wait=2,
|
||||
)
|
||||
agent_config.disabled_microagents = []
|
||||
agent_config.enable_mcp = True
|
||||
agent_config.enable_stuck_detection = True
|
||||
llm_registry.service_to_llm.clear()
|
||||
mock_llm = llm_registry.get_llm('agent_llm', llm_config)
|
||||
agent.llm = mock_llm
|
||||
agent.name = 'test-agent'
|
||||
agent.sandbox_plugins = []
|
||||
agent.config = agent_config
|
||||
agent.llm_registry = llm_registry
|
||||
agent.prompt_manager = MagicMock()
|
||||
|
||||
# Add a proper system message mock
|
||||
system_message = SystemMessageAction(
|
||||
content='Test system message', tools=['test_tool']
|
||||
)
|
||||
system_message._source = EventSource.AGENT
|
||||
system_message._id = -1 # Set invalid ID to avoid the ID check
|
||||
agent.get_system_message.return_value = system_message
|
||||
|
||||
return agent, conversation_stats, llm_registry
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_event_stream():
|
||||
"""Create a mock event stream."""
|
||||
mock = MagicMock(
|
||||
spec=EventStream,
|
||||
event_stream=EventStream(sid='test', file_store=InMemoryFileStore({})),
|
||||
)
|
||||
mock.get_latest_event_id.return_value = 0
|
||||
return mock
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_agent_finish_triggers_posthog_tracking(
|
||||
mock_agent_with_stats, mock_event_stream
|
||||
):
|
||||
"""Test that setting agent state to FINISHED triggers PostHog tracking."""
|
||||
mock_agent, conversation_stats, llm_registry = mock_agent_with_stats
|
||||
|
||||
controller = AgentController(
|
||||
agent=mock_agent,
|
||||
event_stream=mock_event_stream,
|
||||
conversation_stats=conversation_stats,
|
||||
iteration_delta=10,
|
||||
sid='test-conversation-123',
|
||||
user_id='test-user-456',
|
||||
confirmation_mode=False,
|
||||
headless_mode=True,
|
||||
)
|
||||
|
||||
with (
|
||||
patch('openhands.utils.posthog_tracker.posthog') as mock_posthog,
|
||||
patch('os.environ.get') as mock_env_get,
|
||||
):
|
||||
# Setup mocks
|
||||
mock_posthog.capture = MagicMock()
|
||||
mock_env_get.return_value = 'saas'
|
||||
|
||||
# Initialize posthog in the tracker module
|
||||
import openhands.utils.posthog_tracker as tracker
|
||||
|
||||
tracker.posthog = mock_posthog
|
||||
|
||||
# Set agent state to FINISHED
|
||||
await controller.set_agent_state_to(AgentState.FINISHED)
|
||||
|
||||
# Verify PostHog tracking was called
|
||||
mock_posthog.capture.assert_called_once()
|
||||
call_args = mock_posthog.capture.call_args
|
||||
|
||||
assert call_args[1]['distinct_id'] == 'test-user-456'
|
||||
assert call_args[1]['event'] == 'agent_task_completed'
|
||||
assert 'conversation_id' in call_args[1]['properties']
|
||||
assert call_args[1]['properties']['user_id'] == 'test-user-456'
|
||||
assert call_args[1]['properties']['app_mode'] == 'saas'
|
||||
|
||||
await controller.close()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_agent_finish_without_user_id(mock_agent_with_stats, mock_event_stream):
|
||||
"""Test tracking when user_id is None."""
|
||||
mock_agent, conversation_stats, llm_registry = mock_agent_with_stats
|
||||
|
||||
controller = AgentController(
|
||||
agent=mock_agent,
|
||||
event_stream=mock_event_stream,
|
||||
conversation_stats=conversation_stats,
|
||||
iteration_delta=10,
|
||||
sid='test-conversation-789',
|
||||
user_id=None,
|
||||
confirmation_mode=False,
|
||||
headless_mode=True,
|
||||
)
|
||||
|
||||
with (
|
||||
patch('openhands.utils.posthog_tracker.posthog') as mock_posthog,
|
||||
patch('os.environ.get') as mock_env_get,
|
||||
):
|
||||
mock_posthog.capture = MagicMock()
|
||||
mock_env_get.return_value = 'oss'
|
||||
|
||||
import openhands.utils.posthog_tracker as tracker
|
||||
|
||||
tracker.posthog = mock_posthog
|
||||
|
||||
await controller.set_agent_state_to(AgentState.FINISHED)
|
||||
|
||||
mock_posthog.capture.assert_called_once()
|
||||
call_args = mock_posthog.capture.call_args
|
||||
|
||||
# When user_id is None, distinct_id should be conversation_id
|
||||
assert call_args[1]['distinct_id'].startswith('conversation_')
|
||||
assert call_args[1]['properties']['user_id'] is None
|
||||
|
||||
await controller.close()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_other_states_dont_trigger_tracking(
|
||||
mock_agent_with_stats, mock_event_stream
|
||||
):
|
||||
"""Test that non-FINISHED states don't trigger tracking."""
|
||||
mock_agent, conversation_stats, llm_registry = mock_agent_with_stats
|
||||
|
||||
controller = AgentController(
|
||||
agent=mock_agent,
|
||||
event_stream=mock_event_stream,
|
||||
conversation_stats=conversation_stats,
|
||||
iteration_delta=10,
|
||||
sid='test-conversation-999',
|
||||
confirmation_mode=False,
|
||||
headless_mode=True,
|
||||
)
|
||||
|
||||
with patch('openhands.utils.posthog_tracker.posthog') as mock_posthog:
|
||||
mock_posthog.capture = MagicMock()
|
||||
|
||||
import openhands.utils.posthog_tracker as tracker
|
||||
|
||||
tracker.posthog = mock_posthog
|
||||
|
||||
# Try different states
|
||||
await controller.set_agent_state_to(AgentState.RUNNING)
|
||||
await controller.set_agent_state_to(AgentState.PAUSED)
|
||||
await controller.set_agent_state_to(AgentState.STOPPED)
|
||||
|
||||
# PostHog should not be called for non-FINISHED states
|
||||
mock_posthog.capture.assert_not_called()
|
||||
|
||||
await controller.close()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_tracking_error_doesnt_break_agent(
|
||||
mock_agent_with_stats, mock_event_stream
|
||||
):
|
||||
"""Test that tracking errors don't interrupt agent operation."""
|
||||
mock_agent, conversation_stats, llm_registry = mock_agent_with_stats
|
||||
|
||||
controller = AgentController(
|
||||
agent=mock_agent,
|
||||
event_stream=mock_event_stream,
|
||||
conversation_stats=conversation_stats,
|
||||
iteration_delta=10,
|
||||
sid='test-conversation-error',
|
||||
confirmation_mode=False,
|
||||
headless_mode=True,
|
||||
)
|
||||
|
||||
with patch('openhands.utils.posthog_tracker.posthog') as mock_posthog:
|
||||
mock_posthog.capture = MagicMock(side_effect=Exception('PostHog error'))
|
||||
|
||||
import openhands.utils.posthog_tracker as tracker
|
||||
|
||||
tracker.posthog = mock_posthog
|
||||
|
||||
# Should not raise an exception
|
||||
await controller.set_agent_state_to(AgentState.FINISHED)
|
||||
|
||||
# Agent state should still be FINISHED despite tracking error
|
||||
assert controller.state.agent_state == AgentState.FINISHED
|
||||
|
||||
await controller.close()
|
||||
@ -1,356 +0,0 @@
|
||||
"""Unit tests for PostHog tracking utilities."""
|
||||
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from openhands.utils.posthog_tracker import (
|
||||
alias_user_identities,
|
||||
track_agent_task_completed,
|
||||
track_credit_limit_reached,
|
||||
track_credits_purchased,
|
||||
track_user_signup_completed,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_posthog():
|
||||
"""Mock the posthog module."""
|
||||
with patch('openhands.utils.posthog_tracker.posthog') as mock_ph:
|
||||
mock_ph.capture = MagicMock()
|
||||
yield mock_ph
|
||||
|
||||
|
||||
def test_track_agent_task_completed_with_user_id(mock_posthog):
|
||||
"""Test tracking agent task completion with user ID."""
|
||||
# Initialize posthog manually in the test
|
||||
import openhands.utils.posthog_tracker as tracker
|
||||
|
||||
tracker.posthog = mock_posthog
|
||||
|
||||
track_agent_task_completed(
|
||||
conversation_id='test-conversation-123',
|
||||
user_id='user-456',
|
||||
app_mode='saas',
|
||||
)
|
||||
|
||||
mock_posthog.capture.assert_called_once_with(
|
||||
distinct_id='user-456',
|
||||
event='agent_task_completed',
|
||||
properties={
|
||||
'conversation_id': 'test-conversation-123',
|
||||
'user_id': 'user-456',
|
||||
'app_mode': 'saas',
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def test_track_agent_task_completed_without_user_id(mock_posthog):
|
||||
"""Test tracking agent task completion without user ID (anonymous)."""
|
||||
import openhands.utils.posthog_tracker as tracker
|
||||
|
||||
tracker.posthog = mock_posthog
|
||||
|
||||
track_agent_task_completed(
|
||||
conversation_id='test-conversation-789',
|
||||
user_id=None,
|
||||
app_mode='oss',
|
||||
)
|
||||
|
||||
mock_posthog.capture.assert_called_once_with(
|
||||
distinct_id='conversation_test-conversation-789',
|
||||
event='agent_task_completed',
|
||||
properties={
|
||||
'conversation_id': 'test-conversation-789',
|
||||
'user_id': None,
|
||||
'app_mode': 'oss',
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def test_track_agent_task_completed_default_app_mode(mock_posthog):
|
||||
"""Test tracking with default app_mode."""
|
||||
import openhands.utils.posthog_tracker as tracker
|
||||
|
||||
tracker.posthog = mock_posthog
|
||||
|
||||
track_agent_task_completed(
|
||||
conversation_id='test-conversation-999',
|
||||
user_id='user-111',
|
||||
)
|
||||
|
||||
mock_posthog.capture.assert_called_once_with(
|
||||
distinct_id='user-111',
|
||||
event='agent_task_completed',
|
||||
properties={
|
||||
'conversation_id': 'test-conversation-999',
|
||||
'user_id': 'user-111',
|
||||
'app_mode': 'unknown',
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def test_track_agent_task_completed_handles_errors(mock_posthog):
|
||||
"""Test that tracking errors are handled gracefully."""
|
||||
import openhands.utils.posthog_tracker as tracker
|
||||
|
||||
tracker.posthog = mock_posthog
|
||||
mock_posthog.capture.side_effect = Exception('PostHog API error')
|
||||
|
||||
# Should not raise an exception
|
||||
track_agent_task_completed(
|
||||
conversation_id='test-conversation-error',
|
||||
user_id='user-error',
|
||||
app_mode='saas',
|
||||
)
|
||||
|
||||
|
||||
def test_track_agent_task_completed_when_posthog_not_installed():
|
||||
"""Test tracking when posthog is not installed."""
|
||||
import openhands.utils.posthog_tracker as tracker
|
||||
|
||||
# Simulate posthog not being installed
|
||||
tracker.posthog = None
|
||||
|
||||
# Should not raise an exception
|
||||
track_agent_task_completed(
|
||||
conversation_id='test-conversation-no-ph',
|
||||
user_id='user-no-ph',
|
||||
app_mode='oss',
|
||||
)
|
||||
|
||||
|
||||
def test_track_user_signup_completed(mock_posthog):
|
||||
"""Test tracking user signup completion."""
|
||||
import openhands.utils.posthog_tracker as tracker
|
||||
|
||||
tracker.posthog = mock_posthog
|
||||
|
||||
track_user_signup_completed(
|
||||
user_id='test-user-123',
|
||||
signup_timestamp='2025-01-15T10:30:00Z',
|
||||
)
|
||||
|
||||
mock_posthog.capture.assert_called_once_with(
|
||||
distinct_id='test-user-123',
|
||||
event='user_signup_completed',
|
||||
properties={
|
||||
'user_id': 'test-user-123',
|
||||
'signup_timestamp': '2025-01-15T10:30:00Z',
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def test_track_user_signup_completed_handles_errors(mock_posthog):
|
||||
"""Test that user signup tracking errors are handled gracefully."""
|
||||
import openhands.utils.posthog_tracker as tracker
|
||||
|
||||
tracker.posthog = mock_posthog
|
||||
mock_posthog.capture.side_effect = Exception('PostHog API error')
|
||||
|
||||
# Should not raise an exception
|
||||
track_user_signup_completed(
|
||||
user_id='test-user-error',
|
||||
signup_timestamp='2025-01-15T12:00:00Z',
|
||||
)
|
||||
|
||||
|
||||
def test_track_user_signup_completed_when_posthog_not_installed():
|
||||
"""Test user signup tracking when posthog is not installed."""
|
||||
import openhands.utils.posthog_tracker as tracker
|
||||
|
||||
# Simulate posthog not being installed
|
||||
tracker.posthog = None
|
||||
|
||||
# Should not raise an exception
|
||||
track_user_signup_completed(
|
||||
user_id='test-user-no-ph',
|
||||
signup_timestamp='2025-01-15T13:00:00Z',
|
||||
)
|
||||
|
||||
|
||||
def test_track_credit_limit_reached_with_user_id(mock_posthog):
|
||||
"""Test tracking credit limit reached with user ID."""
|
||||
import openhands.utils.posthog_tracker as tracker
|
||||
|
||||
tracker.posthog = mock_posthog
|
||||
|
||||
track_credit_limit_reached(
|
||||
conversation_id='test-conversation-456',
|
||||
user_id='user-789',
|
||||
current_budget=10.50,
|
||||
max_budget=10.00,
|
||||
)
|
||||
|
||||
mock_posthog.capture.assert_called_once_with(
|
||||
distinct_id='user-789',
|
||||
event='credit_limit_reached',
|
||||
properties={
|
||||
'conversation_id': 'test-conversation-456',
|
||||
'user_id': 'user-789',
|
||||
'current_budget': 10.50,
|
||||
'max_budget': 10.00,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def test_track_credit_limit_reached_without_user_id(mock_posthog):
|
||||
"""Test tracking credit limit reached without user ID (anonymous)."""
|
||||
import openhands.utils.posthog_tracker as tracker
|
||||
|
||||
tracker.posthog = mock_posthog
|
||||
|
||||
track_credit_limit_reached(
|
||||
conversation_id='test-conversation-999',
|
||||
user_id=None,
|
||||
current_budget=5.25,
|
||||
max_budget=5.00,
|
||||
)
|
||||
|
||||
mock_posthog.capture.assert_called_once_with(
|
||||
distinct_id='conversation_test-conversation-999',
|
||||
event='credit_limit_reached',
|
||||
properties={
|
||||
'conversation_id': 'test-conversation-999',
|
||||
'user_id': None,
|
||||
'current_budget': 5.25,
|
||||
'max_budget': 5.00,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def test_track_credit_limit_reached_handles_errors(mock_posthog):
|
||||
"""Test that credit limit tracking errors are handled gracefully."""
|
||||
import openhands.utils.posthog_tracker as tracker
|
||||
|
||||
tracker.posthog = mock_posthog
|
||||
mock_posthog.capture.side_effect = Exception('PostHog API error')
|
||||
|
||||
# Should not raise an exception
|
||||
track_credit_limit_reached(
|
||||
conversation_id='test-conversation-error',
|
||||
user_id='user-error',
|
||||
current_budget=15.00,
|
||||
max_budget=10.00,
|
||||
)
|
||||
|
||||
|
||||
def test_track_credit_limit_reached_when_posthog_not_installed():
|
||||
"""Test credit limit tracking when posthog is not installed."""
|
||||
import openhands.utils.posthog_tracker as tracker
|
||||
|
||||
# Simulate posthog not being installed
|
||||
tracker.posthog = None
|
||||
|
||||
# Should not raise an exception
|
||||
track_credit_limit_reached(
|
||||
conversation_id='test-conversation-no-ph',
|
||||
user_id='user-no-ph',
|
||||
current_budget=8.00,
|
||||
max_budget=5.00,
|
||||
)
|
||||
|
||||
|
||||
def test_track_credits_purchased(mock_posthog):
|
||||
"""Test tracking credits purchased."""
|
||||
import openhands.utils.posthog_tracker as tracker
|
||||
|
||||
tracker.posthog = mock_posthog
|
||||
|
||||
track_credits_purchased(
|
||||
user_id='test-user-999',
|
||||
amount_usd=50.00,
|
||||
credits_added=50.00,
|
||||
stripe_session_id='cs_test_abc123',
|
||||
)
|
||||
|
||||
mock_posthog.capture.assert_called_once_with(
|
||||
distinct_id='test-user-999',
|
||||
event='credits_purchased',
|
||||
properties={
|
||||
'user_id': 'test-user-999',
|
||||
'amount_usd': 50.00,
|
||||
'credits_added': 50.00,
|
||||
'stripe_session_id': 'cs_test_abc123',
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def test_track_credits_purchased_handles_errors(mock_posthog):
|
||||
"""Test that credits purchased tracking errors are handled gracefully."""
|
||||
import openhands.utils.posthog_tracker as tracker
|
||||
|
||||
tracker.posthog = mock_posthog
|
||||
mock_posthog.capture.side_effect = Exception('PostHog API error')
|
||||
|
||||
# Should not raise an exception
|
||||
track_credits_purchased(
|
||||
user_id='test-user-error',
|
||||
amount_usd=100.00,
|
||||
credits_added=100.00,
|
||||
stripe_session_id='cs_test_error',
|
||||
)
|
||||
|
||||
|
||||
def test_track_credits_purchased_when_posthog_not_installed():
|
||||
"""Test credits purchased tracking when posthog is not installed."""
|
||||
import openhands.utils.posthog_tracker as tracker
|
||||
|
||||
# Simulate posthog not being installed
|
||||
tracker.posthog = None
|
||||
|
||||
# Should not raise an exception
|
||||
track_credits_purchased(
|
||||
user_id='test-user-no-ph',
|
||||
amount_usd=25.00,
|
||||
credits_added=25.00,
|
||||
stripe_session_id='cs_test_no_ph',
|
||||
)
|
||||
|
||||
|
||||
def test_alias_user_identities(mock_posthog):
|
||||
"""Test aliasing user identities.
|
||||
|
||||
Verifies that posthog.alias(previous_id, distinct_id) is called correctly
|
||||
where git_login is the previous_id and keycloak_user_id is the distinct_id.
|
||||
"""
|
||||
import openhands.utils.posthog_tracker as tracker
|
||||
|
||||
tracker.posthog = mock_posthog
|
||||
mock_posthog.alias = MagicMock()
|
||||
|
||||
alias_user_identities(
|
||||
keycloak_user_id='keycloak-123',
|
||||
git_login='git-user',
|
||||
)
|
||||
|
||||
# Verify: posthog.alias(previous_id='git-user', distinct_id='keycloak-123')
|
||||
mock_posthog.alias.assert_called_once_with('git-user', 'keycloak-123')
|
||||
|
||||
|
||||
def test_alias_user_identities_handles_errors(mock_posthog):
|
||||
"""Test that aliasing errors are handled gracefully."""
|
||||
import openhands.utils.posthog_tracker as tracker
|
||||
|
||||
tracker.posthog = mock_posthog
|
||||
mock_posthog.alias = MagicMock(side_effect=Exception('PostHog API error'))
|
||||
|
||||
# Should not raise an exception
|
||||
alias_user_identities(
|
||||
keycloak_user_id='keycloak-error',
|
||||
git_login='git-error',
|
||||
)
|
||||
|
||||
|
||||
def test_alias_user_identities_when_posthog_not_installed():
|
||||
"""Test aliasing when posthog is not installed."""
|
||||
import openhands.utils.posthog_tracker as tracker
|
||||
|
||||
# Simulate posthog not being installed
|
||||
tracker.posthog = None
|
||||
|
||||
# Should not raise an exception
|
||||
alias_user_identities(
|
||||
keycloak_user_id='keycloak-no-ph',
|
||||
git_login='git-no-ph',
|
||||
)
|
||||
Loading…
x
Reference in New Issue
Block a user