/** * PostHog wiring — session replay, exception capture, console logs. * * NOTE: `backoffice/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 customer * 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 "@/types/auth"; const APP = "freight-portal"; 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); } /** Company context, as returned by `useAuth().company`. */ interface IdentifyCompany { company?: { id?: string; type?: string | null; status?: string | null } | null; } /** * 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 real customers, and debugging a replay never * requires knowing how to phone the person in it. */ export function useIdentify( user: AuthUser | null, company?: IdentifyCompany | null, ): void { useEffect(() => { if (!enabled) return; if (!user?.id) { posthog.reset(); return; } posthog.identify(user.id, { roles: user.roles, status: user.status, user_type: user.userType, company_id: company?.company?.id, company_type: company?.company?.type, company_status: company?.company?.status, }); }, [ user?.id, user?.roles, user?.status, user?.userType, company?.company?.id, company?.company?.type, company?.company?.status, ]); }