"""Server-side product analytics — OpenPanel Track API. Fire-and-forget business events (quote lifecycle, receipts, logins) so the OpenPanel dashboards show what actually happens inside the ERP, not just page views. Never raises: analytics must not break a business flow. Configuration (all optional — disabled when any is missing): PROCUREMENT_OPENPANEL_API_URL e.g. http://openpanel-api:3333 (self-hosted) PROCUREMENT_OPENPANEL_CLIENT_ID project client id (write mode) PROCUREMENT_OPENPANEL_CLIENT_SECRET project client secret Contract (OpenPanel Track API): POST {API_URL}/track with headers ``openpanel-client-id`` / ``openpanel-client-secret`` and a JSON body ``{"type": "track"|"identify", "profileId": ..., "event"/"properties": ...}``. """ import logging from typing import Any import httpx from app.config import settings logger = logging.getLogger("procurement.analytics") _TIMEOUT = httpx.Timeout(3.0) def _configured() -> bool: return bool( settings.OPENPANEL_API_URL and settings.OPENPANEL_CLIENT_ID and settings.OPENPANEL_CLIENT_SECRET ) async def track(event: str, properties: dict[str, Any] | None = None, profile_id: str | None = None) -> None: """Record a named event; swallows every error.""" if not _configured(): return # OpenPanel's Track API takes {type, payload:{…}} (same envelope the browser SDK # sends) — a flat body is rejected with 400, which this module swallows by design. # The payload field is "name" (ITrackPayload in packages/validation) — "event" is # rejected with a 400 that this module swallows. inner: dict[str, Any] = {"name": event, "properties": properties or {}} if profile_id: inner["profileId"] = profile_id body = {"type": "track", "payload": inner} try: async with httpx.AsyncClient(timeout=_TIMEOUT) as client: await client.post( f"{settings.OPENPANEL_API_URL.rstrip('/')}/track", json=body, headers={ "openpanel-client-id": settings.OPENPANEL_CLIENT_ID, "openpanel-client-secret": settings.OPENPANEL_CLIENT_SECRET, }, ) except Exception as exc: # noqa: BLE001 — analytics must never raise logger.debug("analytics track failed (%s): %s", event, exc) async def identify(profile_id: str, properties: dict[str, Any] | None = None) -> None: """Create/update a user profile (e.g. on login).""" if not _configured(): return try: async with httpx.AsyncClient(timeout=_TIMEOUT) as client: await client.post( f"{settings.OPENPANEL_API_URL.rstrip('/')}/track", json={"type": "identify", "payload": {"profileId": profile_id, "properties": properties or {}}}, headers={ "openpanel-client-id": settings.OPENPANEL_CLIENT_ID, "openpanel-client-secret": settings.OPENPANEL_CLIENT_SECRET, }, ) except Exception as exc: # noqa: BLE001 logger.debug("analytics identify failed: %s", exc)