/** * PostHog wiring — session replay, exception capture, console logs. * * NOTE: `portal/src/lib/posthog.ts` is the twin of this file. The init config * below (masking rules) and the PII allowlist in `useIdentify` MUST be kept * identical in both — a change made here alone silently leaks staff data into * the other app's replays. * * This is instrumentation, not analytics: autocapture is off and no product * events are sent. */ import posthog from "posthog-js"; import { useEffect } from "react"; import type { AuthUser } from "@/auth/types"; const APP = "freight-backoffice"; const TOKEN = import.meta.env.VITE_POSTHOG_KEY; const HOST = import.meta.env.VITE_POSTHOG_HOST; /** * Whether init actually ran. Without a token every export below is a no-op, so * local dev and any environment whose env file lacks the vars keeps working — * a missing observability token must never break the app. */ let enabled = false; export function initPostHog(): void { if (enabled || !TOKEN || !HOST) return; posthog.init(TOKEN, { api_host: HOST, defaults: "2026-05-30", // Debuggability, not analytics. autocapture: false, capture_pageview: true, capture_pageleave: true, disable_surveys: true, person_profiles: "identified_only", capture_exceptions: { capture_unhandled_errors: true, capture_unhandled_rejections: true, capture_console_errors: true, }, // Off by default; this is half the point of the integration. enable_recording_console_log: true, // Inputs are masked; rendered text stays visible so replays are readable. // Wrap sensitive elements in `ph-no-capture` to blank them individually. session_recording: { maskAllInputs: true }, }); // Portal and backoffice share one PostHog project — filter by this. posthog.register({ app: APP }); enabled = true; } export function captureException( error: unknown, properties?: Record, ): void { if (!enabled) return; posthog.captureException(error, properties); } /** * Report a failed API call. * * Called from the axios interceptor rather than from `emitApiError`, which * early-returns on suppressed paths (warehouse / first-mile / onboarding / * auth) — hooking there would drop errors on exactly those pages. * * 5xx and network failures are real defects and go to Error tracking. 4xx is * usually the server correctly rejecting input, so it is recorded as a plain * event to keep the issue list signal-heavy. */ export function captureApiError(error: unknown): void { if (!enabled) return; const err = error as { message?: string; config?: { method?: string; url?: string }; response?: { status?: number }; }; const status = err.response?.status; const properties = { api_status: status ?? null, api_method: err.config?.method?.toUpperCase() ?? null, api_path: err.config?.url ?? null, }; if (status && status < 500) { posthog.capture("api_error", properties); return; } posthog.captureException(error, properties); } /** * Identify the current user to PostHog so replays and exceptions are * attributable. * * Deliberately sends NO contact details. `AuthUser` carries email, phoneNumber, * name and username; these are railway staff, and debugging a replay never * requires knowing how to phone the person in it. */ export function useIdentify(user: AuthUser | null): void { const employee = user?.employee?.[0]; useEffect(() => { if (!enabled) return; if (!user?.id) { posthog.reset(); return; } posthog.identify(user.id, { roles: user.roles?.map((role) => role.key ?? role.id), status: user.status, is_super_admin: user.isSuperAdmin, organization_id: employee?.organizationId, unit_id: employee?.unitId, }); }, [ user?.id, user?.roles, user?.status, user?.isSuperAdmin, employee?.organizationId, employee?.unitId, ]); }