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.
This commit is contained in:
ZCode
2026-09-21 09:15:02 +00:00
parent cff35e3660
commit 34290b9371
2 changed files with 10 additions and 7 deletions
+6 -7
View File
@@ -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,
+4
View File
@@ -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