Files
TimeTracker/app/utils/posthog_funnels.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

321 lines
11 KiB
Python

"""
PostHog Funnel Tracking Utilities
This module provides utilities for tracking multi-step conversion funnels
in your application. Use this to understand where users drop off in complex workflows.
"""
from typing import Optional, Dict, Any
from datetime import datetime
import os
def is_funnel_tracking_enabled() -> bool:
"""Check if funnel tracking is enabled."""
return bool(os.getenv("POSTHOG_API_KEY", ""))
def track_funnel_step(
user_id: Any,
funnel_name: str,
step: str,
properties: Optional[Dict[str, Any]] = None
) -> None:
"""
Track a step in a conversion funnel.
This creates events that can be visualized as funnels in PostHog,
showing you where users drop off in multi-step processes.
Args:
user_id: The user ID (internal ID, not PII)
funnel_name: Name of the funnel (e.g., 'onboarding', 'invoice_generation')
step: Current step name (e.g., 'started', 'profile_completed')
properties: Additional properties to track with this step
Example:
# User starts project creation
track_funnel_step(user.id, "project_setup", "started")
# User enters basic info
track_funnel_step(user.id, "project_setup", "basic_info_entered", {
"has_description": True
})
# User completes setup
track_funnel_step(user.id, "project_setup", "completed")
"""
if not is_funnel_tracking_enabled():
return
from app import track_event
event_name = f"funnel.{funnel_name}.{step}"
funnel_properties = {
"funnel": funnel_name,
"step": step,
"step_timestamp": datetime.utcnow().isoformat(),
**(properties or {})
}
track_event(user_id, event_name, funnel_properties)
# ============================================================================
# Predefined Funnels for TimeTracker
# ============================================================================
class Funnels:
"""
Predefined funnel names for consistent tracking.
Define your funnels here to avoid typos and enable autocomplete.
"""
# User onboarding
ONBOARDING = "onboarding"
# Project management
PROJECT_SETUP = "project_setup"
# Invoice generation
INVOICE_GENERATION = "invoice_generation"
# Time tracking
TIME_TRACKING_FLOW = "time_tracking_flow"
# Export workflow
EXPORT_WORKFLOW = "export_workflow"
# Report generation
REPORT_GENERATION = "report_generation"
# ============================================================================
# Onboarding Funnel
# ============================================================================
def track_onboarding_started(user_id: Any, properties: Optional[Dict] = None):
"""Track when user signs up / account is created."""
track_funnel_step(user_id, Funnels.ONBOARDING, "signed_up", properties)
def track_onboarding_profile_completed(user_id: Any, properties: Optional[Dict] = None):
"""Track when user completes their profile."""
track_funnel_step(user_id, Funnels.ONBOARDING, "profile_completed", properties)
def track_onboarding_first_project(user_id: Any, properties: Optional[Dict] = None):
"""Track when user creates their first project."""
track_funnel_step(user_id, Funnels.ONBOARDING, "first_project_created", properties)
def track_onboarding_first_time_entry(user_id: Any, properties: Optional[Dict] = None):
"""Track when user logs their first time entry."""
track_funnel_step(user_id, Funnels.ONBOARDING, "first_time_logged", properties)
def track_onboarding_first_timer(user_id: Any, properties: Optional[Dict] = None):
"""Track when user starts their first timer."""
track_funnel_step(user_id, Funnels.ONBOARDING, "first_timer_started", properties)
def track_onboarding_week_1_completed(user_id: Any, properties: Optional[Dict] = None):
"""Track when user completes their first week of usage."""
track_funnel_step(user_id, Funnels.ONBOARDING, "week_1_completed", properties)
# ============================================================================
# Project Setup Funnel
# ============================================================================
def track_project_setup_started(user_id: Any, properties: Optional[Dict] = None):
"""Track when user starts creating a new project."""
track_funnel_step(user_id, Funnels.PROJECT_SETUP, "started", properties)
def track_project_setup_basic_info(user_id: Any, properties: Optional[Dict] = None):
"""Track when user enters basic project info."""
track_funnel_step(user_id, Funnels.PROJECT_SETUP, "basic_info_entered", properties)
def track_project_setup_billing_configured(user_id: Any, properties: Optional[Dict] = None):
"""Track when user configures billing info."""
track_funnel_step(user_id, Funnels.PROJECT_SETUP, "billing_configured", properties)
def track_project_setup_tasks_added(user_id: Any, properties: Optional[Dict] = None):
"""Track when user adds tasks to project."""
track_funnel_step(user_id, Funnels.PROJECT_SETUP, "tasks_added", properties)
def track_project_setup_completed(user_id: Any, properties: Optional[Dict] = None):
"""Track when user completes project setup."""
track_funnel_step(user_id, Funnels.PROJECT_SETUP, "completed", properties)
# ============================================================================
# Invoice Generation Funnel
# ============================================================================
def track_invoice_page_viewed(user_id: Any, properties: Optional[Dict] = None):
"""Track when user views invoice page."""
track_funnel_step(user_id, Funnels.INVOICE_GENERATION, "page_viewed", properties)
def track_invoice_project_selected(user_id: Any, properties: Optional[Dict] = None):
"""Track when user selects project/client for invoice."""
track_funnel_step(user_id, Funnels.INVOICE_GENERATION, "project_selected", properties)
def track_invoice_previewed(user_id: Any, properties: Optional[Dict] = None):
"""Track when user previews invoice."""
track_funnel_step(user_id, Funnels.INVOICE_GENERATION, "invoice_previewed", properties)
def track_invoice_generated(user_id: Any, properties: Optional[Dict] = None):
"""Track when user generates/downloads invoice."""
track_funnel_step(user_id, Funnels.INVOICE_GENERATION, "invoice_generated", properties)
# ============================================================================
# Time Tracking Flow
# ============================================================================
def track_time_tracking_started(user_id: Any, properties: Optional[Dict] = None):
"""Track when user opens time tracking interface."""
track_funnel_step(user_id, Funnels.TIME_TRACKING_FLOW, "interface_opened", properties)
def track_time_tracking_timer_started(user_id: Any, properties: Optional[Dict] = None):
"""Track when user starts a timer."""
track_funnel_step(user_id, Funnels.TIME_TRACKING_FLOW, "timer_started", properties)
def track_time_tracking_timer_stopped(user_id: Any, properties: Optional[Dict] = None):
"""Track when user stops a timer."""
track_funnel_step(user_id, Funnels.TIME_TRACKING_FLOW, "timer_stopped", properties)
def track_time_tracking_notes_added(user_id: Any, properties: Optional[Dict] = None):
"""Track when user adds notes to time entry."""
track_funnel_step(user_id, Funnels.TIME_TRACKING_FLOW, "notes_added", properties)
def track_time_tracking_saved(user_id: Any, properties: Optional[Dict] = None):
"""Track when time entry is saved."""
track_funnel_step(user_id, Funnels.TIME_TRACKING_FLOW, "entry_saved", properties)
# ============================================================================
# Export Workflow
# ============================================================================
def track_export_started(user_id: Any, properties: Optional[Dict] = None):
"""Track when user initiates export."""
track_funnel_step(user_id, Funnels.EXPORT_WORKFLOW, "started", properties)
def track_export_format_selected(user_id: Any, properties: Optional[Dict] = None):
"""Track when user selects export format."""
track_funnel_step(user_id, Funnels.EXPORT_WORKFLOW, "format_selected", properties)
def track_export_filters_applied(user_id: Any, properties: Optional[Dict] = None):
"""Track when user applies filters to export."""
track_funnel_step(user_id, Funnels.EXPORT_WORKFLOW, "filters_applied", properties)
def track_export_downloaded(user_id: Any, properties: Optional[Dict] = None):
"""Track when export is downloaded."""
track_funnel_step(user_id, Funnels.EXPORT_WORKFLOW, "downloaded", properties)
# ============================================================================
# Report Generation
# ============================================================================
def track_report_page_viewed(user_id: Any, properties: Optional[Dict] = None):
"""Track when user views reports page."""
track_funnel_step(user_id, Funnels.REPORT_GENERATION, "page_viewed", properties)
def track_report_type_selected(user_id: Any, properties: Optional[Dict] = None):
"""Track when user selects report type."""
track_funnel_step(user_id, Funnels.REPORT_GENERATION, "type_selected", properties)
def track_report_filters_applied(user_id: Any, properties: Optional[Dict] = None):
"""Track when user applies filters."""
track_funnel_step(user_id, Funnels.REPORT_GENERATION, "filters_applied", properties)
def track_report_generated(user_id: Any, properties: Optional[Dict] = None):
"""Track when report is generated."""
track_funnel_step(user_id, Funnels.REPORT_GENERATION, "generated", properties)
# ============================================================================
# Helper Functions
# ============================================================================
def track_funnel_abandonment(
user_id: Any,
funnel_name: str,
last_step_completed: str,
reason: Optional[str] = None
) -> None:
"""
Track when a user abandons a funnel.
Args:
user_id: User ID
funnel_name: Name of the funnel
last_step_completed: Last step the user completed before abandoning
reason: Optional reason for abandonment (e.g., 'error', 'timeout')
"""
from app import track_event
track_event(user_id, f"funnel.{funnel_name}.abandoned", {
"funnel": funnel_name,
"last_step": last_step_completed,
"abandonment_reason": reason,
"timestamp": datetime.utcnow().isoformat()
})
def get_funnel_context(funnel_name: str, additional_context: Optional[Dict] = None) -> Dict:
"""
Get standardized context for funnel events.
Args:
funnel_name: Name of the funnel
additional_context: Additional context to include
Returns:
Dict of context properties
"""
from flask import request
context = {
"funnel": funnel_name,
"timestamp": datetime.utcnow().isoformat(),
}
# Add request context if available
try:
if request:
context.update({
"referrer": request.referrer,
"user_agent": request.user_agent.string,
})
except Exception:
pass
# Add additional context
if additional_context:
context.update(additional_context)
return context