OpenHands/openhands/utils/posthog_tracker.py
sp.wack 8192184d3e
chore(backend): Add better PostHog tracking (#11655)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-11-12 16:47:21 +00:00

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),
},
)