diff --git a/frontend/src/lib/analytics.ts b/frontend/src/lib/analytics.ts index 2362fc1..8269b65 100644 --- a/frontend/src/lib/analytics.ts +++ b/frontend/src/lib/analytics.ts @@ -22,8 +22,31 @@ declare global { const OPENPANEL_URL = (import.meta.env?.VITE_OPENPANEL_URL as string | undefined)?.trim(); const OPENPANEL_CLIENT_ID = (import.meta.env?.VITE_OPENPANEL_CLIENT_ID as string | undefined)?.trim(); +const OPENPANEL_SCRIPT_URL = (import.meta.env?.VITE_OPENPANEL_SCRIPT_URL as string | undefined)?.trim(); const SENTRY_DSN = (import.meta.env?.VITE_SENTRY_DSN as string | undefined)?.trim(); +/** + * Load the OpenPanel browser script (op1.js) and its queueing shim. + * + * The shim is OpenPanel's documented snippet: `window.op` becomes a queue while the + * script is in flight, so `op("init", …)` can be called immediately and the real + * script drains the queue on load. Without it, every `window.op?.()` call here is a + * silent no-op — which is what happened before this loader existed. + */ +function loadOpenPanelScript(src: string): void { + if (document.querySelector("script[data-openpanel]")) return; + const shim = document.createElement("script"); + shim.textContent = + 'window.op=window.op||function(){var n=[];return new Proxy(function(){arguments.length&&n.push([].slice.call(arguments))},{get:function(t,r){return"q"===r?n:function(){n.push([r].concat([].slice.call(arguments)))}},has:function(t,r){return"q"===r}}) }();'; + document.head.appendChild(shim); + const script = document.createElement("script"); + script.src = src; + script.async = true; + script.defer = true; + script.dataset.openpanel = "1"; + document.head.appendChild(script); +} + export function initAnalytics(): void { if (SENTRY_DSN) { Sentry.init({ @@ -35,6 +58,7 @@ export function initAnalytics(): void { } if (OPENPANEL_URL && OPENPANEL_CLIENT_ID) { try { + if (OPENPANEL_SCRIPT_URL) loadOpenPanelScript(OPENPANEL_SCRIPT_URL); window.op?.("init", { apiUrl: OPENPANEL_URL, clientId: OPENPANEL_CLIENT_ID,