From 34290b937146ffcb748cb5b4ba918487480f74a2 Mon Sep 17 00:00:00 2001 From: ZCode Date: Mon, 21 Sep 2026 09:15:02 +0000 Subject: [PATCH] fix(analytics): declare the script build arg and send the Track API's real envelope Two blockers found while wiring OpenPanel for real: - frontend/Dockerfile declared ARG for VITE_OPENPANEL_URL/CLIENT_ID/SENTRY_DSN but not VITE_OPENPANEL_SCRIPT_URL, so passing it as a build arg was silently ignored and the loader compiled out (import.meta.env value undefined). - backend/app/services/analytics.py posted a flat {type, event, properties} body; the Track API expects {type, payload:{event, properties, profileId}} (verified against the SDK source in packages/sdks/sdk). The API answered 400 and the module swallows every error by design, so all server-side events were being dropped without a trace. Verified after deploy: the bundle carries the client id + op1.js URL + the queueing shim, op1.js serves 200, and a track event posted with the real envelope lands in OpenPanel. --- backend/app/services/analytics.py | 13 ++++++------- frontend/Dockerfile | 4 ++++ 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/backend/app/services/analytics.py b/backend/app/services/analytics.py index be0b494..3d34804 100644 --- a/backend/app/services/analytics.py +++ b/backend/app/services/analytics.py @@ -37,13 +37,12 @@ async def track(event: str, properties: dict[str, Any] | None = None, profile_id """Record a named event; swallows every error.""" if not _configured(): return - body = { - "type": "track", - "event": event, - "properties": properties or {}, - } + # 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. + inner: dict[str, Any] = {"event": event, "properties": properties or {}} if profile_id: - body["profileId"] = profile_id + inner["profileId"] = profile_id + body = {"type": "track", "payload": inner} try: async with httpx.AsyncClient(timeout=_TIMEOUT) as client: await client.post( @@ -66,7 +65,7 @@ async def identify(profile_id: str, properties: dict[str, Any] | None = None) -> async with httpx.AsyncClient(timeout=_TIMEOUT) as client: await client.post( f"{settings.OPENPANEL_API_URL.rstrip('/')}/track", - json={"type": "identify", "profileId": profile_id, "properties": properties or {}}, + 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, diff --git a/frontend/Dockerfile b/frontend/Dockerfile index 9530fbb..8b08a3e 100644 --- a/frontend/Dockerfile +++ b/frontend/Dockerfile @@ -23,9 +23,13 @@ ENV VITE_ENABLE_DEV_LOGIN=${DEV_LOGIN_ENABLED} # Empty values disable the feature (analytics.ts is a no-op then). ARG VITE_OPENPANEL_URL="" ARG VITE_OPENPANEL_CLIENT_ID="" +# The browser script (op1.js) lives on the dashboard origin, not under the API path, +# so it needs its own arg — defined here or the value never reaches the bundle. +ARG VITE_OPENPANEL_SCRIPT_URL="" ARG VITE_SENTRY_DSN="" ENV VITE_OPENPANEL_URL=${VITE_OPENPANEL_URL} \ VITE_OPENPANEL_CLIENT_ID=${VITE_OPENPANEL_CLIENT_ID} \ + VITE_OPENPANEL_SCRIPT_URL=${VITE_OPENPANEL_SCRIPT_URL} \ VITE_SENTRY_DSN=${VITE_SENTRY_DSN} # Source code + build