feat: add posthog signup and signin events, enable backend posthog events for oss version

This commit is contained in:
Sabiha Khan 2026-04-23 15:55:21 +05:30
parent e556a60ab0
commit 7c22bc5156
10 changed files with 82 additions and 35 deletions

View file

@ -359,7 +359,7 @@ async def test_client_factory(db_session):
Usage:
async def test_something(test_client_factory, db_session):
# Create a custom user
user = await db_session.get_or_create_user_by_provider_id("custom_user_123")
user, _ = await db_session.get_or_create_user_by_provider_id("custom_user_123")
# Create a test client for this user
async with test_client_factory(user) as client:

View file

@ -11,7 +11,10 @@ from api.schemas.user_configuration import UserConfiguration
class UserClient(BaseDBClient):
async def get_or_create_user_by_provider_id(self, provider_id: str) -> UserModel:
async def get_or_create_user_by_provider_id(
self, provider_id: str
) -> tuple[UserModel, bool]:
"""Return (user, was_created) tuple."""
async with self.async_session() as session:
# First try to get existing user
result = await session.execute(
@ -19,36 +22,39 @@ class UserClient(BaseDBClient):
)
user = result.scalars().first()
if user is not None:
return user, False
# Use PostgreSQL's INSERT ... ON CONFLICT DO NOTHING
# This is atomic and handles race conditions at the database level
from sqlalchemy.dialects.postgresql import insert
stmt = insert(UserModel.__table__).values(
provider_id=provider_id,
created_at=datetime.now(timezone.utc),
selected_organization_id=None, # Will be set later
is_superuser=False, # Default value
)
# ON CONFLICT DO NOTHING - if another request already inserted, this becomes a no-op
stmt = stmt.on_conflict_do_nothing(index_elements=["provider_id"])
result = await session.execute(stmt)
await session.commit()
was_created = result.rowcount > 0
# Now fetch the user (either the one we just created or the one that existed)
result = await session.execute(
select(UserModel).where(UserModel.provider_id == provider_id)
)
user = result.scalars().first()
if user is None:
# Use PostgreSQL's INSERT ... ON CONFLICT DO NOTHING
# This is atomic and handles race conditions at the database level
from sqlalchemy.dialects.postgresql import insert
stmt = insert(UserModel.__table__).values(
provider_id=provider_id,
created_at=datetime.now(timezone.utc),
selected_organization_id=None, # Will be set later
is_superuser=False, # Default value
# This should never happen, but handle it just in case
error_msg = (
f"Failed to create or fetch user with provider_id {provider_id}"
)
# ON CONFLICT DO NOTHING - if another request already inserted, this becomes a no-op
stmt = stmt.on_conflict_do_nothing(index_elements=["provider_id"])
result = await session.execute(stmt)
await session.commit()
# Now fetch the user (either the one we just created or the one that existed)
result = await session.execute(
select(UserModel).where(UserModel.provider_id == provider_id)
)
user = result.scalars().first()
if user is None:
# This should never happen, but handle it just in case
error_msg = (
f"Failed to create or fetch user with provider_id {provider_id}"
)
raise ValueError(error_msg)
return user
raise ValueError(error_msg)
return user, was_created
async def get_user_by_id(self, user_id: int) -> UserModel | None:
"""Fetch a user by their internal ID."""

View file

@ -155,3 +155,5 @@ class PostHogEvent(str, Enum):
KNOWLEDGE_BASE_CREATED = "knowledge_base_created"
TOOL_CREATED = "tool_created"
AGENT_EMBEDDED = "agent_embedded"
SIGNED_UP = "signed_up"
SIGNED_IN = "signed_in"

View file

@ -3,8 +3,10 @@ from loguru import logger
from api.db import db_client
from api.db.models import UserModel
from api.enums import PostHogEvent
from api.schemas.auth import AuthResponse, LoginRequest, SignupRequest, UserResponse
from api.services.auth.depends import create_user_configuration_with_mps_key, get_user
from api.services.posthog_client import capture_event
from api.utils.auth import create_jwt_token, hash_password, verify_password
router = APIRouter(
@ -53,6 +55,15 @@ async def signup(request: SignupRequest):
# Create JWT token
token = create_jwt_token(user.id, request.email)
capture_event(
distinct_id=str(user.provider_id),
event=PostHogEvent.SIGNED_UP,
properties={
"organization_id": organization.id,
"auth_provider": "local",
},
)
return AuthResponse(
token=token,
user=UserResponse(
@ -79,6 +90,15 @@ async def login(request: LoginRequest):
# Create JWT token
token = create_jwt_token(user.id, user.email)
capture_event(
distinct_id=str(user.provider_id),
event=PostHogEvent.SIGNED_IN,
properties={
"organization_id": user.selected_organization_id,
"auth_provider": "local",
},
)
return AuthResponse(
token=token,
user=UserResponse(

View file

@ -8,9 +8,11 @@ from pydantic import ValidationError
from api.constants import AUTH_PROVIDER, DOGRAH_MPS_SECRET_KEY, MPS_API_URL
from api.db import db_client
from api.db.models import UserModel
from api.enums import PostHogEvent
from api.schemas.user_configuration import UserConfiguration
from api.services.auth.stack_auth import stackauth
from api.services.configuration.registry import ServiceProviders
from api.services.posthog_client import capture_event
from api.utils.auth import decode_jwt_token
@ -54,7 +56,7 @@ async def get_user(
# ------------------------------------------------------------------
try:
user_model = await db_client.get_or_create_user_by_provider_id(stack_user["id"])
user_model, user_was_created = await db_client.get_or_create_user_by_provider_id(stack_user["id"])
# Sync email from Stack Auth if available and not already set
stack_email = stack_user.get("primary_email_verified") and stack_user.get(
@ -63,6 +65,15 @@ async def get_user(
if stack_email and user_model.email != stack_email:
await db_client.update_user_email(user_model.id, stack_email)
user_model.email = stack_email
if user_was_created:
capture_event(
distinct_id=str(stack_user["id"]),
event=PostHogEvent.SIGNED_UP,
properties={
"auth_provider": "stack",
},
)
except Exception as e:
raise HTTPException(
status_code=500, detail=f"Error while creating user from database {e}"

View file

@ -1,7 +1,7 @@
from loguru import logger
from posthog import Posthog
from api.constants import POSTHOG_API_KEY, POSTHOG_HOST
from api.constants import ENABLE_TELEMETRY, POSTHOG_API_KEY, POSTHOG_HOST
_posthog_client: Posthog | None = None
@ -9,7 +9,7 @@ _posthog_client: Posthog | None = None
def get_posthog() -> Posthog | None:
"""Return the lazily-initialised PostHog client, or None if not configured."""
global _posthog_client
if _posthog_client is None and POSTHOG_API_KEY:
if _posthog_client is None and POSTHOG_API_KEY and ENABLE_TELEMETRY:
_posthog_client = Posthog(POSTHOG_API_KEY, host=POSTHOG_HOST)
return _posthog_client

View file

@ -123,6 +123,11 @@ services:
OSS_JWT_SECRET: "${OSS_JWT_SECRET:-ChangeMeInProduction}"
# Telemetry
ENABLE_TELEMETRY: "${ENABLE_TELEMETRY:-true}"
POSTHOG_API_KEY: "phc_ItizB1dP6yv7ZYobbcqrpxTdbomDA8hJFSEmAMdYvIr"
POSTHOG_HOST: "https://us.i.posthog.com"
ports:
- "8000:8000"
depends_on:

4
ui/package-lock.json generated
View file

@ -1,12 +1,12 @@
{
"name": "ui",
"version": "1.24.0",
"version": "1.26.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "ui",
"version": "1.24.0",
"version": "1.26.0",
"dependencies": {
"@dagrejs/dagre": "^1.1.4",
"@nangohq/frontend": "^0.69.47",

View file

@ -3,6 +3,7 @@
import posthog from 'posthog-js';
import { useEffect } from 'react';
import { PostHogEvent } from '@/constants/posthog-events';
import { useAuth } from '@/lib/auth';
/**
@ -44,6 +45,7 @@ export default function PostHogIdentify() {
...(email && { email }),
...(name && { name }),
});
posthog.capture(PostHogEvent.SIGNED_IN);
} catch (err) {
console.warn('Failed to identify user in PostHog', err);
}

View file

@ -8,6 +8,7 @@ export const PostHogEvent = {
RECORDING_PLAYED: "recording_played",
TRANSCRIPT_VIEWED: "transcript_viewed",
WEB_CALL_INITIATED: "web_call_initiated",
SIGNED_IN: "signed_in",
GITHUB_STAR_CLICKED: "github_star_clicked",
SLACK_COMMUNITY_CLICKED: "slack_community_clicked",
} as const;