diff --git a/apps/edr-freight-web/backoffice/.env.example b/apps/edr-freight-web/backoffice/.env.example index a5e34a35d..c840d18a1 100644 --- a/apps/edr-freight-web/backoffice/.env.example +++ b/apps/edr-freight-web/backoffice/.env.example @@ -4,3 +4,8 @@ VITE_BASE_API_URL=http://localhost:3001 # Proactive token refresh cadence (minutes). Must stay well under the 60-min # server session window. Default: 10. VITE_TOKEN_REFRESH_INTERVAL_MINUTES=10 + +# PostHog — session replay, error tracking, console logs. Both must be set or +# observability stays off (the app works either way). Self-hosted instance. +VITE_POSTHOG_KEY=phc_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +VITE_POSTHOG_HOST=https://posthog.example.com diff --git a/apps/edr-freight-web/backoffice/package.json b/apps/edr-freight-web/backoffice/package.json index 2f2e0d7be..9f7fd27bf 100644 --- a/apps/edr-freight-web/backoffice/package.json +++ b/apps/edr-freight-web/backoffice/package.json @@ -20,6 +20,7 @@ "@mantine/core": "^9.3.0", "@mantine/dates": "^9.3.0", "@mantine/hooks": "^9.3.0", + "@posthog/react": "^1.10.3", "@radix-ui/react-accordion": "^1.2.13", "@radix-ui/react-alert-dialog": "^1.1.16", "@radix-ui/react-avatar": "^1.1.12", @@ -76,6 +77,7 @@ "lucide-react": "^1.14.0", "next-themes": "^0.4.6", "pdf-lib": "^1.17.1", + "posthog-js": "^1.400.1", "prop-types": "^15.8.1", "qs": "^6.15.2", "radix-ui": "^1.4.3", diff --git a/apps/edr-freight-web/backoffice/src/auth/AuthProvider.tsx b/apps/edr-freight-web/backoffice/src/auth/AuthProvider.tsx index 8266d96d7..ff16abd94 100644 --- a/apps/edr-freight-web/backoffice/src/auth/AuthProvider.tsx +++ b/apps/edr-freight-web/backoffice/src/auth/AuthProvider.tsx @@ -7,6 +7,7 @@ import { type ReactNode, } from "react"; +import { useIdentify } from "@/lib/posthog"; import { getMeRequest, loginRequest, verifyMfaRequest } from "./api"; import { AUTH_TOKEN_COOKIE, @@ -69,6 +70,9 @@ export const AuthProvider = ({ children }: { children: ReactNode }) => { const [loading, setLoading] = useState(true); const mfaEmailRef = useRef(null); + // Attribute replays and exceptions to the signed-in user (id/org only). + useIdentify(user); + const loadCurrentUser = async () => { const currentUser = await getMeRequest(); setUser(currentUser); diff --git a/apps/edr-freight-web/backoffice/src/auth/http.ts b/apps/edr-freight-web/backoffice/src/auth/http.ts index 39bb6cab7..e43351015 100644 --- a/apps/edr-freight-web/backoffice/src/auth/http.ts +++ b/apps/edr-freight-web/backoffice/src/auth/http.ts @@ -5,6 +5,7 @@ import { emitApiError, extractApiErrorPayload, } from "@/components/errors/ApiErrorModal"; +import { captureApiError } from "@/lib/posthog"; import { AUTH_TOKEN_COOKIE, REFRESH_TOKEN_COOKIE, @@ -82,6 +83,14 @@ api.interceptors.response.use( async (error) => { const originalRequest = error.config as RetriableRequest | undefined; + // Report the failure to PostHog. Hooked here rather than inside + // `emitApiError`, which stays silent on suppressed paths (warehouse / + // mile / onboarding) — those failures still need reporting. + // 401s are skipped: an expired session is refreshed below, not a defect. + if (!error.response || error.response.status !== 401) { + captureApiError(error); + } + if ( error.response?.status !== 401 || !originalRequest || diff --git a/apps/edr-freight-web/backoffice/src/components/ErrorBoundary.tsx b/apps/edr-freight-web/backoffice/src/components/ErrorBoundary.tsx index 00635118d..1c502e758 100644 --- a/apps/edr-freight-web/backoffice/src/components/ErrorBoundary.tsx +++ b/apps/edr-freight-web/backoffice/src/components/ErrorBoundary.tsx @@ -1,5 +1,7 @@ import { Component, type ErrorInfo, type ReactNode } from "react"; +import { captureException } from "@/lib/posthog"; + interface ErrorBoundaryProps { children: ReactNode; } @@ -21,6 +23,7 @@ export class ErrorBoundary extends Component, +): 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, + ]); +} diff --git a/apps/edr-freight-web/backoffice/src/main.tsx b/apps/edr-freight-web/backoffice/src/main.tsx index 63fbcb274..eacd05459 100644 --- a/apps/edr-freight-web/backoffice/src/main.tsx +++ b/apps/edr-freight-web/backoffice/src/main.tsx @@ -2,6 +2,8 @@ import { StrictMode } from "react"; import { createRoot } from "react-dom/client"; import { BrowserRouter } from "react-router-dom"; import { MantineProvider } from "@mantine/core"; +import posthog from "posthog-js"; +import { PostHogProvider } from "@posthog/react"; import "@mantine/core/styles.css"; import "@mantine/dates/styles.css"; import "@edr/ui-common/styles.css"; @@ -19,6 +21,7 @@ import { ApiErrorModal } from "./components/errors/ApiErrorModal"; import { ErrorBoundary } from "./components/ErrorBoundary"; import { AuthProvider } from "./auth/AuthProvider"; import { queryClient } from "./lib/queryClient"; +import { initPostHog } from "./lib/posthog"; import { freightMantineTheme } from "./theme/freight-brand"; import { QueryClientProvider } from "@tanstack/react-query"; @@ -43,6 +46,10 @@ const applyStoredTheme = () => { applyStoredTheme(); +// Must run before render so replay and exception capture cover startup errors. +// No-ops when VITE_POSTHOG_KEY is unset. +initPostHog(); + const rootElement = document.getElementById("root"); if (!rootElement) { @@ -50,23 +57,25 @@ if (!rootElement) { } createRoot(rootElement).render( - - - - - - - - - {/* Global API error modal — shows the server's actual error - message (suppressed on warehouse / mile / onboarding pages). */} - - - - - - - + + + + + + + + + + {/* Global API error modal — shows the server's actual error + message (suppressed on warehouse / mile / onboarding pages). */} + + + + + + + + ); // run \ No newline at end of file diff --git a/apps/edr-freight-web/backoffice/src/record-management/components/common/ErrorBoundary.tsx b/apps/edr-freight-web/backoffice/src/record-management/components/common/ErrorBoundary.tsx index 9a0c8cc2a..4a188ed6e 100644 --- a/apps/edr-freight-web/backoffice/src/record-management/components/common/ErrorBoundary.tsx +++ b/apps/edr-freight-web/backoffice/src/record-management/components/common/ErrorBoundary.tsx @@ -1,5 +1,7 @@ import React, { Component, ErrorInfo, ReactNode } from "react"; +import { captureException } from "@/lib/posthog"; + interface ErrorBoundaryProps { children: ReactNode; } @@ -20,7 +22,8 @@ export class ErrorBoundary extends Component< } componentDidCatch(error: Error, info: ErrorInfo) { - // Log to console in development; replace with a reporting service (e.g. Sentry) in production + captureException(error, { componentStack: info.componentStack }); + if (import.meta.env.DEV) { console.error("ErrorBoundary caught:", error, info.componentStack); } diff --git a/apps/edr-freight-web/backoffice/src/vite-env.d.ts b/apps/edr-freight-web/backoffice/src/vite-env.d.ts index f24da9f58..53d196cbd 100644 --- a/apps/edr-freight-web/backoffice/src/vite-env.d.ts +++ b/apps/edr-freight-web/backoffice/src/vite-env.d.ts @@ -17,6 +17,11 @@ interface Window { interface ImportMetaEnv { readonly VITE_API_URL: string; readonly VITE_BASE_API_URL: string; + readonly VITE_TOKEN_REFRESH_INTERVAL_MINUTES?: string; + /** PostHog project token. Absent = observability disabled (see lib/posthog.ts). */ + readonly VITE_POSTHOG_KEY?: string; + /** Self-hosted PostHog instance URL. */ + readonly VITE_POSTHOG_HOST?: string; } interface ImportMeta { diff --git a/apps/edr-freight-web/portal/package.json b/apps/edr-freight-web/portal/package.json index 2cfd488d6..1e7b7155d 100644 --- a/apps/edr-freight-web/portal/package.json +++ b/apps/edr-freight-web/portal/package.json @@ -18,6 +18,7 @@ "@mantine/core": "^9.3.0", "@mantine/dates": "^9.3.0", "@mantine/hooks": "^9.3.0", + "@posthog/react": "^1.10.3", "@tanstack/react-query": "^5.59.0", "@tria-plc/iamui": "file:../../../local-packages/tria-plc-iamui-0.1.1.tgz", "@vis.gl/react-google-maps": "^1.8.3", @@ -26,6 +27,7 @@ "clsx": "^2.1.1", "date-fns": "^3.6.0", "lucide-react": "^1.14.0", + "posthog-js": "^1.400.1", "radix-ui": "^1.4.3", "react": "19.2.6", "react-dom": "19.2.6", diff --git a/apps/edr-freight-web/portal/src/App.tsx b/apps/edr-freight-web/portal/src/App.tsx index 659831616..7c5b5e5b5 100644 --- a/apps/edr-freight-web/portal/src/App.tsx +++ b/apps/edr-freight-web/portal/src/App.tsx @@ -25,6 +25,7 @@ import OnboardingResumeBanner, { import OnboardingWizardDialog from "./components/onboarding/OnboardingWizardDialog"; import { ApiErrorModal } from "./components/errors/ApiErrorModal"; import useAuth from "./hooks/useAuth"; +import { useIdentify } from "./lib/posthog"; import { startTokenRefreshScheduler, stopTokenRefreshScheduler, @@ -221,6 +222,9 @@ const App = () => { const { user, company, companyType, createProfile, isAuthenticated } = useAuth(); + // Attribute replays and exceptions to the signed-in user (id/org only). + useIdentify(user, company); + // Keep the server session alive while a user is logged in. Runs after // login, signup, and page-reload bootstrap alike. useEffect(() => { diff --git a/apps/edr-freight-web/portal/src/components/errors/AppErrorFallback.tsx b/apps/edr-freight-web/portal/src/components/errors/AppErrorFallback.tsx new file mode 100644 index 000000000..7c6046113 --- /dev/null +++ b/apps/edr-freight-web/portal/src/components/errors/AppErrorFallback.tsx @@ -0,0 +1,31 @@ +/** + * Shown when a render error escapes to the app root. + * + * Before this existed, a render exception unmounted the tree and left the + * customer staring at a blank white page with no way forward. The matching + * `$exception` is reported by the surrounding PostHogErrorBoundary. + */ +export function AppErrorFallback() { + return ( +
+
+

+ Something went wrong +

+

+ This page failed to load. The problem has been reported. Try reloading + — if it keeps happening, please contact support. +

+ +
+
+ ); +} + +export default AppErrorFallback; diff --git a/apps/edr-freight-web/portal/src/lib/posthog.ts b/apps/edr-freight-web/portal/src/lib/posthog.ts new file mode 100644 index 000000000..59599fbb6 --- /dev/null +++ b/apps/edr-freight-web/portal/src/lib/posthog.ts @@ -0,0 +1,151 @@ +/** + * 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; + profile?: { activeProfileType?: 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, + active_profile_type: company?.profile?.activeProfileType, + }); + }, [ + user?.id, + user?.roles, + user?.status, + user?.userType, + company?.company?.id, + company?.company?.type, + company?.company?.status, + company?.profile?.activeProfileType, + ]); +} diff --git a/apps/edr-freight-web/portal/src/main.tsx b/apps/edr-freight-web/portal/src/main.tsx index 44f2d3fb9..983aa5b7a 100644 --- a/apps/edr-freight-web/portal/src/main.tsx +++ b/apps/edr-freight-web/portal/src/main.tsx @@ -3,6 +3,8 @@ import { createRoot } from "react-dom/client"; import { BrowserRouter } from "react-router-dom"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { MantineProvider } from "@mantine/core"; +import posthog from "posthog-js"; +import { PostHogProvider, PostHogErrorBoundary } from "@posthog/react"; import "@mantine/core/styles.css"; import "@mantine/dates/styles.css"; import "@edr/ui-common/styles.css"; @@ -10,6 +12,8 @@ import "../index.css"; import "@edr/ui-common/theme.css"; import { Toaster } from "react-hot-toast"; import { mantineTheme } from "./theme/mantine"; +import { initPostHog } from "./lib/posthog"; +import { AppErrorFallback } from "./components/errors/AppErrorFallback"; import App from "./App"; @@ -26,6 +30,10 @@ import App from "./App"; } }); +// Must run before render so replay and exception capture cover startup errors. +// No-ops when VITE_POSTHOG_KEY is unset. +initPostHog(); + const queryClient = new QueryClient(); const rootElement = document.getElementById("root"); @@ -36,13 +44,17 @@ if (!rootElement) { createRoot(document.getElementById("root")!).render( - - - - - - - - + + + + + }> + + + + + + + , ); diff --git a/apps/edr-freight-web/portal/src/utils/api.ts b/apps/edr-freight-web/portal/src/utils/api.ts index 0e88c88b3..69dfe308f 100644 --- a/apps/edr-freight-web/portal/src/utils/api.ts +++ b/apps/edr-freight-web/portal/src/utils/api.ts @@ -6,6 +6,7 @@ import { emitApiError, extractApiErrorPayload, } from "@/components/errors/ApiErrorModal"; +import { captureApiError } from "@/lib/posthog"; const client = axios.create({ baseURL: API_BASE_URL, @@ -91,6 +92,14 @@ client.interceptors.response.use( _retry?: boolean; }; + // Report the failure to PostHog. Hooked here rather than inside + // `emitApiError`, which stays silent on suppressed paths (warehouse / + // mile / onboarding) — those failures still need reporting. + // 401s are skipped: an expired session is refreshed below, not a defect. + if (!error.response || error.response.status !== 401) { + captureApiError(error); + } + // Don't intercept if: // - no response (network error) // - status is not 401 diff --git a/apps/edr-freight-web/portal/src/vite-env.d.ts b/apps/edr-freight-web/portal/src/vite-env.d.ts index b24245689..a4955ecee 100644 --- a/apps/edr-freight-web/portal/src/vite-env.d.ts +++ b/apps/edr-freight-web/portal/src/vite-env.d.ts @@ -16,7 +16,13 @@ interface Window { interface ImportMetaEnv { readonly VITE_API_URL: string; + readonly VITE_BASE_API_URL: string; readonly VITE_GOOGLE_MAPS_API_KEY?: string; + readonly VITE_TOKEN_REFRESH_INTERVAL_MINUTES?: string; + /** PostHog project token. Absent = observability disabled (see lib/posthog.ts). */ + readonly VITE_POSTHOG_KEY?: string; + /** Self-hosted PostHog instance URL. */ + readonly VITE_POSTHOG_HOST?: string; } interface ImportMeta { diff --git a/docker-compose.yaml b/docker-compose.yaml index 7637c500a..a621749dc 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -56,6 +56,8 @@ services: VITE_BASE_API_URL: ${VITE_BASE_API_URL:-} VITE_USER_MANAGEMENT_BASE: ${VITE_USER_MANAGEMENT_BASE:-} VITE_GOOGLE_MAPS_API_KEY: ${VITE_GOOGLE_MAPS_API_KEY:-} + VITE_POSTHOG_KEY: ${VITE_POSTHOG_KEY:-} + VITE_POSTHOG_HOST: ${VITE_POSTHOG_HOST:-} secrets: - npmrc ports: @@ -72,6 +74,8 @@ services: VITE_BASE_API_URL: ${VITE_BASE_API_URL:-} VITE_USER_MANAGEMENT_BASE: ${VITE_USER_MANAGEMENT_BASE:-} VITE_GOOGLE_MAPS_API_KEY: ${VITE_GOOGLE_MAPS_API_KEY:-} + VITE_POSTHOG_KEY: ${VITE_POSTHOG_KEY:-} + VITE_POSTHOG_HOST: ${VITE_POSTHOG_HOST:-} secrets: - npmrc ports: diff --git a/infrastructure/docker/Dockerfile.web b/infrastructure/docker/Dockerfile.web index 6ef9231f0..1e26bf643 100644 --- a/infrastructure/docker/Dockerfile.web +++ b/infrastructure/docker/Dockerfile.web @@ -28,11 +28,15 @@ ARG VITE_BASE_API_URL ARG VITE_USER_MANAGEMENT_BASE ARG NEXT_PUBLIC_API_URL ARG VITE_GOOGLE_MAPS_API_KEY +ARG VITE_POSTHOG_KEY +ARG VITE_POSTHOG_HOST ENV VITE_API_URL=${VITE_API_URL} ENV VITE_BASE_API_URL=${VITE_BASE_API_URL} ENV VITE_USER_MANAGEMENT_BASE=${VITE_USER_MANAGEMENT_BASE} ENV NEXT_PUBLIC_API_URL=${NEXT_PUBLIC_API_URL} ENV VITE_GOOGLE_MAPS_API_KEY=${VITE_GOOGLE_MAPS_API_KEY} +ENV VITE_POSTHOG_KEY=${VITE_POSTHOG_KEY} +ENV VITE_POSTHOG_HOST=${VITE_POSTHOG_HOST} RUN if [ -z "$VITE_API_URL" ] || [ -z "$VITE_BASE_API_URL" ] || [ -z "$VITE_USER_MANAGEMENT_BASE" ]; then \ echo "ERROR: VITE_API_URL, VITE_BASE_API_URL, and VITE_USER_MANAGEMENT_BASE must all be set" && \ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 97702902c..108b45d8b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -241,6 +241,9 @@ importers: '@mantine/hooks': specifier: ^9.3.0 version: 9.3.0(react@19.2.6) + '@posthog/react': + specifier: ^1.10.3 + version: 1.10.3(@types/react@18.3.31)(posthog-js@1.400.1)(react@19.2.6) '@radix-ui/react-accordion': specifier: ^1.2.13 version: 1.2.13(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) @@ -409,6 +412,9 @@ importers: pdf-lib: specifier: ^1.17.1 version: 1.17.1 + posthog-js: + specifier: ^1.400.1 + version: 1.400.1 prop-types: specifier: ^15.8.1 version: 15.8.1 @@ -566,12 +572,15 @@ importers: '@mantine/hooks': specifier: ^9.3.0 version: 9.3.0(react@19.2.6) + '@posthog/react': + specifier: ^1.10.3 + version: 1.10.3(@types/react@18.3.31)(posthog-js@1.400.1)(react@19.2.6) '@tanstack/react-query': specifier: ^5.59.0 version: 5.101.0(react@19.2.6) '@tria-plc/iamui': specifier: file:../../../local-packages/tria-plc-iamui-0.1.1.tgz - version: file:local-packages/tria-plc-iamui-0.1.1.tgz(a5d0bdee55164ae56064fb273b530e00) + version: file:local-packages/tria-plc-iamui-0.1.1.tgz(0ce39b7e349029277dcd938d06eeb0f7) '@vis.gl/react-google-maps': specifier: ^1.8.3 version: 1.8.3(react-dom@19.2.6(react@19.2.6))(react@19.2.6) @@ -590,6 +599,9 @@ importers: lucide-react: specifier: ^1.14.0 version: 1.17.0(react@19.2.6) + posthog-js: + specifier: ^1.400.1 + version: 1.400.1 radix-ui: specifier: ^1.4.3 version: 1.5.0(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) @@ -3125,6 +3137,22 @@ packages: '@popperjs/core@2.11.8': resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==} + '@posthog/core@1.41.1': + resolution: {integrity: sha512-lKjPdeawDSvRhHnP14RwTSI5CofuyluhG3ISHRa+Kj6PyfSrEUyIkoVOpYWicFGgWikeaJCZzFQpo7ngHt9BcA==} + + '@posthog/react@1.10.3': + resolution: {integrity: sha512-Qu//fGQmVlX0B9kTA3LLg67e7AYLEmeuA0Bf1qSyUM0uUILcRQGjQezhNQPLYSTakOqvXEnl6fM2iQBF6Toxrw==} + peerDependencies: + '@types/react': '>=16.8.0' + posthog-js: '>=1.257.2' + react: '>=16.8.0' + peerDependenciesMeta: + '@types/react': + optional: true + + '@posthog/types@1.394.0': + resolution: {integrity: sha512-ifQ7p8o8hoHErlJmpzCFzHQcuRam0vXk8LBVhBu4BlPYP6S0tog4FSAFItnI/nwN6cHZI0WFQilr7sIqQa7Flg==} + '@prisma/client@6.19.3': resolution: {integrity: sha512-mKq3jQFhjvko5LTJFHGilsuQs+W+T3Gm451NzuTDGQxwCzwXHYnIu2zGkRoW+Exq3Rob7yp2MfzSrdIiZVhrBg==} engines: {node: '>=18.18'} @@ -7050,6 +7078,9 @@ packages: resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==} engines: {node: ^12.20 || >= 14.13} + fflate@0.4.8: + resolution: {integrity: sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA==} + fflate@0.8.3: resolution: {integrity: sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==} @@ -9498,10 +9529,21 @@ packages: resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==} engines: {node: '>=0.10.0'} + posthog-js@1.400.1: + resolution: {integrity: sha512-NGfzNwTu+VBw4FekgYs/aQbEkTFkvmpTFUKDGZw/9K6R/sG2WyuLsnnXySRcNH8RMki0Io8v9flNATrrTfmT+Q==} + powershell-utils@0.1.0: resolution: {integrity: sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A==} engines: {node: '>=20'} + preact@10.29.7: + resolution: {integrity: sha512-DCHYrK/B10yUD3ZjLfhZ3WIE/9Vf9VFUODcRE2dRomTYDpJk6z6L9wecSfhfE6M9ZTHUdyQkoC46arIDhEV84Q==} + peerDependencies: + preact-render-to-string: '>=5' + peerDependenciesMeta: + preact-render-to-string: + optional: true + prelude-ls@1.2.1: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} @@ -9601,6 +9643,9 @@ packages: resolution: {integrity: sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==} engines: {node: '>=0.6'} + query-selector-shadow-dom@1.0.1: + resolution: {integrity: sha512-lT5yCqEBgfoMYpf3F2xQRK7zEr1rhIIZuceDK6+xRkJQ4NMbHTwXqk4NkwDwQMNqXgG9r9fyHnzwNVs6zV5KRw==} + query-string@7.1.3: resolution: {integrity: sha512-hh2WYhq4fi8+b+/2Kg9CEge4fDPvHS534aOOvOZeQ3+Vf2mCFsaFBYj0i+iXcAq6I9Vzp5fjMFBlONvayDC1qg==} engines: {node: '>=6'} @@ -11478,6 +11523,9 @@ packages: resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} engines: {node: '>= 8'} + web-vitals@5.3.0: + resolution: {integrity: sha512-q6LWsLatGYZp5VGBIOvbTj6JBV2nOmC8KvWztXBmwJcfFAzhwKwbOxhUH306XY3CcaZDUlSmSuNPBsCn0bFu+g==} + webdriver-bidi-protocol@0.4.1: resolution: {integrity: sha512-ARrjNjtWRRs2w4Tk7nqrf2gBI0QXWuOmMCx2hU+1jUt6d00MjMxURrhxhGbrsoiZKJrhTSTzbIrc554iKI10qw==} @@ -13696,6 +13744,19 @@ snapshots: '@popperjs/core@2.11.8': {} + '@posthog/core@1.41.1': + dependencies: + '@posthog/types': 1.394.0 + + '@posthog/react@1.10.3(@types/react@18.3.31)(posthog-js@1.400.1)(react@19.2.6)': + dependencies: + posthog-js: 1.400.1 + react: 19.2.6 + optionalDependencies: + '@types/react': 18.3.31 + + '@posthog/types@1.394.0': {} + '@prisma/client@6.19.3(prisma@6.19.3(typescript@5.9.3))(typescript@5.9.3)': optionalDependencies: prisma: 6.19.3(typescript@5.9.3) @@ -16092,130 +16153,6 @@ snapshots: - utf-8-validate - vite - '@tria-plc/iamui@file:local-packages/tria-plc-iamui-0.1.1.tgz(a5d0bdee55164ae56064fb273b530e00)': - dependencies: - '@emotion/react': 11.14.0(@types/react@18.3.31)(react@19.2.6) - '@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@18.3.31)(react@19.2.6))(@types/react@18.3.31)(react@19.2.6) - '@hookform/resolvers': 5.4.0(react-hook-form@7.77.0(react@19.2.6)) - '@lottiefiles/react-lottie-player': 3.6.0(react@19.2.6) - '@mantine/charts': 7.17.8(@mantine/core@7.17.8(@mantine/hooks@7.17.8(react@19.2.6))(@types/react@18.3.31)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(@mantine/hooks@7.17.8(react@19.2.6))(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(recharts@3.8.1(@types/react@18.3.31)(react-dom@19.2.6(react@19.2.6))(react-is@19.2.7)(react@19.2.6)(redux@5.0.1)) - '@mantine/core': 7.17.8(@mantine/hooks@7.17.8(react@19.2.6))(@types/react@18.3.31)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@mantine/dates': 7.17.8(@mantine/core@7.17.8(@mantine/hooks@7.17.8(react@19.2.6))(@types/react@18.3.31)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(@mantine/hooks@7.17.8(react@19.2.6))(dayjs@1.11.21)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@mantine/hooks': 7.17.8(react@19.2.6) - '@mantine/notifications': 7.17.8(@mantine/core@7.17.8(@mantine/hooks@7.17.8(react@19.2.6))(@types/react@18.3.31)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(@mantine/hooks@7.17.8(react@19.2.6))(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-accordion': 1.2.13(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-alert-dialog': 1.1.16(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-avatar': 1.1.12(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-checkbox': 1.3.4(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-collapsible': 1.1.13(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-context-menu': 2.3.0(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-dialog': 1.1.16(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-dropdown-menu': 2.1.17(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-hover-card': 1.1.16(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-label': 2.1.9(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-navigation-menu': 1.2.15(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-popover': 1.1.16(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-progress': 1.1.9(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-radio-group': 1.4.0(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-scroll-area': 1.2.11(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-select': 2.3.0(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-separator': 1.1.9(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-slot': 1.2.5(@types/react@18.3.31)(react@19.2.6) - '@radix-ui/react-switch': 1.3.0(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-tabs': 1.1.14(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-toast': 1.2.16(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-tooltip': 1.2.9(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@react-pdf-viewer/core': 3.12.0(pdfjs-dist@5.4.296)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@react-pdf-viewer/default-layout': 3.12.0(pdfjs-dist@5.4.296)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@react-pdf/renderer': 4.5.1(react@19.2.6) - '@reduxjs/toolkit': 2.12.0(react-redux@9.3.0(@types/react@18.3.31)(react@19.2.6)(redux@5.0.1))(react@19.2.6) - '@tabler/icons-react': 3.44.0(react@19.2.6) - '@tailwindcss/vite': 4.3.0(vite@5.4.21(@types/node@24.13.1)(lightningcss@1.32.0)(terser@5.48.0)) - '@tanstack/react-query': 5.101.0(react@19.2.6) - '@tanstack/react-query-devtools': 5.101.0(@tanstack/react-query@5.101.0(react@19.2.6))(react@19.2.6) - '@tanstack/react-table': 8.21.3(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@tinymce/tinymce-react': 6.3.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(tinymce@7.9.3) - '@types/dompurify': 3.2.0 - '@types/node': 24.13.1 - '@types/tinymce': 4.6.9 - axios: 1.17.0 - class-variance-authority: 0.7.1 - clsx: 2.1.1 - cmdk: 1.1.1(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - date-fns: 3.6.0 - dayjs: 1.11.21 - dompurify: 3.4.8 - ethiopian-calendar-date-converter: 2.1.6 - ethiopian-calendar-new: 1.1.0 - file-type: 18.7.0 - framer-motion: 12.40.0(@emotion/is-prop-valid@1.4.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - html2canvas: 1.4.1 - i18next: 25.10.10(typescript@5.9.3) - i18next-browser-languagedetector: 8.2.1 - jquery: 3.7.1 - js-cookie: 3.0.8 - jspdf: 3.0.4 - lodash: 4.18.1 - lucide-react: 0.513.0(react@19.2.6) - mantine-react-table: 2.0.0-beta.9(@mantine/core@7.17.8(@mantine/hooks@7.17.8(react@19.2.6))(@types/react@18.3.31)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(@mantine/dates@7.17.8(@mantine/core@7.17.8(@mantine/hooks@7.17.8(react@19.2.6))(@types/react@18.3.31)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(@mantine/hooks@7.17.8(react@19.2.6))(dayjs@1.11.21)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(@mantine/hooks@7.17.8(react@19.2.6))(@tabler/icons-react@3.44.0(react@19.2.6))(clsx@2.1.1)(dayjs@1.11.21)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - mui-ethiopian-datepicker: 0.3.2(4b3af212eafdf0059f009b005d7e343d) - next-themes: 0.4.6(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - path: 0.12.7 - pdf-lib: 1.17.1 - qs: 6.15.2 - react: 19.2.6 - react-cookie: 8.1.2(@types/react@18.3.31)(react@19.2.6) - react-css-nocode-editor: 1.0.13(react-dom@19.2.6(react@19.2.6))(react-is@19.2.7)(react@19.2.6) - react-day-picker: 8.10.2(date-fns@3.6.0)(react@19.2.6) - react-dom: 19.2.6(react@19.2.6) - react-dropzone: 14.4.1(react@19.2.6) - react-hook-form: 7.77.0(react@19.2.6) - react-i18next: 15.7.4(i18next@25.10.10(typescript@5.9.3))(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(typescript@5.9.3) - react-icons: 5.6.0(react@19.2.6) - react-image-crop: 11.0.10(react@19.2.6) - react-intersection-observer: 9.16.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - react-pdf: 10.4.1(@types/react@18.3.31)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - react-pdf-html: 2.1.5(@react-pdf/renderer@4.5.1(react@19.2.6))(react@19.2.6) - react-redux: 9.3.0(@types/react@18.3.31)(react@19.2.6)(redux@5.0.1) - react-resizable-panels: 3.0.6(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - react-router-dom: 7.17.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - react-signature-canvas: 1.1.0-alpha.2(@types/prop-types@15.7.15)(@types/react@18.3.31)(prop-types@15.8.1)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - recharts: 3.8.1(@types/react@18.3.31)(react-dom@19.2.6(react@19.2.6))(react-is@19.2.7)(react@19.2.6)(redux@5.0.1) - rollup-plugin-visualizer: 7.0.1(rollup@4.61.1) - socket.io-client: 4.8.3 - sonner: 2.0.7(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - tailwind-merge: 3.6.0 - tailwind-scrollbar-hide: 4.0.0(tailwindcss@4.3.0) - tailwindcss: 4.3.0 - tailwindcss-animate: 1.0.7(tailwindcss@4.3.0) - tinymce: 7.9.3 - url: 0.11.4 - vaul: 1.1.2(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - xlsx: 0.18.5 - zod: 3.25.76 - transitivePeerDependencies: - - '@babel/core' - - '@emotion/is-prop-valid' - - '@mui/icons-material' - - '@mui/material' - - '@mui/x-date-pickers' - - '@types/prop-types' - - '@types/react' - - '@types/react-dom' - - bufferutil - - debug - - pdfjs-dist - - prop-types - - react-is - - react-native - - redux - - rolldown - - rollup - - supports-color - - typescript - - utf-8-validate - - vite - '@ts-morph/common@0.27.0': dependencies: fast-glob: 3.3.3 @@ -17402,16 +17339,6 @@ snapshots: transitivePeerDependencies: - supports-color - babel-plugin-styled-components@2.3.0(styled-components@5.3.11(react-dom@19.2.6(react@19.2.6))(react-is@19.2.7)(react@19.2.6))(supports-color@5.5.0): - dependencies: - '@babel/helper-annotate-as-pure': 7.29.7 - '@babel/helper-module-imports': 7.29.7(supports-color@5.5.0) - '@babel/plugin-syntax-jsx': 7.29.7(@babel/core@7.29.7) - picomatch: 4.0.4 - styled-components: 5.3.11(react-dom@19.2.6(react@19.2.6))(react-is@19.2.7)(react@19.2.6) - transitivePeerDependencies: - - supports-color - babel-polyfill@6.26.0: dependencies: babel-runtime: 6.26.0 @@ -18015,8 +17942,7 @@ snapshots: core-js@2.6.12: {} - core-js@3.49.0: - optional: true + core-js@3.49.0: {} core-util-is@1.0.3: {} @@ -19202,6 +19128,8 @@ snapshots: node-domexception: 1.0.0 web-streams-polyfill: 3.3.3 + fflate@0.4.8: {} + fflate@0.8.3: {} figures@1.7.0: @@ -21856,8 +21784,23 @@ snapshots: dependencies: xtend: 4.0.2 + posthog-js@1.400.1: + dependencies: + '@posthog/core': 1.41.1 + '@posthog/types': 1.394.0 + core-js: 3.49.0 + dompurify: 3.4.8 + fflate: 0.4.8 + preact: 10.29.7 + query-selector-shadow-dom: 1.0.1 + web-vitals: 5.3.0 + transitivePeerDependencies: + - preact-render-to-string + powershell-utils@0.1.0: {} + preact@10.29.7: {} + prelude-ls@1.2.1: {} prettier@3.8.3: {} @@ -21981,6 +21924,8 @@ snapshots: dependencies: side-channel: 1.1.0 + query-selector-shadow-dom@1.0.1: {} + query-string@7.1.3: dependencies: decode-uri-component: 0.2.2 @@ -22173,15 +22118,6 @@ snapshots: - '@babel/core' - react-is - react-css-nocode-editor@1.0.13(react-dom@19.2.6(react@19.2.6))(react-is@19.2.7)(react@19.2.6): - dependencies: - react: 19.2.6 - react-dom: 19.2.6(react@19.2.6) - styled-components: 5.3.11(react-dom@19.2.6(react@19.2.6))(react-is@19.2.7)(react@19.2.6) - transitivePeerDependencies: - - '@babel/core' - - react-is - react-day-picker@8.10.2(date-fns@3.6.0)(react@19.2.6): dependencies: date-fns: 3.6.0 @@ -23371,24 +23307,6 @@ snapshots: transitivePeerDependencies: - '@babel/core' - styled-components@5.3.11(react-dom@19.2.6(react@19.2.6))(react-is@19.2.7)(react@19.2.6): - dependencies: - '@babel/helper-module-imports': 7.29.7(supports-color@5.5.0) - '@babel/traverse': 7.29.7(supports-color@5.5.0) - '@emotion/is-prop-valid': 1.4.0 - '@emotion/stylis': 0.8.5 - '@emotion/unitless': 0.7.5 - babel-plugin-styled-components: 2.3.0(styled-components@5.3.11(react-dom@19.2.6(react@19.2.6))(react-is@19.2.7)(react@19.2.6))(supports-color@5.5.0) - css-to-react-native: 3.2.0 - hoist-non-react-statics: 3.3.2 - react: 19.2.6 - react-dom: 19.2.6(react@19.2.6) - react-is: 19.2.7 - shallowequal: 1.1.0 - supports-color: 5.5.0 - transitivePeerDependencies: - - '@babel/core' - styled-jsx@5.1.1(babel-plugin-macros@3.1.0)(react@18.3.1): dependencies: client-only: 0.0.1 @@ -24334,6 +24252,8 @@ snapshots: web-streams-polyfill@3.3.3: {} + web-vitals@5.3.0: {} + webdriver-bidi-protocol@0.4.1: {} webidl-conversions@7.0.0: {}