- /health/ready (DB+MinIO+Casdoor checks, webhook post-deploy gate), /metrics via prometheus-fastapi-instrumentator, Sentry/GlitchTip capture, X-Request-ID middleware + JSON access logging (PROCUREMENT_LOG_FORMAT) - server-side analytics (OpenPanel Track API): login, quote lifecycle, receipts — fire-and-forget, disabled without env config - monitoring/ compose project: GlitchTip(+pg), Prometheus(+alerts), Grafana(provisioned ERP dashboard), Loki+Promtail, node/postgres exporters, cAdvisor — mem-limited (~2.2GB), loopback admin UIs, separate from app deploys; erp_shared network for scraping - frontend: @openpanel/web + @sentry/react (build-arg gated, no-op when unset), funnel events (login, wizard steps, quote submitted) - compose: api healthcheck, observability env wiring, .env.example keys
81 lines
2.4 KiB
TypeScript
81 lines
2.4 KiB
TypeScript
// ============================================================
|
|
// 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<string, unknown>;
|
|
|
|
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 SENTRY_DSN = (import.meta.env?.VITE_SENTRY_DSN as string | undefined)?.trim();
|
|
|
|
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 {
|
|
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);
|