Second half of the envelope fix: ITrackPayload is {name, properties, profileId} — sending
'event' is rejected with 400 "payload/name Invalid input: expected string", which the
module swallows by design. Verified against packages/validation/src/track.validation.ts
and the SDK's track(name, properties) signature.
78 lines
3.1 KiB
Python
78 lines
3.1 KiB
Python
"""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)
|