Files
TimeTracker/app/config/analytics_defaults.py
T
Dries Peeters ef9b66f5e7 fix(api): align client search, OpenAPI version, and Client construction
Global search referenced Client.company, which is not a column on Client, so client matches failed at runtime. Legacy and v1 search, plus search_clients(), now filter on name, email, description, and contact_person; result descriptions use the same fields. Legacy /api/search returns count: 0 for queries shorter than two characters so responses stay consistent.

OpenAPI info.version is taken from get_version_from_setup(), with a config fallback when the resolved version is unknown. get_version_from_setup() also honors TIMETRACKER_VERSION and APP_VERSION for CI and container builds.

Client.__init__ accepts custom_fields. ClientService no longer passes status= into Client(), which the initializer does not support.

Tests add HTTP route contract checks and OpenAPI version alignment, fix subcontractor search fixtures (Client/Task construction and v1 client fixture naming), and update related API integration tests.
2026-04-15 12:57:01 +02:00

180 lines
6.2 KiB
Python

"""
Analytics configuration for TimeTracker.
These values are embedded at build time and cannot be overridden by users.
This allows collecting anonymized usage metrics from all installations
to improve the product while respecting user privacy.
Key Privacy Protections:
- Telemetry is OPT-IN (disabled by default)
- No personally identifiable information is ever collected
- Users can disable telemetry at any time via admin dashboard
- All tracked events are documented and transparent
DO NOT commit actual keys to this file - they are injected at build time only.
"""
# 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
SENTRY_DSN_DEFAULT = "%%SENTRY_DSN_PLACEHOLDER%%"
SENTRY_TRACES_RATE_DEFAULT = "0.1"
# Telemetry Configuration
# All builds have analytics configured, but telemetry is OPT-IN
TELE_ENABLED_DEFAULT = "false" # Disabled by default for privacy
def get_version_from_setup():
"""
Get the application version from setup.py.
setup.py is the SINGLE SOURCE OF TRUTH for version information.
This function reads setup.py at runtime to get the current version.
All other code should reference this function, not define versions themselves.
Override at runtime with TIMETRACKER_VERSION or APP_VERSION (e.g. CI/containers).
This function tries multiple paths to find setup.py to work correctly
in both production and development modes.
Returns:
str: Application version (e.g., "4.5.0") or "unknown" if setup.py can't be read
"""
import os
import re
env_version = (os.environ.get("TIMETRACKER_VERSION") or os.environ.get("APP_VERSION") or "").strip()
if env_version:
return env_version
# Try multiple possible paths to setup.py
possible_paths = []
# Path 1: Relative to this file (app/config/analytics_defaults.py -> setup.py)
try:
base_path = os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
possible_paths.append(os.path.join(base_path, "setup.py"))
except Exception:
pass
# Path 2: Current working directory
try:
possible_paths.append(os.path.join(os.getcwd(), "setup.py"))
except Exception:
pass
# Path 3: From environment variable (if set)
try:
project_root = os.getenv("PROJECT_ROOT") or os.getenv("APP_ROOT")
if project_root:
possible_paths.append(os.path.join(project_root, "setup.py"))
except Exception:
pass
# Path 4: Try to find setup.py by walking up from current file
try:
current = os.path.dirname(__file__)
for _ in range(5): # Max 5 levels up
current = os.path.dirname(current)
setup_path = os.path.join(current, "setup.py")
if os.path.exists(setup_path):
possible_paths.append(setup_path)
break
except Exception:
pass
# Try each path until we find setup.py
for setup_path in possible_paths:
try:
if os.path.exists(setup_path):
# Read setup.py
with open(setup_path, "r", encoding="utf-8") as f:
content = f.read()
# Extract version using regex
# Matches: version='X.Y.Z' or version="X.Y.Z"
version_match = re.search(r'version\s*=\s*[\'"]([^\'"]+)[\'"]', content)
if version_match:
return version_match.group(1)
except Exception:
continue
# Fallback version if setup.py can't be read
# This is the ONLY place besides setup.py where version is defined
return "unknown"
# Keep the old function name for backward compatibility
_get_version_from_setup = get_version_from_setup
def get_analytics_config():
"""
Get analytics configuration.
Analytics keys are embedded at build time and cannot be overridden
to ensure consistent telemetry collection across all installations.
However, users maintain full control:
- Telemetry is OPT-IN (disabled by default)
- Can be disabled anytime in admin dashboard
- No PII is ever collected
Returns:
dict: Analytics configuration
"""
# Helper to check if a value is a placeholder (not replaced by GitHub Actions)
def is_placeholder(value):
return value.startswith("%%") and value.endswith("%%")
# 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 ""
# App version - read from setup.py at runtime
app_version = get_version_from_setup()
# Note: Environment variables are NOT checked for keys to prevent override
# Users control telemetry via the opt-in/opt-out toggle in admin dashboard
return {
"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,
"telemetry_enabled_default": False, # Always opt-in
}
def has_analytics_configured():
"""
Check if analytics keys are configured (embedded at build time).
Returns:
bool: True if analytics keys are embedded
"""
def is_placeholder(value):
return value.startswith("%%") and value.endswith("%%")
# Check if keys have been replaced during build
return (not is_placeholder(OTEL_EXPORTER_OTLP_ENDPOINT_DEFAULT)) and (
not is_placeholder(OTEL_EXPORTER_OTLP_TOKEN_DEFAULT)
)