Files
bambuddy/backend/app/services/telemetry.py
T
maziggy 83cbac04b7 - Add interactive API Browser to Settings > API Keys
- New APIBrowser component with full OpenAPI schema integration
    - Fetches and parses /openapi.json automatically
    - Groups endpoints by API tags (printers, archives, settings, etc.)
    - Expandable endpoint sections with color-coded method badges
    - Path parameter, query parameter, and JSON body editors
    - Auto-populates request body with schema examples
    - Live API request execution with response display
    - Response shows status code, timing, and formatted JSON
    - Copy response button with clipboard fallback
    - Search to filter endpoints across all categories
    - Expand All / Collapse All buttons
    - Link to Swagger UI (/docs)

  - Two-column layout for API Keys tab
    - Left: API key management + webhook documentation
    - Right: API Browser with dedicated test key input

  - Parameter validation
    - Shows warning for missing required parameters
    - Validates before sending requests to avoid 422 errors

  - UX improvements
    - "Use in API Browser" button on newly created keys
    - Responsive layout (stacked on mobile, side-by-side on xl+)
2026-01-02 15:00:21 +01:00

120 lines
3.7 KiB
Python

"""Anonymous telemetry service for BamBuddy."""
import asyncio
import logging
import uuid
from datetime import datetime, timedelta
import httpx
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from backend.app.core.config import APP_VERSION
from backend.app.models.settings import Settings
logger = logging.getLogger(__name__)
# Default telemetry server URL (can be overridden via settings)
DEFAULT_TELEMETRY_URL = "https://telemetry.bambuddy.cool"
# How often to send heartbeats (once per day)
HEARTBEAT_INTERVAL = timedelta(hours=24)
_last_heartbeat: datetime | None = None
async def get_or_create_installation_id(db: AsyncSession) -> str:
"""Get existing installation ID or create a new one."""
result = await db.execute(select(Settings).where(Settings.key == "installation_id"))
setting = result.scalar_one_or_none()
if setting:
return setting.value
# Generate new UUID
installation_id = str(uuid.uuid4())
# Save to database
new_setting = Settings(key="installation_id", value=installation_id)
db.add(new_setting)
await db.commit()
logger.info(f"Generated new installation ID: {installation_id[:8]}...")
return installation_id
async def is_telemetry_enabled(db: AsyncSession) -> bool:
"""Check if telemetry is enabled (opt-out model)."""
result = await db.execute(select(Settings).where(Settings.key == "telemetry_enabled"))
setting = result.scalar_one_or_none()
# Default to enabled (opt-out model)
if not setting:
return True
return setting.value.lower() == "true"
async def get_telemetry_url(db: AsyncSession) -> str:
"""Get telemetry server URL from settings."""
result = await db.execute(select(Settings).where(Settings.key == "telemetry_url"))
setting = result.scalar_one_or_none()
return setting.value if setting else DEFAULT_TELEMETRY_URL
async def send_heartbeat(db: AsyncSession) -> bool:
"""Send anonymous heartbeat to telemetry server."""
global _last_heartbeat
try:
# Check if telemetry is enabled
if not await is_telemetry_enabled(db):
logger.debug("Telemetry disabled, skipping heartbeat")
return False
# Rate limit: only send once per day
if _last_heartbeat and datetime.now() - _last_heartbeat < HEARTBEAT_INTERVAL:
logger.debug("Heartbeat already sent recently, skipping")
return True
installation_id = await get_or_create_installation_id(db)
telemetry_url = await get_telemetry_url(db)
async with httpx.AsyncClient(timeout=10.0) as client:
response = await client.post(
f"{telemetry_url}/heartbeat",
json={
"installation_id": installation_id,
"version": APP_VERSION,
},
)
response.raise_for_status()
_last_heartbeat = datetime.now()
logger.info(f"Telemetry heartbeat sent to {telemetry_url}")
return True
except httpx.HTTPError as e:
logger.debug(f"Telemetry heartbeat failed (network): {e}")
return False
except Exception as e:
logger.debug(f"Telemetry heartbeat failed: {e}")
return False
async def start_telemetry_loop(get_session):
"""Background task to send periodic heartbeats."""
# Wait a bit before first heartbeat to let app initialize
await asyncio.sleep(30)
while True:
try:
async with get_session() as db:
await send_heartbeat(db)
except Exception as e:
logger.debug(f"Telemetry loop error: {e}")
# Check daily
await asyncio.sleep(HEARTBEAT_INTERVAL.total_seconds())