Skip to content
Merged
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
59 changes: 26 additions & 33 deletions .github/workflows/cd-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -370,7 +370,8 @@ jobs:

- name: Inject analytics configuration from GitHub Secrets
env:
POSTHOG_API_KEY: ${{ secrets.POSTHOG_API_KEY }}
OTEL_EXPORTER_OTLP_ENDPOINT: ${{ secrets.OTEL_EXPORTER_OTLP_ENDPOINT }}
OTEL_EXPORTER_OTLP_TOKEN: ${{ secrets.OTEL_EXPORTER_OTLP_TOKEN }}
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
run: |
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
Expand All @@ -384,18 +385,17 @@ jobs:
# Show file before injection
echo "📄 File content BEFORE injection (showing placeholders):"
echo "──────────────────────────────────────────────────────────────────"
grep -E "(POSTHOG_API_KEY_DEFAULT|SENTRY_DSN_DEFAULT)" app/config/analytics_defaults.py || true
grep -E "(OTEL_EXPORTER_OTLP_ENDPOINT_DEFAULT|OTEL_EXPORTER_OTLP_TOKEN_DEFAULT|SENTRY_DSN_DEFAULT)" app/config/analytics_defaults.py || true
echo "──────────────────────────────────────────────────────────────────"
echo ""

# Verify secrets are available
echo "🔍 Verifying GitHub Secrets availability..."
if [ -z "$POSTHOG_API_KEY" ]; then
echo "⚠️ POSTHOG_API_KEY secret not set (optional - analytics disabled in this build)"
echo " → To enable: Add POSTHOG_API_KEY secret (format: phc_xxxxx) in Settings → Secrets"
if [ -z "$OTEL_EXPORTER_OTLP_ENDPOINT" ] || [ -z "$OTEL_EXPORTER_OTLP_TOKEN" ]; then
echo "⚠️ Grafana OTLP secrets not fully set (telemetry sink disabled in this build)"
echo " → To enable: Add OTEL_EXPORTER_OTLP_ENDPOINT and OTEL_EXPORTER_OTLP_TOKEN in Settings → Secrets"
else
echo "✅ POSTHOG_API_KEY secret found in GitHub Secret Store"
echo " → Format: ${POSTHOG_API_KEY:0:8}***${POSTHOG_API_KEY: -4} (${#POSTHOG_API_KEY} characters)"
echo "✅ Grafana OTLP secrets found in GitHub Secret Store"
fi

if [ -z "$SENTRY_DSN" ]; then
Expand All @@ -409,28 +409,34 @@ jobs:

# Perform replacement (use empty string if secrets not set)
echo "🔧 Injecting secrets into application configuration..."
sed -i "s|%%POSTHOG_API_KEY_PLACEHOLDER%%|${POSTHOG_API_KEY:-}|g" app/config/analytics_defaults.py
sed -i "s|%%OTEL_EXPORTER_OTLP_ENDPOINT_PLACEHOLDER%%|${OTEL_EXPORTER_OTLP_ENDPOINT:-}|g" app/config/analytics_defaults.py
sed -i "s|%%OTEL_EXPORTER_OTLP_TOKEN_PLACEHOLDER%%|${OTEL_EXPORTER_OTLP_TOKEN:-}|g" app/config/analytics_defaults.py
sed -i "s|%%SENTRY_DSN_PLACEHOLDER%%|${SENTRY_DSN:-}|g" app/config/analytics_defaults.py
echo " → Placeholders replaced with secret values (or empty if not set)"
echo ""

# Show file after injection (redacted)
echo "📄 File content AFTER injection (secrets redacted):"
echo "──────────────────────────────────────────────────────────────────"
grep -E "(POSTHOG_API_KEY_DEFAULT|SENTRY_DSN_DEFAULT)" app/config/analytics_defaults.py | \
grep -E "(OTEL_EXPORTER_OTLP_ENDPOINT_DEFAULT|OTEL_EXPORTER_OTLP_TOKEN_DEFAULT|SENTRY_DSN_DEFAULT)" app/config/analytics_defaults.py | \
sed 's/\(phc_[a-zA-Z0-9]\{8\}\)[a-zA-Z0-9]*\([a-zA-Z0-9]\{4\}\)/\1***\2/g' | \
sed 's|\(https://[^@]*@[^/]*\)|***REDACTED***|g' || true
echo "──────────────────────────────────────────────────────────────────"
echo ""

# Verify placeholders were replaced
echo "🔍 Verifying injection was successful..."
if grep -q "%%POSTHOG_API_KEY_PLACEHOLDER%%" app/config/analytics_defaults.py; then
echo "❌ ERROR: PostHog API key placeholder was NOT replaced!"
echo " The placeholder '%%POSTHOG_API_KEY_PLACEHOLDER%%' is still present in the file."
if grep -q "%%OTEL_EXPORTER_OTLP_ENDPOINT_PLACEHOLDER%%" app/config/analytics_defaults.py; then
echo "❌ ERROR: Grafana endpoint placeholder was NOT replaced!"
exit 1
else
echo "✅ PostHog API key placeholder successfully replaced"
echo "✅ Grafana endpoint placeholder successfully replaced"
fi
if grep -q "%%OTEL_EXPORTER_OTLP_TOKEN_PLACEHOLDER%%" app/config/analytics_defaults.py; then
echo "❌ ERROR: Grafana token placeholder was NOT replaced!"
exit 1
else
echo "✅ Grafana token placeholder successfully replaced"
fi

if grep -q "%%SENTRY_DSN_PLACEHOLDER%%" app/config/analytics_defaults.py; then
Expand All @@ -441,30 +447,18 @@ jobs:
echo "✅ Sentry DSN placeholder successfully replaced"
fi

# Verify PostHog key format only when it was provided (non-empty)
if [ -n "$POSTHOG_API_KEY" ]; then
if ! grep -q 'POSTHOG_API_KEY_DEFAULT = "phc_' app/config/analytics_defaults.py; then
echo "❌ ERROR: PostHog API key format validation FAILED!"
echo " Expected format: phc_* (PostHog Cloud key)"
echo " Please verify the secret value in GitHub Settings."
exit 1
else
echo "✅ PostHog API key format validated (phc_* pattern confirmed)"
fi
else
echo "✅ PostHog API key skipped (not configured - analytics disabled)"
fi
echo "✅ Grafana OTLP values injected"
echo ""

echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "✅ SUCCESS: Analytics credentials injected from GitHub Secret Store"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
echo "📊 Injected Credentials Summary:"
if [ -n "$POSTHOG_API_KEY" ]; then
echo " • PostHog API Key: phc_***${POSTHOG_API_KEY: -4} ✓"
if [ -n "$OTEL_EXPORTER_OTLP_ENDPOINT" ] && [ -n "$OTEL_EXPORTER_OTLP_TOKEN" ]; then
echo " • Grafana OTLP: configured ✓"
else
echo " • PostHog API Key: [Not configured - analytics disabled] ⚠️"
echo " • Grafana OTLP: [Not configured - telemetry sink disabled] ⚠️"
fi
if [ -n "$SENTRY_DSN" ]; then
echo " • Sentry DSN: ${SENTRY_DSN:0:20}*** ✓"
Expand Down Expand Up @@ -597,10 +591,9 @@ jobs:
# See docs/analytics.md for configuration details
- SENTRY_DSN=\${SENTRY_DSN:-}
- SENTRY_TRACES_RATE=\${SENTRY_TRACES_RATE:-0.0}
- POSTHOG_API_KEY=\${POSTHOG_API_KEY:-}
- POSTHOG_HOST=\${POSTHOG_HOST:-https://app.posthog.com}
- OTEL_EXPORTER_OTLP_ENDPOINT=\${OTEL_EXPORTER_OTLP_ENDPOINT:-}
- OTEL_EXPORTER_OTLP_TOKEN=\${OTEL_EXPORTER_OTLP_TOKEN:-}
- ENABLE_TELEMETRY=\${ENABLE_TELEMETRY:-false}
- TELE_URL=\${TELE_URL:-}
- TELE_SALT=\${TELE_SALT:-8f4a7b2e9c1d6f3a5e8b4c7d2a9f6e3b1c8d5a7f2e9b4c6d3a8f5e1b7c4d9a2f}

# Expose only internally; nginx publishes ports
Expand Down Expand Up @@ -1337,7 +1330,7 @@ jobs:
echo "" >> $GITHUB_STEP_SUMMARY
echo "### 🔐 Analytics Configuration" >> $GITHUB_STEP_SUMMARY
echo "Analytics credentials were **successfully injected** from GitHub Secret Store:" >> $GITHUB_STEP_SUMMARY
echo "- ✅ **PostHog API Key**: Injected from \`POSTHOG_API_KEY\` secret" >> $GITHUB_STEP_SUMMARY
echo "- ✅ **OTLP**: Injected from \`OTEL_EXPORTER_OTLP_ENDPOINT\` + \`OTEL_EXPORTER_OTLP_TOKEN\` secrets" >> $GITHUB_STEP_SUMMARY
echo "- ✅ **Sentry DSN**: Injected from \`SENTRY_DSN\` secret" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "> 📍 **Secret Location**: Repository Settings → Secrets and variables → Actions" >> $GITHUB_STEP_SUMMARY
Expand Down
19 changes: 9 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ TimeTracker is built with modern, reliable technologies:
- **APScheduler** — Background task scheduling
- **Prometheus Client** — Metrics collection
- **Sentry SDK** — Error monitoring (optional)
- **PostHog** — Product analytics (optional)
- **Grafana OTLP** — Telemetry sink (optional)

### Development & Testing
- **pytest** — Testing framework
Expand Down Expand Up @@ -798,22 +798,21 @@ TimeTracker includes **optional** analytics and monitoring features to help impr
- Helps identify and fix bugs quickly
- **Opt-in:** Set `SENTRY_DSN` environment variable

#### 4. **Product Analytics** (Optional - PostHog)
#### 4. **Product Analytics** (Optional - Grafana OTLP)
- Tracks feature usage and user behavior patterns with advanced features:
- **Person Properties**: Role, auth method, login history
- **Feature Flags**: Gradual rollouts, A/B testing, kill switches
- **Group Analytics**: Segment by version, platform, deployment
- **Rich Context**: Browser, device, environment on every event
- **Opt-in:** Set `POSTHOG_API_KEY` environment variable
- See [POSTHOG_ADVANCED_FEATURES.md](docs/admin/monitoring/POSTHOG_ADVANCED_FEATURES.md) for complete guide
- **Sink config:** Set `GRAFANA_OTLP_ENDPOINT` and `GRAFANA_OTLP_TOKEN`

#### 5. **Installation Telemetry** (Optional, Anonymous)
- Sends anonymous installation data via PostHog with:
- Sends anonymous installation data via Grafana OTLP with:
- Anonymized fingerprint (SHA-256 hash, cannot be reversed)
- Application version
- Platform information
- **No PII:** No IP addresses, usernames, or business data
- **Opt-in:** Set `ENABLE_TELEMETRY=true` and `POSTHOG_API_KEY` environment variables
- **Opt-in:** Set `ENABLE_TELEMETRY=true` for detailed analytics; base telemetry remains anonymous

### How to Enable Analytics

Expand All @@ -822,11 +821,11 @@ TimeTracker includes **optional** analytics and monitoring features to help impr
SENTRY_DSN=https://your-sentry-dsn@sentry.io/project-id
SENTRY_TRACES_RATE=0.1 # 10% sampling for performance traces

# Enable PostHog product analytics (optional)
POSTHOG_API_KEY=your-posthog-api-key
POSTHOG_HOST=https://app.posthog.com
# Configure Grafana Cloud OTLP sink (optional)
GRAFANA_OTLP_ENDPOINT=https://otlp-gateway-prod-eu-west-2.grafana.net/otlp/v1/logs
GRAFANA_OTLP_TOKEN=your-grafana-otlp-token

# Enable anonymous telemetry (optional, uses PostHog)
# Enable detailed analytics (optional)
ENABLE_TELEMETRY=true
TELE_SALT=your-unique-salt
APP_VERSION=1.0.0
Expand Down
13 changes: 0 additions & 13 deletions app/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
from datetime import timedelta
from urllib.parse import urlparse

import posthog
import sentry_sdk
from authlib.integrations.flask_client import OAuth
from dotenv import load_dotenv
Expand Down Expand Up @@ -658,18 +657,6 @@ def record_metrics_and_log(response):
except Exception as e:
app.logger.warning(f"Failed to initialize Sentry: {e}")

# Initialize PostHog for product analytics
# Priority: Env var > Built-in default > Disabled
posthog_api_key = analytics_config.get("posthog_api_key", "")
posthog_host = analytics_config.get("posthog_host", "https://app.posthog.com")
if posthog_api_key:
try:
posthog.project_api_key = posthog_api_key
posthog.host = posthog_host
app.logger.info(f"PostHog product analytics initialized (host: {posthog_host})")
except Exception as e:
app.logger.warning(f"Failed to initialize PostHog: {e}")

# Fail-fast on weak/missing secret in production
# Skip validation in testing or debug mode
is_testing = app.config.get("TESTING", False)
Expand Down
26 changes: 17 additions & 9 deletions app/config/analytics_defaults.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,11 @@
DO NOT commit actual keys to this file - they are injected at build time only.
"""

# PostHog Configuration
# Replaced by GitHub Actions: POSTHOG_API_KEY_PLACEHOLDER
POSTHOG_API_KEY_DEFAULT = "%%POSTHOG_API_KEY_PLACEHOLDER%%"
POSTHOG_HOST_DEFAULT = "https://us.i.posthog.com"
# OTEL OTLP Configuration
# Replaced by GitHub Actions: OTEL_EXPORTER_OTLP_ENDPOINT_PLACEHOLDER
# Replaced by GitHub Actions: OTEL_EXPORTER_OTLP_TOKEN_PLACEHOLDER
OTEL_EXPORTER_OTLP_ENDPOINT_DEFAULT = "%%OTEL_EXPORTER_OTLP_ENDPOINT_PLACEHOLDER%%"
OTEL_EXPORTER_OTLP_TOKEN_DEFAULT = "%%OTEL_EXPORTER_OTLP_TOKEN_PLACEHOLDER%%"

# Sentry Configuration
# Replaced by GitHub Actions: SENTRY_DSN_PLACEHOLDER
Expand Down Expand Up @@ -128,8 +129,13 @@ def get_analytics_config():
def is_placeholder(value):
return value.startswith("%%") and value.endswith("%%")

# PostHog configuration - use embedded keys (no override)
posthog_api_key = POSTHOG_API_KEY_DEFAULT if not is_placeholder(POSTHOG_API_KEY_DEFAULT) else ""
# OTEL OTLP configuration - use embedded values (no override)
otel_exporter_otlp_endpoint = (
OTEL_EXPORTER_OTLP_ENDPOINT_DEFAULT if not is_placeholder(OTEL_EXPORTER_OTLP_ENDPOINT_DEFAULT) else ""
)
otel_exporter_otlp_token = (
OTEL_EXPORTER_OTLP_TOKEN_DEFAULT if not is_placeholder(OTEL_EXPORTER_OTLP_TOKEN_DEFAULT) else ""
)

# Sentry configuration - use embedded keys (no override)
sentry_dsn = SENTRY_DSN_DEFAULT if not is_placeholder(SENTRY_DSN_DEFAULT) else ""
Expand All @@ -141,8 +147,8 @@ def is_placeholder(value):
# Users control telemetry via the opt-in/opt-out toggle in admin dashboard

return {
"posthog_api_key": posthog_api_key,
"posthog_host": POSTHOG_HOST_DEFAULT, # Fixed host, no override
"otel_exporter_otlp_endpoint": otel_exporter_otlp_endpoint,
"otel_exporter_otlp_token": otel_exporter_otlp_token,
"sentry_dsn": sentry_dsn,
"sentry_traces_rate": float(SENTRY_TRACES_RATE_DEFAULT), # Fixed rate, no override
"app_version": app_version,
Expand All @@ -162,4 +168,6 @@ def is_placeholder(value):
return value.startswith("%%") and value.endswith("%%")

# Check if keys have been replaced during build
return not is_placeholder(POSTHOG_API_KEY_DEFAULT)
return (not is_placeholder(OTEL_EXPORTER_OTLP_ENDPOINT_DEFAULT)) and (
not is_placeholder(OTEL_EXPORTER_OTLP_TOKEN_DEFAULT)
)
Loading
Loading