Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# PostHog configuration
POSTHOG_PROJECT_TOKEN=phc_your_project_token_here
POSTHOG_HOST=https://us.i.posthog.com
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.env
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
# User Service - Pure Python dependencies
# AI Meeting Summarizer - Pure Python dependencies
# This service uses only Python standard library
# No external dependencies required
# PostHog analytics and log export
posthog==7.78.0
python-dotenv==1.2.4
opentelemetry-api==1.45.1
opentelemetry-sdk==1.45.1
opentelemetry-exporter-otlp==1.45.1
154 changes: 154 additions & 0 deletions apps/basic-integration/python/meeting-summarizer/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
Automatically summarize meetings with AI-powered analysis.
"""

import atexit
import json
import logging
import signal
Expand All @@ -19,12 +20,101 @@
from threading import Lock
import traceback
import mimetypes
from functools import wraps

from dotenv import load_dotenv

from database import UserDatabase
from models import User, Meeting
from ai_summarizer import AISummarizer


load_dotenv()


def _is_development():
"""Return whether the process is running with development diagnostics."""
environment = os.getenv('APP_ENV', os.getenv('ENVIRONMENT', '')).lower()
return environment in {'development', 'dev', 'debug'} or os.getenv('DEBUG', '').lower() == 'true'


def initialize_posthog():
"""Create the process-wide PostHog client when it is configured."""
project_token = os.getenv('POSTHOG_PROJECT_TOKEN')
host = os.getenv('POSTHOG_HOST')

for variable_name, value in (
('POSTHOG_PROJECT_TOKEN', project_token),
('POSTHOG_HOST', host),
):
if value:
continue
if _is_development():
raise RuntimeError(
f'{variable_name} variable required by PostHog is missing or un-configured, '
f'this causes events to be silently missed. This error stops appearing once '
f'{variable_name} is configured'
)
return None

from posthog import Posthog

client = Posthog(
project_token,
host=host,
enable_exception_autocapture=True,
)
atexit.register(client.shutdown)
return client


posthog_client = initialize_posthog()
posthog_log = logging.getLogger('posthog.logs')


def configure_posthog_log_export():
"""Export only purpose-built application logs to PostHog over OTLP."""
project_token = os.getenv('POSTHOG_PROJECT_TOKEN')
host = os.getenv('POSTHOG_HOST')
if not project_token or not host:
return

from opentelemetry._logs import set_logger_provider
from opentelemetry.exporter.otlp.proto.http._log_exporter import OTLPLogExporter
from opentelemetry.sdk._logs import LoggerProvider, LoggingHandler
from opentelemetry.sdk._logs.export import BatchLogRecordProcessor

logger_provider = LoggerProvider()
set_logger_provider(logger_provider)
logger_provider.add_log_record_processor(
BatchLogRecordProcessor(
OTLPLogExporter(
endpoint=f'{host.rstrip("/")}/i/v1/logs',
headers={'Authorization': f'Bearer {project_token}'},
)
)
)
posthog_log.setLevel(logging.INFO)
posthog_log.addHandler(LoggingHandler(logger_provider=logger_provider))
atexit.register(logger_provider.shutdown)


def with_posthog_request_context(handler):
"""Create an isolated PostHog context for each HTTP request."""
@wraps(handler)
def wrapped(self, *args, **kwargs):
if not posthog_client:
return handler(self, *args, **kwargs)

with posthog_client.new_context(fresh=True):
user = self._get_current_user()
if user:
self._identify_posthog_user(user)
return handler(self, *args, **kwargs)

return wrapped


# Session management
class SessionManager:
"""Simple session management"""
Expand Down Expand Up @@ -108,6 +198,22 @@ def _get_current_user(self):
return self.db.get_user(session['user_id'])
return None

def _identify_posthog_user(self, user, set_person_properties=False):
"""Associate the current request context with the authenticated user."""
if not posthog_client:
return

posthog_client.identify_context(user.user_id)

if set_person_properties:
properties = {
'email': user.email,
'username': user.username,
}
if user.full_name:
properties['full_name'] = user.full_name
posthog_client.set(user.user_id, properties=properties)

def _serve_static_file(self, file_path):
"""Serve a static file"""
try:
Expand Down Expand Up @@ -138,6 +244,7 @@ def _serve_static_file(self, file_path):
self._set_headers(500)
self.wfile.write(b'Internal server error')

@with_posthog_request_context
def do_GET(self):
"""Handle GET requests"""
try:
Expand Down Expand Up @@ -223,6 +330,16 @@ def do_GET(self):
meeting_id = path.split('/')[-1]
meeting = self.db.get_meeting(meeting_id)
if meeting and meeting.user_id == user.user_id:
if posthog_client:
posthog_client.capture(
'meeting_viewed',
properties={
'duration_minutes': meeting.duration_minutes,
'action_item_count': len(meeting.action_items),
'key_point_count': len(meeting.key_points),
'participant_count': len(meeting.participants),
},
)
self._send_json(meeting.to_dict())
else:
self._send_json({'error': 'Meeting not found'}, 404)
Expand All @@ -246,6 +363,7 @@ def do_GET(self):
logging.error(f"Error in GET request: {e}\n{traceback.format_exc()}")
self._send_json({'error': 'Internal server error'}, 500)

@with_posthog_request_context
def do_POST(self):
"""Handle POST requests"""
try:
Expand Down Expand Up @@ -273,6 +391,11 @@ def do_POST(self):
logging.info(f"Login successful for: {email}")
# Create session
session_id = self.sessions.create_session(user.user_id)
# Login changes identity during this request, so bind the
# authenticated user before later instrumentation runs.
self._identify_posthog_user(user, set_person_properties=True)
if posthog_client:
posthog_client.capture('user_logged_in')

# Send response with session cookie
self.send_response(200)
Expand All @@ -296,8 +419,11 @@ def do_POST(self):

# API: Logout
if path == '/api/auth/logout':
current_user = self._get_current_user()
session_id = self._get_session_id()
if session_id:
if posthog_client and current_user:
posthog_client.capture('user_logged_out')
self.sessions.delete_session(session_id)

self._set_headers(200, 'application/json')
Expand Down Expand Up @@ -370,6 +496,19 @@ def do_POST(self):
)

if self.db.create_meeting(meeting):
meeting_attributes = {
'transcript_word_count': len(transcript.split()),
'action_item_count': len(action_items),
'key_point_count': len(key_points),
'participant_count': len(participants),
'duration_minutes': duration,
}
if posthog_client:
posthog_client.capture(
'meeting_created',
properties=meeting_attributes,
)
posthog_log.info('meeting_analysis_completed', extra=meeting_attributes)
self._send_json(meeting.to_dict(), 201)
else:
self._send_json({'error': 'Failed to create meeting'}, 500)
Expand All @@ -383,6 +522,7 @@ def do_POST(self):
logging.error(f"Error in POST request: {e}\n{traceback.format_exc()}")
self._send_json({'error': 'Internal server error'}, 500)

@with_posthog_request_context
def do_PUT(self):
"""Handle PUT requests"""
try:
Expand Down Expand Up @@ -412,6 +552,7 @@ def do_PUT(self):
logging.error(f"Error in PUT request: {e}\n{traceback.format_exc()}")
self._send_json({'error': 'Internal server error'}, 500)

@with_posthog_request_context
def do_DELETE(self):
"""Handle DELETE requests"""
try:
Expand Down Expand Up @@ -449,6 +590,18 @@ def do_DELETE(self):
return

if self.db.delete_meeting(meeting_id):
meeting_attributes = {
'duration_minutes': meeting.duration_minutes,
'action_item_count': len(meeting.action_items),
'key_point_count': len(meeting.key_points),
'participant_count': len(meeting.participants),
}
if posthog_client:
posthog_client.capture(
'meeting_deleted',
properties=meeting_attributes,
)
posthog_log.info('meeting_deleted', extra=meeting_attributes)
self._send_json({'success': True})
else:
self._send_json({'error': 'Failed to delete meeting'}, 500)
Expand Down Expand Up @@ -550,6 +703,7 @@ def create_demo_users(db):
def main():
"""Main entry point"""
setup_logging()
configure_posthog_log_export()
signal.signal(signal.SIGINT, signal_handler)
signal.signal(signal.SIGTERM, signal_handler)

Expand Down
Loading