Files
TimeTracker/app/utils/posthog_monitoring.py
Dries Peeters 7dd39ef55a feat(ci): enhance PostHog credential injection visibility in release builds
Improved the Release Build workflow to clearly show that PostHog and Sentry
credentials are being injected from the GitHub Secret Store, providing better
transparency and auditability.

Changes:
- Enhanced workflow step name to explicitly mention "GitHub Secrets"
- Added comprehensive logging with visual separators and clear sections
- Added before/after file content display showing placeholder replacement
- Added secret availability verification with format validation
- Added detailed error messages with step-by-step fix instructions
- Enhanced release summary to highlight successful credential injection
- Updated build configuration documentation with cross-references

Benefits:
- Developers can immediately see credentials come from GitHub Secret Store
- Security teams have clear audit trail of credential injection process
- Better troubleshooting with detailed error messages
- Secrets remain protected with proper redaction (first 8 + last 4 chars)
- Multiple validation steps ensure correct injection

The workflow now outputs 50+ lines of structured logging showing:
- Secret store location (Settings → Secrets and variables → Actions)
- Target file being modified (app/config/analytics_defaults.py)
- Verification that secrets are available
- Format validation (phc_* pattern for PostHog)
- Confirmation of successful placeholder replacement
- Summary with redacted credential previews

Workflow: .github/workflows/cd-release.yml
Documentation: docs/cicd/README_BUILD_CONFIGURATION.md

Fully backward compatible - no breaking changes.
2025-10-23 15:32:57 +02:00

480 lines
13 KiB
Python

"""
PostHog Monitoring Utilities
Track errors, performance metrics, and application health through PostHog.
"""
from typing import Optional, Dict, Any
import time
import os
from functools import wraps
from contextlib import contextmanager
def is_monitoring_enabled() -> bool:
"""Check if PostHog monitoring is enabled."""
return bool(os.getenv("POSTHOG_API_KEY", ""))
# ============================================================================
# Error Tracking
# ============================================================================
def track_error(
user_id: Any,
error_type: str,
error_message: str,
context: Optional[Dict] = None,
severity: str = "error"
) -> None:
"""
Track application errors in PostHog.
Args:
user_id: User ID (or 'anonymous' for unauthenticated)
error_type: Type of error (e.g., 'validation', 'database', 'api', '404')
error_message: Error message (sanitized, no PII)
context: Additional context (page, action, etc.)
severity: Error severity ('error', 'warning', 'critical')
Example:
try:
generate_report()
except ValueError as e:
track_error(
current_user.id,
"validation",
"Invalid date range for report",
{"report_type": "summary"}
)
raise
"""
if not is_monitoring_enabled():
return
from app import track_event
from flask import request
error_properties = {
"error_type": error_type,
"error_message": error_message[:500], # Limit message length
"severity": severity,
"timestamp": time.time(),
}
# Add context
if context:
error_properties["error_context"] = context
# Add request context if available
try:
if request:
error_properties.update({
"$current_url": request.url,
"$pathname": request.path,
"method": request.method,
})
except Exception:
pass
track_event(user_id, "error_occurred", error_properties)
def track_http_error(
user_id: Any,
status_code: int,
error_message: str,
context: Optional[Dict] = None
) -> None:
"""
Track HTTP errors (404, 500, etc.).
Args:
user_id: User ID
status_code: HTTP status code
error_message: Error message
context: Additional context
"""
track_error(
user_id,
f"http_{status_code}",
error_message,
{
"status_code": status_code,
**(context or {})
},
severity="warning" if status_code < 500 else "error"
)
def track_validation_error(
user_id: Any,
field: str,
error_message: str,
context: Optional[Dict] = None
) -> None:
"""
Track form validation errors.
Args:
user_id: User ID
field: Field that failed validation
error_message: Validation error message
context: Additional context
"""
track_error(
user_id,
"validation",
error_message,
{
"field": field,
**(context or {})
},
severity="warning"
)
# ============================================================================
# Performance Tracking
# ============================================================================
def track_performance(
user_id: Any,
metric_name: str,
duration_ms: float,
context: Optional[Dict] = None,
threshold_ms: Optional[float] = None
) -> None:
"""
Track performance metrics in PostHog.
Args:
user_id: User ID
metric_name: Name of the metric (e.g., 'report_generation', 'export_csv')
duration_ms: Duration in milliseconds
context: Additional context
threshold_ms: If provided, also track if duration exceeded threshold
Example:
start = time.time()
generate_report()
duration = (time.time() - start) * 1000
track_performance(
current_user.id,
"report_generation",
duration,
{"report_type": "summary", "entries_count": 100}
)
"""
if not is_monitoring_enabled():
return
from app import track_event
performance_properties = {
"metric_name": metric_name,
"duration_ms": duration_ms,
"duration_seconds": duration_ms / 1000,
**(context or {})
}
# Check if threshold exceeded
if threshold_ms is not None:
performance_properties["threshold_exceeded"] = duration_ms > threshold_ms
performance_properties["threshold_ms"] = threshold_ms
track_event(user_id, "performance_metric", performance_properties)
@contextmanager
def measure_performance(
user_id: Any,
metric_name: str,
context: Optional[Dict] = None,
threshold_ms: Optional[float] = None
):
"""
Context manager to measure performance of a code block.
Usage:
with measure_performance(current_user.id, "report_generation", {"type": "summary"}):
generate_report()
"""
start = time.time()
try:
yield
finally:
duration_ms = (time.time() - start) * 1000
track_performance(user_id, metric_name, duration_ms, context, threshold_ms)
def performance_tracked(metric_name: str, threshold_ms: Optional[float] = None):
"""
Decorator to track performance of a function.
Usage:
@performance_tracked("report_generation", threshold_ms=5000)
def generate_report():
# ... generate report
pass
"""
def decorator(f):
@wraps(f)
def wrapped(*args, **kwargs):
from flask_login import current_user
user_id = current_user.id if current_user.is_authenticated else "anonymous"
start = time.time()
try:
result = f(*args, **kwargs)
return result
finally:
duration_ms = (time.time() - start) * 1000
track_performance(
user_id,
metric_name,
duration_ms,
{"function": f.__name__},
threshold_ms
)
return wrapped
return decorator
# ============================================================================
# Database Performance Tracking
# ============================================================================
def track_query_performance(
user_id: Any,
query_type: str,
duration_ms: float,
context: Optional[Dict] = None
) -> None:
"""
Track database query performance.
Args:
user_id: User ID
query_type: Type of query (e.g., 'select', 'insert', 'update', 'complex_join')
duration_ms: Query duration in milliseconds
context: Additional context (table, filters, etc.)
"""
track_performance(
user_id,
f"db_query.{query_type}",
duration_ms,
{
"query_type": query_type,
**(context or {})
},
threshold_ms=1000 # Warn if query takes > 1 second
)
# ============================================================================
# API Performance Tracking
# ============================================================================
def track_api_call(
user_id: Any,
endpoint: str,
method: str,
status_code: int,
duration_ms: float,
context: Optional[Dict] = None
) -> None:
"""
Track API call performance and status.
Args:
user_id: User ID
endpoint: API endpoint
method: HTTP method
status_code: Response status code
duration_ms: Request duration in milliseconds
context: Additional context
"""
from app import track_event
track_event(user_id, "api_call", {
"endpoint": endpoint,
"method": method,
"status_code": status_code,
"duration_ms": duration_ms,
"success": 200 <= status_code < 400,
**(context or {})
})
# ============================================================================
# Page Load Tracking
# ============================================================================
def track_page_load(
user_id: Any,
page_name: str,
duration_ms: float,
context: Optional[Dict] = None
) -> None:
"""
Track page load performance.
Args:
user_id: User ID
page_name: Name of the page
duration_ms: Load duration in milliseconds
context: Additional context
"""
track_performance(
user_id,
f"page_load.{page_name}",
duration_ms,
{
"page_name": page_name,
**(context or {})
},
threshold_ms=3000 # Warn if page takes > 3 seconds
)
# ============================================================================
# Export/Report Performance
# ============================================================================
def track_export_performance(
user_id: Any,
export_type: str,
row_count: int,
duration_ms: float,
file_size_bytes: Optional[int] = None
) -> None:
"""
Track export generation performance.
Args:
user_id: User ID
export_type: Type of export (csv, excel, pdf)
row_count: Number of rows exported
duration_ms: Generation duration in milliseconds
file_size_bytes: Generated file size in bytes
"""
context = {
"export_type": export_type,
"row_count": row_count,
"rows_per_second": int(row_count / (duration_ms / 1000)) if duration_ms > 0 else 0,
}
if file_size_bytes:
context["file_size_bytes"] = file_size_bytes
context["file_size_kb"] = round(file_size_bytes / 1024, 2)
track_performance(
user_id,
f"export.{export_type}",
duration_ms,
context,
threshold_ms=10000 # Warn if export takes > 10 seconds
)
# ============================================================================
# Health Monitoring
# ============================================================================
def track_health_check(
status: str,
checks: Dict[str, bool],
response_time_ms: float
) -> None:
"""
Track health check results.
Args:
status: Overall health status ('healthy', 'degraded', 'unhealthy')
checks: Dict of individual health checks and their results
response_time_ms: Health check response time
"""
if not is_monitoring_enabled():
return
from app import track_event
track_event("system", "health_check", {
"status": status,
"checks": checks,
"all_healthy": all(checks.values()),
"response_time_ms": response_time_ms,
"failed_checks": [k for k, v in checks.items() if not v],
})
# ============================================================================
# Resource Usage Tracking
# ============================================================================
def track_resource_usage(
user_id: Any,
resource_type: str,
usage_amount: float,
unit: str,
context: Optional[Dict] = None
) -> None:
"""
Track resource usage (memory, CPU, disk, etc.).
Args:
user_id: User ID or 'system'
resource_type: Type of resource (memory, cpu, disk, api_calls)
usage_amount: Amount used
unit: Unit of measurement (mb, percent, count)
context: Additional context
"""
from app import track_event
track_event(user_id, "resource_usage", {
"resource_type": resource_type,
"usage_amount": usage_amount,
"unit": unit,
**(context or {})
})
# ============================================================================
# Slow Operation Detection
# ============================================================================
def track_slow_operation(
user_id: Any,
operation_name: str,
expected_ms: float,
actual_ms: float,
context: Optional[Dict] = None
) -> None:
"""
Track operations that exceed expected duration.
Args:
user_id: User ID
operation_name: Name of the operation
expected_ms: Expected duration in milliseconds
actual_ms: Actual duration in milliseconds
context: Additional context
"""
track_error(
user_id,
"slow_operation",
f"{operation_name} took {actual_ms}ms (expected {expected_ms}ms)",
{
"operation": operation_name,
"expected_ms": expected_ms,
"actual_ms": actual_ms,
"slowdown_factor": actual_ms / expected_ms if expected_ms > 0 else 0,
**(context or {})
},
severity="warning"
)