// ============================================================ // Product analytics + error capture (Phase 2) // // - OpenPanel (@openpanel/web): funnels/usage — initialized only when // VITE_OPENPANEL_URL and VITE_OPENPANEL_CLIENT_ID are set (build args), // so local dev and air-gapped environments stay silent. // - GlitchTip (@sentry/react): frontend exception capture, only when // VITE_SENTRY_DSN is set. // // Both are no-ops without configuration; every helper is safe to call // unconditionally from anywhere in the app. // ============================================================ import * as Sentry from "@sentry/react"; type Props = Record; declare global { interface Window { op?: (...args: unknown[]) => void; } } 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({ dsn: SENTRY_DSN, environment: (import.meta.env?.MODE as string | undefined) ?? "production", // Frontend traces are expensive and rarely needed — errors only. tracesSampleRate: 0, }); } 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, trackScreenViews: true, // pageview → which pages users visit }); } catch { /* analytics must never break the app */ } } } /** Attach the ERP user identity to analytics (after login / app load). */ export function identifyUser(profile: { id?: string; name?: string; role?: string | null; }): void { if (!profile.id) return; try { window.op?.("identify", { profileId: profile.id, firstName: profile.name || undefined, data: { role: profile.role ?? undefined }, }); track("user_identified", { role: profile.role ?? undefined }); } catch { /* ignore */ } } export function track(event: string, properties?: Props): void { try { window.op?.("track", event, properties ?? {}); } catch { /* ignore */ } } export function captureError(err: unknown): void { if (SENTRY_DSN) Sentry.captureException(err); } export const analyticsEnabled = Boolean(OPENPANEL_URL && OPENPANEL_CLIENT_ID);