feat(monitoring): observability stack + app instrumentation
- /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
This commit is contained in:
@@ -0,0 +1,80 @@
|
||||
// ============================================================
|
||||
// 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);
|
||||
Reference in New Issue
Block a user