mirror of
https://github.com/dograh-hq/dograh.git
synced 2026-06-07 07:55:16 +02:00
feat: add posthog signup and signin events, enable backend posthog events for oss version
This commit is contained in:
parent
e556a60ab0
commit
7c22bc5156
10 changed files with 82 additions and 35 deletions
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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."""
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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}"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
4
ui/package-lock.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue