mirror of
https://github.com/OpenHands/OpenHands.git
synced 2025-12-26 13:52:43 +08:00
271 lines
7.8 KiB
Python
271 lines
7.8 KiB
Python
"""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),
|
|
},
|
|
)
|