From c00a7a439aa2e9c29b34551c2994868c373ebb23 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 16 Apr 2026 23:29:36 +0000 Subject: [PATCH 01/12] feat(electron): error handling foundation Adds the infrastructure for a coherent client-side error story: - `lib/errors.ts`: canonical ApiError + QuotaExceededError, `toUserMessage` (friendly strings for ApiError/ZodError/network errors, strips Electron IPC message prefixes), `logError` (expected), `reportError` (unexpected). - `lib/query-client.ts`: QueryClient factory with sane retry defaults (no retry on 4xx except 408/429, 2 retries otherwise; 0 mutation retries), `QueryCache` onError logs + opts in via `meta.toastOnError`, and `MutationCache` onError toasts `toUserMessage(err)` by default with `meta.suppressToast` as the opt-out. - `components/app-error-boundary.tsx` + `error-fallback.tsx`: two boundaries (top-level outside the router, route-level inside) with a Card-based fallback offering 'Go home' + 'Try again'. Route boundary resets on pathname change and clears React Query error cache on retry. - `main/ipc-utils.ts` + main.ts migration: `safeHandle` wraps ipcMain.handle so main-process failures log with full stack and surface a sanitized message to the renderer. `link:fetch-metadata` keeps its null contract but now logs. - `useCreateParticle` opts out of the global toast (compose-overlay renders its own quota UX) so nothing double-toasts. Render crashes now have a recovery UI, every mutation gets a free error toast, and silent-catch cleanup + Sentry land in follow-up PRs. --- js/package.json | 1 + js/src/App.tsx | 50 ++++++----- js/src/api/client.ts | 12 +-- js/src/components/app-error-boundary.tsx | 55 ++++++++++++ js/src/components/error-fallback.tsx | 101 +++++++++++++++++++++++ js/src/features/layout.tsx | 3 +- js/src/hooks/use-create-particle.ts | 18 ++-- js/src/lib/errors.ts | 92 +++++++++++++++++++++ js/src/lib/query-client.ts | 62 ++++++++++++++ js/src/main.ts | 15 ++-- js/src/main/ipc-utils.ts | 24 ++++++ js/yarn.lock | 5 ++ 12 files changed, 389 insertions(+), 49 deletions(-) create mode 100644 js/src/components/app-error-boundary.tsx create mode 100644 js/src/components/error-fallback.tsx create mode 100644 js/src/lib/errors.ts create mode 100644 js/src/lib/query-client.ts create mode 100644 js/src/main/ipc-utils.ts diff --git a/js/package.json b/js/package.json index 45da921..a75b43a 100644 --- a/js/package.json +++ b/js/package.json @@ -65,6 +65,7 @@ "radix-ui": "^1.4.3", "react": "^19.2.4", "react-dom": "^19.2.4", + "react-error-boundary": "^6.1.1", "react-markdown": "^10.1.0", "react-router-dom": "^7.13.0", "react-use": "^17.6.0", diff --git a/js/src/App.tsx b/js/src/App.tsx index 09999e1..be7e21d 100644 --- a/js/src/App.tsx +++ b/js/src/App.tsx @@ -3,10 +3,7 @@ import { HashRouter, Routes, Route, useNavigate } from "react-router-dom"; import { TooltipProvider } from "@/components/ui/tooltip"; import { useAuthStore } from "@/stores/auth-store"; import { LoginPage } from "@/features/auth/login-page"; -import { - QueryClient, - QueryClientProvider, -} from '@tanstack/react-query' +import { QueryClientProvider } from '@tanstack/react-query'; import SettingsPage from "@/features/settings-page"; import AudioVideoSettingsPage from "@/features/settings/audio-video-settings-page"; import NetworkSelector from "@/features/network-selector"; @@ -16,8 +13,13 @@ import Layout from "@/features/layout"; import NetworkSettingsPage from "@/features/network-settings"; import { Toaster } from "@/components/ui/sonner"; import { PusherProvider } from "@/lib/pusher-provider"; +import { createQueryClient } from "@/lib/query-client"; +import { + RouteErrorBoundary, + TopLevelErrorBoundary, +} from "@/components/app-error-boundary"; -const queryClient = new QueryClient(); +const queryClient = createQueryClient(); const App = () => { const status = useAuthStore((s) => s.status); @@ -62,30 +64,34 @@ function AuthenticatedApp() { return ( - - } /> - } /> + + + } /> + } /> - - } /> - - } /> - } /> - } /> + + } /> + + } /> + } /> + } /> + - - + + ); } const AppWithProviders = () => ( - - - - - - + + + + + + + + ); export default AppWithProviders; diff --git a/js/src/api/client.ts b/js/src/api/client.ts index 938355d..7040cfa 100644 --- a/js/src/api/client.ts +++ b/js/src/api/client.ts @@ -1,5 +1,6 @@ import { appConfig } from "@/config/env"; import { useSessionStore } from "@/stores/session-store"; +import { ApiError } from "@/lib/errors"; import type { z } from "zod"; import { BillingStatusSchema, @@ -27,15 +28,8 @@ import type { SignInRequest, } from "./types"; -export class ApiError extends Error { - constructor( - public status: number, - message: string, - ) { - super(message); - this.name = "ApiError"; - } -} +// Re-export for back-compat with existing `import { ApiError } from "@/api/client"`. +export { ApiError }; interface ApiClientConfig { baseUrl: string; diff --git a/js/src/components/app-error-boundary.tsx b/js/src/components/app-error-boundary.tsx new file mode 100644 index 0000000..7f8a0b6 --- /dev/null +++ b/js/src/components/app-error-boundary.tsx @@ -0,0 +1,55 @@ +import type { PropsWithChildren } from "react"; +import { ErrorBoundary } from "react-error-boundary"; +import { useLocation } from "react-router-dom"; +import { useQueryErrorResetBoundary } from "@tanstack/react-query"; +import { reportError } from "@/lib/errors"; +import { + RouteErrorFallback, + TopLevelErrorFallback, +} from "@/components/error-fallback"; + +/** + * Catches render crashes OUTSIDE react-router. Used once at the root so a + * broken bootstrap still shows a recovery UI instead of a white screen. + */ +export function TopLevelErrorBoundary({ children }: PropsWithChildren) { + return ( + + reportError(error, { + boundary: "top", + componentStack: info.componentStack, + }) + } + > + {children} + + ); +} + +/** + * Catches render crashes INSIDE the router. Auto-resets when the route + * changes (so clicking "Go home" clears the error) and resets the React + * Query error cache on retry so stale failures don't stick. + */ +export function RouteErrorBoundary({ children }: PropsWithChildren) { + const location = useLocation(); + const { reset: resetQueries } = useQueryErrorResetBoundary(); + return ( + + reportError(error, { + boundary: "route", + pathname: location.pathname, + componentStack: info.componentStack, + }) + } + onReset={() => resetQueries()} + resetKeys={[location.pathname]} + > + {children} + + ); +} diff --git a/js/src/components/error-fallback.tsx b/js/src/components/error-fallback.tsx new file mode 100644 index 0000000..4769d5d --- /dev/null +++ b/js/src/components/error-fallback.tsx @@ -0,0 +1,101 @@ +import type { FallbackProps } from "react-error-boundary"; +import { useNavigate } from "react-router-dom"; +import { AlertTriangle } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { toUserMessage } from "@/lib/errors"; +import { appEnv } from "@/config/env"; + +function ErrorCard({ + error, + onGoHome, + onRetry, +}: { + error: unknown; + onGoHome: () => void; + onRetry: () => void; +}) { + return ( +
+ + +
+ + Something went wrong +
+ {toUserMessage(error)} +
+ {appEnv === "dev" && error instanceof Error ? ( + +
+ + Technical details + +
+                {error.stack ?? error.message}
+              
+
+
+ ) : null} + + + + +
+
+ ); +} + +/** + * Fallback for the boundary mounted OUTSIDE the router. `useNavigate` isn't + * available here, so "Go home" drops the hash fragment directly. + */ +export function TopLevelErrorFallback({ + error, + resetErrorBoundary, +}: FallbackProps) { + const goHome = () => { + window.location.hash = "#/"; + resetErrorBoundary(); + }; + return ( + + ); +} + +/** + * Fallback for boundaries mounted INSIDE the router. Uses react-router's + * navigate so the browser history stays consistent. + */ +export function RouteErrorFallback({ + error, + resetErrorBoundary, +}: FallbackProps) { + const navigate = useNavigate(); + const goHome = () => { + navigate("/"); + resetErrorBoundary(); + }; + return ( + + ); +} diff --git a/js/src/features/layout.tsx b/js/src/features/layout.tsx index 7aa3d45..b5ae8da 100644 --- a/js/src/features/layout.tsx +++ b/js/src/features/layout.tsx @@ -20,6 +20,7 @@ import type { Particle } from "@/api/types"; import { PropsWithChildren, useCallback } from "react"; import { useDockBadge } from "@/hooks/use-dock-badge"; import { toast } from "sonner"; +import { RouteErrorBoundary } from "@/components/app-error-boundary"; function getParticleDisplayName(particle: Particle): string { switch (particle.type) { @@ -177,7 +178,7 @@ export default function Layout({ children }: PropsWithChildren) { return (
- {children} + {children}
); } diff --git a/js/src/hooks/use-create-particle.ts b/js/src/hooks/use-create-particle.ts index b49767e..c08bc8b 100644 --- a/js/src/hooks/use-create-particle.ts +++ b/js/src/hooks/use-create-particle.ts @@ -2,6 +2,7 @@ import { useMutation, useQueryClient } from "@tanstack/react-query"; import { createParticle, createStreamParticle } from "@/lib/firestore-particles"; import { CONTAINER_TYPES, type NetworkUsage, type ParticleType, type ParticlePropertiesMap } from "@/api/types"; import { parseParticlePath, particlePath, ParticlePath, toFirestoreChildrenPath } from "@/lib/particle-path"; +import { QuotaExceededError } from "@/lib/errors"; import { isUsageExhausted, networkUsageQueryKey, @@ -9,18 +10,8 @@ import { useInvalidateNetworkUsage, } from "./use-network-usage"; -/** - * Thrown when a free-plan network attempts to create a non-container particle - * after hitting its daily message limit. Callers should surface an upgrade - * prompt; compose UI should also disable triggers proactively via - * `useNetworkUsage` rather than relying on this throw. - */ -export class QuotaExceededError extends Error { - constructor(public readonly networkId: string) { - super("Daily message limit reached"); - this.name = "QuotaExceededError"; - } -} +// Re-export for back-compat with existing `import { QuotaExceededError } from "@/hooks/use-create-particle"`. +export { QuotaExceededError }; interface CreateParticleParams { // Path to which the new particle will be added as a child @@ -36,6 +27,9 @@ export function useCreateParticle() { const invalidateUsage = useInvalidateNetworkUsage(); return useMutation({ + // Compose UI renders a custom quota-exceeded toast + cancels the overlay. + // Opt out of the global mutation error toast to avoid a double-toast. + meta: { suppressToast: true }, mutationFn: async (params: CreateParticleParams) => { const { networkId } = parseParticlePath(params.path); diff --git a/js/src/lib/errors.ts b/js/src/lib/errors.ts new file mode 100644 index 0000000..87883e9 --- /dev/null +++ b/js/src/lib/errors.ts @@ -0,0 +1,92 @@ +import { z } from "zod"; +import { appEnv } from "@/config/env"; + +/** + * Thrown by the API client for any non-2xx response. + */ +export class ApiError extends Error { + constructor( + public status: number, + message: string, + ) { + super(message); + this.name = "ApiError"; + } +} + +/** + * Thrown when a free-plan network attempts to create a non-container particle + * after hitting its daily message limit. Callers should surface an upgrade + * prompt; compose UI should also disable triggers proactively via + * `useNetworkUsage` rather than relying on this throw. + */ +export class QuotaExceededError extends Error { + constructor(public readonly networkId: string) { + super("Daily message limit reached"); + this.name = "QuotaExceededError"; + } +} + +const IPC_PREFIX = /^Error invoking remote method '[^']+':\s*/; + +function normalizeMessage(message: string): string { + return message.replace(IPC_PREFIX, "").replace(/^Error:\s*/, "").trim(); +} + +/** + * Maps any thrown value to a short, user-facing string. Used by the global + * MutationCache toast handler, the error-boundary fallback, and any call site + * that wants to surface an error in the UI. + */ +export function toUserMessage(err: unknown): string { + if (err instanceof ApiError) { + if (err.status === 401) return "Please sign in again."; + if (err.status === 403) return "You don't have permission to do that."; + if (err.status === 404) return "Not found."; + if (err.status === 408 || err.status === 429) { + return "Please try again in a moment."; + } + if (err.status >= 500) { + return "Something went wrong on our end. Please try again."; + } + return normalizeMessage(err.message) || "Request failed."; + } + + if (err instanceof z.ZodError) { + return "Received unexpected data from the server."; + } + + if (err instanceof TypeError && /fetch|network/i.test(err.message)) { + return "Network error. Check your connection."; + } + + if (err instanceof Error) { + return normalizeMessage(err.message) || "Something went wrong."; + } + + return "Something went wrong."; +} + +type ErrorContext = Record; + +/** + * For expected-but-worth-recording failures (silent-catch sites). In dev, + * prints to console. In prod today this is a no-op; PR 4 wires this up to + * Sentry breadcrumbs without touching call sites. + */ +export function logError(err: unknown, context?: ErrorContext): void { + if (appEnv === "dev") { + // eslint-disable-next-line no-console + console.error("[error]", err, context ?? {}); + } +} + +/** + * For unexpected failures the user may not have seen (render crashes, query + * errors, background mutations). Always surfaces somewhere. PR 4 wires this + * up to `Sentry.captureException`. + */ +export function reportError(err: unknown, context?: ErrorContext): void { + // eslint-disable-next-line no-console + console.error("[error]", err, context ?? {}); +} diff --git a/js/src/lib/query-client.ts b/js/src/lib/query-client.ts new file mode 100644 index 0000000..e49addc --- /dev/null +++ b/js/src/lib/query-client.ts @@ -0,0 +1,62 @@ +import { + MutationCache, + QueryCache, + QueryClient, +} from "@tanstack/react-query"; +import { toast } from "sonner"; +import { ApiError, logError, reportError, toUserMessage } from "@/lib/errors"; + +declare module "@tanstack/react-query" { + interface Register { + queryMeta: { + /** Show a sonner error toast automatically on query failure. */ + toastOnError?: boolean; + }; + mutationMeta: { + /** Opt out of the default sonner error toast (caller handles feedback). */ + suppressToast?: boolean; + }; + } +} + +function shouldRetryQuery(failureCount: number, err: unknown): boolean { + if (err instanceof ApiError) { + // Retry only on transient status codes; 4xx generally won't succeed on retry. + if (err.status === 408 || err.status === 429) return failureCount < 2; + if (err.status >= 400 && err.status < 500) return false; + } + return failureCount < 2; +} + +export function createQueryClient(): QueryClient { + return new QueryClient({ + defaultOptions: { + queries: { + retry: shouldRetryQuery, + refetchOnWindowFocus: false, + }, + mutations: { + // Mutations have side effects — never auto-retry. + retry: 0, + }, + }, + queryCache: new QueryCache({ + onError: (err, query) => { + logError(err, { scope: "query", queryKey: query.queryKey }); + if (query.meta?.toastOnError) { + toast.error(toUserMessage(err)); + } + }, + }), + mutationCache: new MutationCache({ + onError: (err, _variables, _context, mutation) => { + reportError(err, { + scope: "mutation", + mutationKey: mutation.options.mutationKey, + }); + if (mutation.meta?.suppressToast) return; + toast.error(toUserMessage(err)); + }, + }), + }); +} diff --git a/js/src/main.ts b/js/src/main.ts index e56453f..16fca8a 100644 --- a/js/src/main.ts +++ b/js/src/main.ts @@ -5,6 +5,7 @@ import { updateElectronApp, UpdateSourceType } from 'update-electron-app'; import type { LinkMetadata } from './lib/link-metadata'; import { appConfig } from './config/env'; +import { safeHandle } from './main/ipc-utils'; if (app.isPackaged) { updateElectronApp({ @@ -291,7 +292,7 @@ ipcMain.on('window:close-huddle', () => { huddleWindow?.close(); }); -ipcMain.handle('screen:get-sources', async () => { +safeHandle('screen:get-sources', async () => { const sources = await desktopCapturer.getSources({ types: ['screen', 'window'], thumbnailSize: { width: 320, height: 180 }, @@ -457,7 +458,11 @@ async function fetchLinkMetadata(url: string): Promise { metadataCache.set(url, metadata); return metadata; - } catch { + } catch (err) { + // Metadata is a progressive enhancement — keep the null contract, but log + // so upstream failures (DNS, TLS, aborted fetches) aren't invisible. + // eslint-disable-next-line no-console + console.error('[ipc:link.fetch-metadata]', err); return null; } } @@ -470,9 +475,9 @@ ipcMain.on('app:set-dock-badge', (_event, count: number) => { } }); -ipcMain.handle('app:get-version', () => app.getVersion()); +safeHandle('app:get-version', () => app.getVersion()); -ipcMain.handle('link:fetch-metadata', async (_event, url: string) => { +safeHandle('link:fetch-metadata', async (_event, url) => { if (typeof url !== 'string') return null; try { new URL(url); @@ -482,7 +487,7 @@ ipcMain.handle('link:fetch-metadata', async (_event, url: string) => { return fetchLinkMetadata(url); }); -ipcMain.handle('link:open-external', async (_event, url: string) => { +safeHandle('link:open-external', async (_event, url) => { if (typeof url !== 'string') return; // Only allow http(s) URLs for security if (!url.startsWith('http://') && !url.startsWith('https://')) return; diff --git a/js/src/main/ipc-utils.ts b/js/src/main/ipc-utils.ts new file mode 100644 index 0000000..de53265 --- /dev/null +++ b/js/src/main/ipc-utils.ts @@ -0,0 +1,24 @@ +import { ipcMain, type IpcMainInvokeEvent } from "electron"; + +type InvokeHandler = ( + event: IpcMainInvokeEvent, + ...args: unknown[] +) => Promise | unknown; + +/** + * Wraps `ipcMain.handle` so handler failures log the full stack in the main + * process and re-throw a sanitized message to the renderer. Without this, + * main-process exceptions silently reject the renderer promise with no trace. + */ +export function safeHandle(channel: string, handler: InvokeHandler): void { + ipcMain.handle(channel, async (event, ...args) => { + try { + return await handler(event, ...(args as unknown[])); + } catch (err) { + // eslint-disable-next-line no-console + console.error(`[ipc:${channel}]`, err); + const message = err instanceof Error ? err.message : "IPC error"; + throw new Error(message); + } + }); +} diff --git a/js/yarn.lock b/js/yarn.lock index bd78187..ca86d6c 100644 --- a/js/yarn.lock +++ b/js/yarn.lock @@ -8291,6 +8291,11 @@ react-dom@^19.2.4: dependencies: scheduler "^0.27.0" +react-error-boundary@^6.1.1: + version "6.1.1" + resolved "https://registry.yarnpkg.com/react-error-boundary/-/react-error-boundary-6.1.1.tgz#491d655e86c32434ede852755bb649119fdddd89" + integrity sha512-BrYwPOdXi5mqkk5lw+Uvt0ThHx32rCt3BkukS4X23A2AIWDPSGX6iaWTc0y9TU/mHDA/6qOSGel+B2ERkOvD1w== + react-markdown@^10.1.0: version "10.1.0" resolved "https://registry.yarnpkg.com/react-markdown/-/react-markdown-10.1.0.tgz#e22bc20faddbc07605c15284255653c0f3bad5ca" -- 2.54.0 From 30469fda6612eb316749b4c6e06b009bfc2c393f Mon Sep 17 00:00:00 2001 From: talksik Date: Thu, 16 Apr 2026 18:00:43 -0700 Subject: [PATCH 02/12] revert "fix: prevent writing to nonexistent firestore docs" This reverts commit 46170eb5d38090f770faffa895425ffc71f5bcf3. --- go/internal/handler/handler.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go/internal/handler/handler.go b/go/internal/handler/handler.go index 86d9488..51a8970 100644 --- a/go/internal/handler/handler.go +++ b/go/internal/handler/handler.go @@ -1151,7 +1151,7 @@ func (h *Handler) HandleLivekitWebhook(w http.ResponseWriter, r *http.Request) { _, err = docRef.Update(ctx, []firestore.Update{ {Path: "huddle_active_participants", Value: participantIds}, - }, firestore.Exists) + }) if err != nil { slog.Error("failed to update huddle participants in firestore", "error", err, "path", docPath) } -- 2.54.0 From 8632c98725479b436a366cdf6f6a2b95793e0911 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 17 Apr 2026 02:52:45 +0000 Subject: [PATCH 03/12] refactor(errors): defer mutation errors to global handler MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Now that MutationCache toasts via toUserMessage by default, the per-hook onError duplicates drop away. Also normalizes inline query-error UI and surfaces a previously-silent failure. Mutations — removed redundant onError toasts: - network-selector: useAcceptInvitation, createNetwork (inline) - network-settings: useInviteMembers, useRevokeInvitation, useRemoveMember - network-billing: useCreateCheckoutSession, useCreatePortalSession (onSuccess toasts stay — they carry domain context like network name) Queries — consistent inline error UX via toUserMessage: - network-selector: failed-to-load state gets a "Try again" button - network-billing: "Couldn't load billing" includes friendly reason - network-settings: useNetworkInvitations failure now surfaces a hint (previously rendered as "0 pending" — silently wrong) Trimmed noisy JSDoc from PR 1 files (errors.ts, query-client.ts, app-error-boundary.tsx, error-fallback.tsx, ipc-utils.ts). --- js/src/components/app-error-boundary.tsx | 8 ++------ js/src/components/error-fallback.tsx | 9 +-------- js/src/features/network-billing.tsx | 7 ++----- js/src/features/network-selector.tsx | 16 +++++++-------- js/src/features/network-settings.tsx | 19 +++++++++--------- js/src/lib/errors.ts | 25 ++++-------------------- js/src/lib/query-client.ts | 10 ++-------- js/src/main/ipc-utils.ts | 5 ++--- 8 files changed, 29 insertions(+), 70 deletions(-) diff --git a/js/src/components/app-error-boundary.tsx b/js/src/components/app-error-boundary.tsx index 7f8a0b6..1265a01 100644 --- a/js/src/components/app-error-boundary.tsx +++ b/js/src/components/app-error-boundary.tsx @@ -8,10 +8,7 @@ import { TopLevelErrorFallback, } from "@/components/error-fallback"; -/** - * Catches render crashes OUTSIDE react-router. Used once at the root so a - * broken bootstrap still shows a recovery UI instead of a white screen. - */ +/** Catches render crashes OUTSIDE the router so bootstrap failures still recover. */ export function TopLevelErrorBoundary({ children }: PropsWithChildren) { return ( { createCheckout.mutate(cadence, { onSuccess: ({ url }) => window.electronLink.openExternal(url), - onError: (err) => toast.error(err.message || "Failed to start checkout"), }); }; @@ -230,8 +229,6 @@ function ProBilling({ const handleManage = () => { createPortal.mutate(undefined, { onSuccess: ({ url }) => window.electronLink.openExternal(url), - onError: (err) => - toast.error(err.message || "Failed to open billing portal"), }); }; @@ -342,7 +339,7 @@ function AdminBillingControls({ networkId }: { networkId: string }) { if (error) { return (
- Failed to load billing. + Couldn't load billing: {toUserMessage(error)}
); } diff --git a/js/src/features/network-selector.tsx b/js/src/features/network-selector.tsx index 01e1b86..381dfb6 100644 --- a/js/src/features/network-selector.tsx +++ b/js/src/features/network-selector.tsx @@ -22,6 +22,7 @@ import { useNetworks } from "@/hooks/use-networks"; import { useMyInvitations, useAcceptInvitation } from "@/hooks/use-member-management"; import { apiClient } from "@/api/client"; import { Progress } from "@/components/ui/progress"; +import { toUserMessage } from "@/lib/errors"; import type { Network, Invitation } from "@/api/types"; function NetworkRow({ @@ -86,9 +87,6 @@ function InvitationRow({ invitation }: { invitation: Invitation }) { onSuccess: () => { toast.success(`Joined ${invitation.network_name}`); }, - onError: (err) => { - toast.error(err.message || "Failed to accept invitation"); - }, }); }; @@ -138,9 +136,6 @@ function CreateNetworkDialog({ setName(""); navigate(`/${network.id}/settings`); }, - onError: (err) => { - toast.error(err.message || "Failed to create network"); - }, }); const handleSubmit = (e: React.FormEvent) => { @@ -200,9 +195,12 @@ export default function NetworkSelector() { if (error) { return ( -
-

Failed to load networks

-

{error.message}

+
+

Couldn't load your networks

+

{toUserMessage(error)}

+
); } diff --git a/js/src/features/network-settings.tsx b/js/src/features/network-settings.tsx index 81edf50..4a4ba62 100644 --- a/js/src/features/network-settings.tsx +++ b/js/src/features/network-settings.tsx @@ -79,9 +79,6 @@ function InviteForm({ networkId }: { networkId: string }) { toast.success(`Invitation sent to ${trimmed}`); setEmail(""); }, - onError: (err) => { - toast.error(err.message || "Failed to send invitation"); - }, }); }; @@ -119,9 +116,6 @@ function PendingInvitationRow({ onSuccess: () => { toast.success(`Invitation to ${email} revoked`); }, - onError: (err) => { - toast.error(err.message || "Failed to revoke invitation"); - }, }); }; @@ -188,7 +182,7 @@ export default function NetworkSettingsPage() { const [searchParams] = useSearchParams(); const { data: networks } = useNetworks(); const network = networks?.find((n) => n.id === networkId); - const { data: invitations } = useNetworkInvitations(networkId!); + const { data: invitations, error: invitationsError } = useNetworkInvitations(networkId!); const currentUser = useAuthStore((s) => s.user); const isAdmin = currentUser?.id === network?.admin_human.id; const [memberToRemove, setMemberToRemove] = useState(null); @@ -286,6 +280,14 @@ export default function NetworkSettingsPage() { /> + {invitationsError && ( + <> + +

+ Couldn't load pending invitations. +

+ + )} {pendingCount > 0 && ( <> @@ -354,9 +356,6 @@ export default function NetworkSettingsPage() { toast.success(`Removed ${target.email}`); setMemberToRemove(null); }, - onError: (err) => { - toast.error(err.message || "Failed to remove member"); - }, }); }} onClose={() => { diff --git a/js/src/lib/errors.ts b/js/src/lib/errors.ts index 87883e9..459af06 100644 --- a/js/src/lib/errors.ts +++ b/js/src/lib/errors.ts @@ -1,9 +1,6 @@ import { z } from "zod"; import { appEnv } from "@/config/env"; -/** - * Thrown by the API client for any non-2xx response. - */ export class ApiError extends Error { constructor( public status: number, @@ -16,9 +13,8 @@ export class ApiError extends Error { /** * Thrown when a free-plan network attempts to create a non-container particle - * after hitting its daily message limit. Callers should surface an upgrade - * prompt; compose UI should also disable triggers proactively via - * `useNetworkUsage` rather than relying on this throw. + * after hitting its daily message limit. Compose UI also disables triggers + * proactively via `useNetworkUsage` — this throw is a last-line defense. */ export class QuotaExceededError extends Error { constructor(public readonly networkId: string) { @@ -33,11 +29,6 @@ function normalizeMessage(message: string): string { return message.replace(IPC_PREFIX, "").replace(/^Error:\s*/, "").trim(); } -/** - * Maps any thrown value to a short, user-facing string. Used by the global - * MutationCache toast handler, the error-boundary fallback, and any call site - * that wants to surface an error in the UI. - */ export function toUserMessage(err: unknown): string { if (err instanceof ApiError) { if (err.status === 401) return "Please sign in again."; @@ -69,11 +60,7 @@ export function toUserMessage(err: unknown): string { type ErrorContext = Record; -/** - * For expected-but-worth-recording failures (silent-catch sites). In dev, - * prints to console. In prod today this is a no-op; PR 4 wires this up to - * Sentry breadcrumbs without touching call sites. - */ +/** Expected-but-recordable failures. Dev logs; prod is currently a no-op. */ export function logError(err: unknown, context?: ErrorContext): void { if (appEnv === "dev") { // eslint-disable-next-line no-console @@ -81,11 +68,7 @@ export function logError(err: unknown, context?: ErrorContext): void { } } -/** - * For unexpected failures the user may not have seen (render crashes, query - * errors, background mutations). Always surfaces somewhere. PR 4 wires this - * up to `Sentry.captureException`. - */ +/** Unexpected failures the user may not see. Always surfaces somewhere. */ export function reportError(err: unknown, context?: ErrorContext): void { // eslint-disable-next-line no-console console.error("[error]", err, context ?? {}); diff --git a/js/src/lib/query-client.ts b/js/src/lib/query-client.ts index e49addc..e5e5ac1 100644 --- a/js/src/lib/query-client.ts +++ b/js/src/lib/query-client.ts @@ -8,14 +8,8 @@ import { ApiError, logError, reportError, toUserMessage } from "@/lib/errors"; declare module "@tanstack/react-query" { interface Register { - queryMeta: { - /** Show a sonner error toast automatically on query failure. */ - toastOnError?: boolean; - }; - mutationMeta: { - /** Opt out of the default sonner error toast (caller handles feedback). */ - suppressToast?: boolean; - }; + queryMeta: { toastOnError?: boolean }; + mutationMeta: { suppressToast?: boolean }; } } diff --git a/js/src/main/ipc-utils.ts b/js/src/main/ipc-utils.ts index de53265..901aecc 100644 --- a/js/src/main/ipc-utils.ts +++ b/js/src/main/ipc-utils.ts @@ -6,9 +6,8 @@ type InvokeHandler = ( ) => Promise | unknown; /** - * Wraps `ipcMain.handle` so handler failures log the full stack in the main - * process and re-throw a sanitized message to the renderer. Without this, - * main-process exceptions silently reject the renderer promise with no trace. + * Wraps `ipcMain.handle` so handler failures log with full stack in main and + * re-throw a sanitized message to the renderer. */ export function safeHandle(channel: string, handler: InvokeHandler): void { ipcMain.handle(channel, async (event, ...args) => { -- 2.54.0 From 795bdfc2865c15bd0ef07a3035b648f3eac6e3cc Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 17 Apr 2026 02:55:23 +0000 Subject: [PATCH 04/12] refactor(errors): route silent catches through logError/reportError MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every catch now either surfaces, re-throws, or calls logError with a scope tag. No more empty catches or bare console.error: - auth-store: signInToFirebase / restoreSession / signOut paths gain logError context. Behavior is unchanged (best-effort local sign-out, fall back to login on restore failure). - use-stream-autoplay: Audio.play() and download-URL fetches log their failures instead of dropping silently (both are nice-to-haves so UX stays silent — but we can now trace "why didn't autoplay trigger?"). - pusher-client: ws errors / parse failures / server errors / listener crashes all routed through logError, and listener bugs (which silently break user flows) now go through reportError so they're actually surfaced in observability. - settings-page: email-notifications toggle now toasts on failure instead of silently reverting with no explanation. - huddle-app: screen-share failures use logError. --- js/src/features/settings-page.tsx | 7 +++++-- js/src/hooks/use-stream-autoplay.ts | 13 +++++++++---- js/src/huddle_window/HuddleApp.tsx | 3 ++- js/src/lib/pusher-client.ts | 17 +++++++++++------ js/src/stores/auth-store.ts | 20 +++++++++++++------- 5 files changed, 40 insertions(+), 20 deletions(-) diff --git a/js/src/features/settings-page.tsx b/js/src/features/settings-page.tsx index 6d9a706..d6988a9 100644 --- a/js/src/features/settings-page.tsx +++ b/js/src/features/settings-page.tsx @@ -11,6 +11,8 @@ import { ScrollArea } from "@/components/ui/scroll-area"; import { CopyableEmail } from "@/components/copyable-email"; import { useAuthStore } from "@/stores/auth-store"; import { apiClient } from "@/api/client"; +import { logError, toUserMessage } from "@/lib/errors"; +import { toast } from "sonner"; import { PRIVACY_URL, SUPPORT_EMAIL, TERMS_URL } from "@/lib/constants"; import { ArrowLeft } from "lucide-react"; @@ -84,12 +86,13 @@ export default function SettingsPage() { })); try { await apiClient.updateSettings({ email_notifications_enabled: checked }); - } catch { - // Revert on failure + } catch (err) { setEmailNotifications(!checked); useAuthStore.setState((state) => ({ user: state.user ? { ...state.user, email_notifications_enabled: !checked } : null, })); + toast.error(toUserMessage(err)); + logError(err, { scope: "settings.emailNotifications" }); } }; diff --git a/js/src/hooks/use-stream-autoplay.ts b/js/src/hooks/use-stream-autoplay.ts index 16f8235..05abeee 100644 --- a/js/src/hooks/use-stream-autoplay.ts +++ b/js/src/hooks/use-stream-autoplay.ts @@ -5,6 +5,7 @@ import { apiClient } from "@/api/client"; import { useAuthStore } from "@/stores/auth-store"; import { useAutoplayStore } from "@/stores/autoplay-store"; import { resolveHumanDisplay } from "@/lib/humans"; +import { logError } from "@/lib/errors"; /** * Triggers autoplay when a stream's latest child changes to a new media particle. @@ -37,7 +38,11 @@ export function useStreamAutoplay( if (useAutoplayStore.getState().muted) return; if (latestChild.type === "text") { - new Audio(beepSound).play().catch(() => {}); + // Browser autoplay policy can block this before user interaction; that's + // fine — the beep is a nice-to-have, not a critical signal. + new Audio(beepSound).play().catch((err) => + logError(err, { scope: "autoplay.beep" }), + ); return; } @@ -60,8 +65,8 @@ export function useStreamAutoplay( senderName: displayName, senderInitials: initials, }); - }).catch(() => { - // Failed to get download URL — skip autoplay silently - }); + }).catch((err) => + logError(err, { scope: "autoplay.fetchUrl", particleId: particle.id }), + ); }, [latestChild?.id]); // eslint-disable-line react-hooks/exhaustive-deps } diff --git a/js/src/huddle_window/HuddleApp.tsx b/js/src/huddle_window/HuddleApp.tsx index 6ce85ff..3f85480 100644 --- a/js/src/huddle_window/HuddleApp.tsx +++ b/js/src/huddle_window/HuddleApp.tsx @@ -23,6 +23,7 @@ import { import { RoomEvent, Track } from 'livekit-client'; import { useState, useEffect, useRef, useCallback } from 'react'; import { ScreenSourcePicker } from '@/components/screen-source-picker'; +import { logError } from '@/lib/errors'; function readConnectionFromHash(): { token: string; serverUrl: string } | null { const params = new URLSearchParams(window.location.hash.slice(1)); @@ -144,7 +145,7 @@ function HuddleContent() { setIsSharing(true); videoTrack.onended = () => stopScreenShare(); } catch (err) { - console.error('Failed to start screen share:', err); + logError(err, { scope: "huddle.screenShare" }); } }, [room], diff --git a/js/src/lib/pusher-client.ts b/js/src/lib/pusher-client.ts index 1ec4e7b..631e182 100644 --- a/js/src/lib/pusher-client.ts +++ b/js/src/lib/pusher-client.ts @@ -4,6 +4,8 @@ * channel subscriptions, and event dispatching. */ +import { logError, reportError } from "@/lib/errors"; + export type ConnectionState = | "disconnected" | "connecting" @@ -100,8 +102,8 @@ export class PusherClient { }; this.ws.onerror = (event) => { - console.warn("[pusher] websocket error", event); - // onclose will fire after onerror, so reconnection is handled there + // onclose fires after onerror — reconnection is handled there. + logError(event, { scope: "pusher.ws" }); }; this.ws.onmessage = (event) => { @@ -182,13 +184,15 @@ export class PusherClient { let msg: ServerMessage; try { msg = JSON.parse(data); - } catch { - console.warn("[pusher] failed to parse message", data); + } catch (err) { + logError(err, { scope: "pusher.parse", data }); return; } if (msg.type === "error") { - console.warn("[pusher] server error:", msg.message); + logError(new Error(msg.message ?? "pusher server error"), { + scope: "pusher.server", + }); return; } @@ -204,7 +208,8 @@ export class PusherClient { try { cb(msg); } catch (err) { - console.error("[pusher] listener error", err); + // Listener bugs silently break user flows — escalate to reportError. + reportError(err, { scope: "pusher.listener", channel: msg.channel }); } } } diff --git a/js/src/stores/auth-store.ts b/js/src/stores/auth-store.ts index 426cb20..427157d 100644 --- a/js/src/stores/auth-store.ts +++ b/js/src/stores/auth-store.ts @@ -3,14 +3,17 @@ import { signInWithCustomToken, signOut as firebaseSignOut } from "firebase/auth import { apiClient, ApiError } from "@/api/client"; import type { Human } from "@/api/types"; import { firebaseAuth } from "@/firebase"; +import { logError } from "@/lib/errors"; import { useSessionStore } from "./session-store"; async function signInToFirebase() { try { const { token } = await apiClient.getFirebaseToken(); await signInWithCustomToken(firebaseAuth, token); - } catch (e) { - console.error("Failed to sign in to Firebase", e); + } catch (err) { + // Firestore subscriptions will fail until the next successful sign-in; the + // rest of the app keeps working against Orion. + logError(err, { scope: "auth.firebase" }); } } @@ -50,7 +53,9 @@ export const useAuthStore = create((set) => ({ const user = await apiClient.me(); await signInToFirebase(); set({ status: "authenticated", user }); - } catch { + } catch (err) { + // Expected on expired/invalid tokens — fall back to the login screen. + logError(err, { scope: "auth.restore" }); useSessionStore.getState().clearToken(); set({ status: "unauthenticated", user: null }); } @@ -91,11 +96,12 @@ export const useAuthStore = create((set) => ({ set({ isSigningOut: true }); try { await apiClient.signOut(); - } catch { - // Best-effort — sign out locally regardless + } catch (err) { + // Best-effort — sign out locally regardless. + logError(err, { scope: "auth.signOut" }); } finally { - await firebaseSignOut(firebaseAuth).catch((e) => - console.error("Firebase sign-out failed", e), + await firebaseSignOut(firebaseAuth).catch((err) => + logError(err, { scope: "auth.firebaseSignOut" }), ); useSessionStore.getState().clearToken(); set({ -- 2.54.0 From b0281ae8f9667b2b3a4fd5c3c90cf9fe7153a084 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 17 Apr 2026 03:01:02 +0000 Subject: [PATCH 05/12] feat(errors): add Sentry observability behind a sinks facade MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit logError / reportError now route through a pair of sinks installed at bootstrap by each process (main + every renderer entry). No call site knows about Sentry — if sentryDsn is empty, the sinks simply aren't installed and logError/reportError stay console-only. - appConfig.sentryDsn: per-env string (empty for now — populate when ops creates the DSNs). Empty is the no-op mode for dev. - lib/errors.ts: installErrorSinks({ capture, breadcrumb }) gates Sentry.captureException / Sentry.addBreadcrumb. Everything flows through toUserMessage and the two existing call types. - lib/sentry.ts: initSentryRenderer() for the main window + autoplay, huddle, and screen-record renderers. - main/sentry.ts: initSentryMain() runs before anything else in main.ts to catch bootstrap failures. Captures uncaught exceptions and the crash reporter automatically. - main/ipc-utils.ts::safeHandle now routes through reportError. - main.ts::fetchLinkMetadata logs via logError. --- js/package.json | 1 + js/src/autoplay_window/renderer.tsx | 3 + js/src/config/env.ts | 4 + js/src/huddle_window/renderer.tsx | 3 + js/src/lib/errors.ts | 21 +- js/src/lib/sentry.ts | 28 ++ js/src/main.ts | 7 +- js/src/main/ipc-utils.ts | 4 +- js/src/main/sentry.ts | 30 ++ js/src/renderer.tsx | 3 + js/src/screen_record_window/renderer.tsx | 3 + js/yarn.lock | 577 ++++++++++++++++++++++- 12 files changed, 677 insertions(+), 7 deletions(-) create mode 100644 js/src/lib/sentry.ts create mode 100644 js/src/main/sentry.ts diff --git a/js/package.json b/js/package.json index a75b43a..4924793 100644 --- a/js/package.json +++ b/js/package.json @@ -53,6 +53,7 @@ "dependencies": { "@livekit/components-react": "^2.9.20", "@livekit/components-styles": "^1.2.0", + "@sentry/electron": "^7.11.0", "@tanstack/react-query": "^5.90.21", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", diff --git a/js/src/autoplay_window/renderer.tsx b/js/src/autoplay_window/renderer.tsx index 263518d..f9493f7 100644 --- a/js/src/autoplay_window/renderer.tsx +++ b/js/src/autoplay_window/renderer.tsx @@ -1,6 +1,9 @@ import { createRoot } from 'react-dom/client'; import { AutoplayApp } from './AutoplayApp'; +import { initSentryRenderer } from '@/lib/sentry'; import '@/styles/globals.css'; +initSentryRenderer(); + const root = createRoot(document.getElementById('root')!); root.render(); diff --git a/js/src/config/env.ts b/js/src/config/env.ts index c80a415..ce5bd42 100644 --- a/js/src/config/env.ts +++ b/js/src/config/env.ts @@ -23,6 +23,8 @@ type AppConfig = { orionUrl: string; pusherUrl: string; firebase: FirebaseConfig; + /** Empty string disables Sentry. Populate per-env when ready to ship. */ + sentryDsn: string; }; const configs: Record<"dev" | "prod", AppConfig> = { @@ -37,6 +39,7 @@ const configs: Record<"dev" | "prod", AppConfig> = { projectId: "flowy-dev-440017", storageBucket: "flowy-dev-440017.firebasestorage.app", }, + sentryDsn: "", }, prod: { orionUrl: "https://orion.flowy.live", @@ -49,6 +52,7 @@ const configs: Record<"dev" | "prod", AppConfig> = { projectId: "flowy-prod-440017", storageBucket: "flowy-prod-440017.firebasestorage.app", }, + sentryDsn: "", }, }; diff --git a/js/src/huddle_window/renderer.tsx b/js/src/huddle_window/renderer.tsx index 771a288..2d16877 100644 --- a/js/src/huddle_window/renderer.tsx +++ b/js/src/huddle_window/renderer.tsx @@ -1,6 +1,9 @@ import { createRoot } from 'react-dom/client'; import { HuddleApp } from './HuddleApp'; +import { initSentryRenderer } from '@/lib/sentry'; import '@/styles/globals.css'; +initSentryRenderer(); + const root = createRoot(document.getElementById('root')!); root.render(); diff --git a/js/src/lib/errors.ts b/js/src/lib/errors.ts index 459af06..25d3d79 100644 --- a/js/src/lib/errors.ts +++ b/js/src/lib/errors.ts @@ -59,17 +59,34 @@ export function toUserMessage(err: unknown): string { } type ErrorContext = Record; +type ErrorSink = (err: unknown, context?: ErrorContext) => void; -/** Expected-but-recordable failures. Dev logs; prod is currently a no-op. */ +// Sentry (or any observability backend) installs itself via `installErrorSinks` +// from renderer.tsx / main.ts. Until then, logError is a dev-only console call +// and reportError always prints — no call site needs to know. +let captureSink: ErrorSink | null = null; +let breadcrumbSink: ErrorSink | null = null; + +export function installErrorSinks(sinks: { + capture: ErrorSink; + breadcrumb: ErrorSink; +}): void { + captureSink = sinks.capture; + breadcrumbSink = sinks.breadcrumb; +} + +/** Expected-but-recordable failures. Breadcrumb only — never pages anyone. */ export function logError(err: unknown, context?: ErrorContext): void { if (appEnv === "dev") { // eslint-disable-next-line no-console console.error("[error]", err, context ?? {}); } + breadcrumbSink?.(err, context); } -/** Unexpected failures the user may not see. Always surfaces somewhere. */ +/** Unexpected failures the user may not see. Always captured. */ export function reportError(err: unknown, context?: ErrorContext): void { // eslint-disable-next-line no-console console.error("[error]", err, context ?? {}); + captureSink?.(err, context); } diff --git a/js/src/lib/sentry.ts b/js/src/lib/sentry.ts new file mode 100644 index 0000000..284a35c --- /dev/null +++ b/js/src/lib/sentry.ts @@ -0,0 +1,28 @@ +import * as Sentry from "@sentry/electron/renderer"; +import { appConfig } from "@/config/env"; +import { installErrorSinks } from "@/lib/errors"; + +/** + * Initialise Sentry for a renderer process. No-ops when `sentryDsn` is empty + * so dev builds and unconfigured envs stay quiet. + */ +export function initSentryRenderer(): void { + if (!appConfig.sentryDsn) return; + + Sentry.init({ + dsn: appConfig.sentryDsn, + tracesSampleRate: 0, + }); + + installErrorSinks({ + capture: (err, context) => + Sentry.captureException(err, { extra: context }), + breadcrumb: (err, context) => + Sentry.addBreadcrumb({ + category: "error", + level: "error", + message: err instanceof Error ? err.message : String(err), + data: context, + }), + }); +} diff --git a/js/src/main.ts b/js/src/main.ts index 16fca8a..a8784a9 100644 --- a/js/src/main.ts +++ b/js/src/main.ts @@ -5,7 +5,11 @@ import { updateElectronApp, UpdateSourceType } from 'update-electron-app'; import type { LinkMetadata } from './lib/link-metadata'; import { appConfig } from './config/env'; +import { logError } from './lib/errors'; import { safeHandle } from './main/ipc-utils'; +import { initSentryMain } from './main/sentry'; + +initSentryMain(); if (app.isPackaged) { updateElectronApp({ @@ -461,8 +465,7 @@ async function fetchLinkMetadata(url: string): Promise { } catch (err) { // Metadata is a progressive enhancement — keep the null contract, but log // so upstream failures (DNS, TLS, aborted fetches) aren't invisible. - // eslint-disable-next-line no-console - console.error('[ipc:link.fetch-metadata]', err); + logError(err, { scope: 'link.fetchMetadata', url }); return null; } } diff --git a/js/src/main/ipc-utils.ts b/js/src/main/ipc-utils.ts index 901aecc..94b4b8a 100644 --- a/js/src/main/ipc-utils.ts +++ b/js/src/main/ipc-utils.ts @@ -1,4 +1,5 @@ import { ipcMain, type IpcMainInvokeEvent } from "electron"; +import { reportError } from "../lib/errors"; type InvokeHandler = ( event: IpcMainInvokeEvent, @@ -14,8 +15,7 @@ export function safeHandle(channel: string, handler: InvokeHandler): void { try { return await handler(event, ...(args as unknown[])); } catch (err) { - // eslint-disable-next-line no-console - console.error(`[ipc:${channel}]`, err); + reportError(err, { scope: `ipc.${channel}` }); const message = err instanceof Error ? err.message : "IPC error"; throw new Error(message); } diff --git a/js/src/main/sentry.ts b/js/src/main/sentry.ts new file mode 100644 index 0000000..213e966 --- /dev/null +++ b/js/src/main/sentry.ts @@ -0,0 +1,30 @@ +import { app } from "electron"; +import * as Sentry from "@sentry/electron/main"; +import { appConfig } from "../config/env"; +import { installErrorSinks } from "../lib/errors"; + +/** + * Initialise Sentry for the main process. Captures uncaught exceptions from + * the Node side and the crash reporter. Safe to call before `app.whenReady`. + */ +export function initSentryMain(): void { + if (!appConfig.sentryDsn) return; + + Sentry.init({ + dsn: appConfig.sentryDsn, + tracesSampleRate: 0, + release: app.getVersion(), + }); + + installErrorSinks({ + capture: (err, context) => + Sentry.captureException(err, { extra: context }), + breadcrumb: (err, context) => + Sentry.addBreadcrumb({ + category: "error", + level: "error", + message: err instanceof Error ? err.message : String(err), + data: context, + }), + }); +} diff --git a/js/src/renderer.tsx b/js/src/renderer.tsx index 20dfe8c..0bbc099 100644 --- a/js/src/renderer.tsx +++ b/js/src/renderer.tsx @@ -1,6 +1,9 @@ import { createRoot } from 'react-dom/client'; import App from './App'; +import { initSentryRenderer } from '@/lib/sentry'; import './styles/globals.css'; +initSentryRenderer(); + const root = createRoot(document.getElementById('root')!); root.render(); diff --git a/js/src/screen_record_window/renderer.tsx b/js/src/screen_record_window/renderer.tsx index 69357e2..7ef4c8e 100644 --- a/js/src/screen_record_window/renderer.tsx +++ b/js/src/screen_record_window/renderer.tsx @@ -1,6 +1,9 @@ import { createRoot } from 'react-dom/client'; import { ScreenRecordControlApp } from './ScreenRecordControlApp'; +import { initSentryRenderer } from '@/lib/sentry'; import '@/styles/globals.css'; +initSentryRenderer(); + const root = createRoot(document.getElementById('root')!); root.render(); diff --git a/js/yarn.lock b/js/yarn.lock index ca86d6c..f67f16a 100644 --- a/js/yarn.lock +++ b/js/yarn.lock @@ -881,6 +881,16 @@ resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.57.1.tgz#de633db3ec2ef6a3c89e2f19038063e8a122e2c2" integrity sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q== +"@fastify/otel@0.18.0": + version "0.18.0" + resolved "https://registry.yarnpkg.com/@fastify/otel/-/otel-0.18.0.tgz#d21814af7c97579856698e03aae0581beb3e734b" + integrity sha512-3TASCATfw+ctICSb4ymrv7iCm0qJ0N9CarB+CZ7zIJ7KqNbwI5JjyDL1/sxoC0ccTO1Zyd1iQ+oqncPg5FJXaA== + dependencies: + "@opentelemetry/core" "^2.0.0" + "@opentelemetry/instrumentation" "^0.212.0" + "@opentelemetry/semantic-conventions" "^1.28.0" + minimatch "^10.2.4" + "@firebase/ai@2.9.0": version "2.9.0" resolved "https://registry.yarnpkg.com/@firebase/ai/-/ai-2.9.0.tgz#9e6f3546eb688e31488f3e081702773300d609f1" @@ -1798,6 +1808,303 @@ resolved "https://registry.yarnpkg.com/@open-draft/until/-/until-2.1.0.tgz#0acf32f470af2ceaf47f095cdecd40d68666efda" integrity sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg== +"@opentelemetry/api-logs@0.207.0": + version "0.207.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/api-logs/-/api-logs-0.207.0.tgz#ae991c51eedda55af037a3e6fc1ebdb12b289f49" + integrity sha512-lAb0jQRVyleQQGiuuvCOTDVspc14nx6XJjP4FspJ1sNARo3Regq4ZZbrc3rN4b1TYSuUCvgH+UXUPug4SLOqEQ== + dependencies: + "@opentelemetry/api" "^1.3.0" + +"@opentelemetry/api-logs@0.212.0": + version "0.212.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/api-logs/-/api-logs-0.212.0.tgz#ec66a0951b84b1f082e13fd8a027b9f9d65a3f7a" + integrity sha512-TEEVrLbNROUkYY51sBJGk7lO/OLjuepch8+hmpM6ffMJQ2z/KVCjdHuCFX6fJj8OkJP2zckPjrJzQtXU3IAsFg== + dependencies: + "@opentelemetry/api" "^1.3.0" + +"@opentelemetry/api-logs@0.214.0": + version "0.214.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/api-logs/-/api-logs-0.214.0.tgz#74a54ad7b166c6fa30a0df811954c0f5a435deee" + integrity sha512-40lSJeqYO8Uz2Yj7u94/SJWE/wONa7rmMKjI1ZcIjgf3MHNHv1OZUCrCETGuaRF62d5pQD1wKIW+L4lmSMTzZA== + dependencies: + "@opentelemetry/api" "^1.3.0" + +"@opentelemetry/api@^1.3.0", "@opentelemetry/api@^1.9.1": + version "1.9.1" + resolved "https://registry.yarnpkg.com/@opentelemetry/api/-/api-1.9.1.tgz#c1b0346de336ba55af2d5a7970882037baedec05" + integrity sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q== + +"@opentelemetry/context-async-hooks@^2.6.1": + version "2.6.1" + resolved "https://registry.yarnpkg.com/@opentelemetry/context-async-hooks/-/context-async-hooks-2.6.1.tgz#06e60d5b3fba992a832af7f034758574e951bba3" + integrity sha512-XHzhwRNkBpeP8Fs/qjGrAf9r9PRv67wkJQ/7ZPaBQQ68DYlTBBx5MF9LvPx7mhuXcDessKK2b+DcxqwpgkcivQ== + +"@opentelemetry/core@2.6.1", "@opentelemetry/core@^2.0.0", "@opentelemetry/core@^2.6.1": + version "2.6.1" + resolved "https://registry.yarnpkg.com/@opentelemetry/core/-/core-2.6.1.tgz#a59d22a9ae3be80bb41b280bbbe1fe9fbdb6c2a5" + integrity sha512-8xHSGWpJP9wBxgBpnqGL0R3PbdWQndL1Qp50qrg71+B28zK5OQmUgcDKLJgzyAAV38t4tOyLMGDD60LneR5W8g== + dependencies: + "@opentelemetry/semantic-conventions" "^1.29.0" + +"@opentelemetry/instrumentation-amqplib@0.61.0": + version "0.61.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-amqplib/-/instrumentation-amqplib-0.61.0.tgz#e9d52f56dfc4cb8a26837f31c1832af18859f1f2" + integrity sha512-mCKoyTGfRNisge4br0NpOFSy2Z1NnEW8hbCJdUDdJFHrPqVzc4IIBPA/vX0U+LUcQqrQvJX+HMIU0dbDRe0i0Q== + dependencies: + "@opentelemetry/core" "^2.0.0" + "@opentelemetry/instrumentation" "^0.214.0" + "@opentelemetry/semantic-conventions" "^1.33.0" + +"@opentelemetry/instrumentation-connect@0.57.0": + version "0.57.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-connect/-/instrumentation-connect-0.57.0.tgz#66b58af135ef6d52ad546cb440b808a149118296" + integrity sha512-FMEBChnI4FLN5TE9DHwfH7QpNir1JzXno1uz/TAucVdLCyrG0jTrKIcNHt/i30A0M2AunNBCkcd8Ei26dIPKdg== + dependencies: + "@opentelemetry/core" "^2.0.0" + "@opentelemetry/instrumentation" "^0.214.0" + "@opentelemetry/semantic-conventions" "^1.27.0" + "@types/connect" "3.4.38" + +"@opentelemetry/instrumentation-dataloader@0.31.0": + version "0.31.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-dataloader/-/instrumentation-dataloader-0.31.0.tgz#43bfbe09f99e84eb0d8b6e9f914c2e51a45e6d95" + integrity sha512-f654tZFQXS5YeLDNb9KySrwtg7SnqZN119FauD7acBoTzuLduaiGTNz88ixcVSOOMGZ+EjJu/RFtx5klObC95g== + dependencies: + "@opentelemetry/instrumentation" "^0.214.0" + +"@opentelemetry/instrumentation-express@0.62.0": + version "0.62.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-express/-/instrumentation-express-0.62.0.tgz#c03e353caf04b7074004ce899faf759dec210b8d" + integrity sha512-Tvx+vgAZKEQxU3Rx+xWLiR0mLxHwmk69/8ya04+VsV9WYh8w6Lhx5hm5yAMvo1wy0KqWgFKBLwSeo3sHCwdOww== + dependencies: + "@opentelemetry/core" "^2.0.0" + "@opentelemetry/instrumentation" "^0.214.0" + "@opentelemetry/semantic-conventions" "^1.27.0" + +"@opentelemetry/instrumentation-fs@0.33.0": + version "0.33.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-fs/-/instrumentation-fs-0.33.0.tgz#75f2ccf653b772801b398cc2ad0974e8785f2e3d" + integrity sha512-sCZWXGalQ01wr3tAhSR9ucqFJ0phidpAle6/17HVjD6gN8FLmZMK/8sKxdXYHy3PbnlV1P4zeiSVFNKpbFMNLA== + dependencies: + "@opentelemetry/core" "^2.0.0" + "@opentelemetry/instrumentation" "^0.214.0" + +"@opentelemetry/instrumentation-generic-pool@0.57.0": + version "0.57.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-generic-pool/-/instrumentation-generic-pool-0.57.0.tgz#4220a2fc1974b40a989171a9b5f3d1eeab92683f" + integrity sha512-orhmlaK+ZIW9hKU+nHTbXrCSXZcH83AescTqmpamHRobRmYSQwRbD0a1odc0yAzuzOtxYiHiXAnpnIpaSSY7Ow== + dependencies: + "@opentelemetry/instrumentation" "^0.214.0" + +"@opentelemetry/instrumentation-graphql@0.62.0": + version "0.62.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-graphql/-/instrumentation-graphql-0.62.0.tgz#dc2fc92c6be331c4f95b62a40983c8aedb8f9bf9" + integrity sha512-3YNuLVPUxafXkH1jBAbGsKNsP3XVzcFDhCDCE3OqBwCwShlqQbLMRMFh1T/d5jaVZiGVmSsfof+ICKD2iOV8xg== + dependencies: + "@opentelemetry/instrumentation" "^0.214.0" + +"@opentelemetry/instrumentation-hapi@0.60.0": + version "0.60.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-hapi/-/instrumentation-hapi-0.60.0.tgz#ad1ba65a32347351c310ac0f194fe66b8e9d9e7d" + integrity sha512-aNljZKYrEa7obLAxd1bCEDxF7kzCLGXTuTJZ8lMR9rIVEjmuKBXN1gfqpm/OB//Zc2zP4iIve1jBp7sr3mQV6w== + dependencies: + "@opentelemetry/core" "^2.0.0" + "@opentelemetry/instrumentation" "^0.214.0" + "@opentelemetry/semantic-conventions" "^1.27.0" + +"@opentelemetry/instrumentation-http@0.214.0": + version "0.214.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-http/-/instrumentation-http-0.214.0.tgz#d4a31a638b798e191f4f556c257a4d3c97d65ba0" + integrity sha512-FlkDhZDRjDJDcO2LcSCtjRpkal1NJ8y0fBqBhTvfAR3JSYY2jAIj1kSS5IjmEBt4c3aWv+u/lqLuoCDrrKCSKg== + dependencies: + "@opentelemetry/core" "2.6.1" + "@opentelemetry/instrumentation" "0.214.0" + "@opentelemetry/semantic-conventions" "^1.29.0" + forwarded-parse "2.1.2" + +"@opentelemetry/instrumentation-ioredis@0.62.0": + version "0.62.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-ioredis/-/instrumentation-ioredis-0.62.0.tgz#4fd1775577132de5d92165caee6bbc0ae16a8c8a" + integrity sha512-ZYt//zcPve8qklaZX+5Z4MkU7UpEkFRrxsf2cnaKYBitqDnsCN69CPAuuMOX6NYdW2rG9sFy7V/QWtBlP5XiNQ== + dependencies: + "@opentelemetry/instrumentation" "^0.214.0" + "@opentelemetry/redis-common" "^0.38.2" + "@opentelemetry/semantic-conventions" "^1.33.0" + +"@opentelemetry/instrumentation-kafkajs@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-kafkajs/-/instrumentation-kafkajs-0.23.0.tgz#6b7d449d88d674ddc295a0d0cf2156f0f7d5889f" + integrity sha512-4K+nVo+zI+aDz0Z85SObwbdixIbzS9moIuKJaYsdlzcHYnKOPtB7ya8r8Ezivy/GVIBHiKJVq4tv+BEkgOMLaQ== + dependencies: + "@opentelemetry/instrumentation" "^0.214.0" + "@opentelemetry/semantic-conventions" "^1.30.0" + +"@opentelemetry/instrumentation-knex@0.58.0": + version "0.58.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-knex/-/instrumentation-knex-0.58.0.tgz#48878fe40bc48834d6b4c4148433c84524a2558a" + integrity sha512-Hc/o8fSsaWxZ8r1Yw4rNDLwTpUopTf4X32y4W6UhlHmW8Wizz8wfhgOKIelSeqFVTKBBPIDUOsQWuIMxBmu8Bw== + dependencies: + "@opentelemetry/instrumentation" "^0.214.0" + "@opentelemetry/semantic-conventions" "^1.33.1" + +"@opentelemetry/instrumentation-koa@0.62.0": + version "0.62.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-koa/-/instrumentation-koa-0.62.0.tgz#65fdf96c1b1ffb382167cd3b7a244631afd0cc1f" + integrity sha512-uVip0VuGUQXZ+vFxkKxAUNq8qNl+VFlyHDh/U6IQ8COOEDfbEchdaHnpFrMYF3psZRUuoSIgb7xOeXj00RdwDA== + dependencies: + "@opentelemetry/core" "^2.0.0" + "@opentelemetry/instrumentation" "^0.214.0" + "@opentelemetry/semantic-conventions" "^1.36.0" + +"@opentelemetry/instrumentation-lru-memoizer@0.58.0": + version "0.58.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-lru-memoizer/-/instrumentation-lru-memoizer-0.58.0.tgz#7c730a0cb963e8ac5f3d11023518050e5f124a6a" + integrity sha512-6grM3TdMyHzlGY1cUA+mwoPueB1F3dYKgKtZIH6jOFXqfHAByyLTc+6PFjGM9tKh52CFBJaDwodNlL/Td39z7Q== + dependencies: + "@opentelemetry/instrumentation" "^0.214.0" + +"@opentelemetry/instrumentation-mongodb@0.67.0": + version "0.67.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-mongodb/-/instrumentation-mongodb-0.67.0.tgz#ac45611586e363e2d96c735d50f97556dd33c37e" + integrity sha512-1WJp5N1lYfHq2IhECOTewFs5Tf2NfUOwQRqs/rZdXKTezArMlucxgzAaqcgp3A3YREXopXTpXHsxZTGHjNhMdQ== + dependencies: + "@opentelemetry/instrumentation" "^0.214.0" + "@opentelemetry/semantic-conventions" "^1.33.0" + +"@opentelemetry/instrumentation-mongoose@0.60.0": + version "0.60.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-mongoose/-/instrumentation-mongoose-0.60.0.tgz#9481a90d3f75d66244d7f63709529cb7f2823103" + integrity sha512-8BahAZpKsOoc+lrZGb7Ofn4g3z8qtp5IxDfvAVpKXsEheQN7ONMH5djT5ihy6yf8yyeQJGS0gXFfpEAEeEHqQg== + dependencies: + "@opentelemetry/core" "^2.0.0" + "@opentelemetry/instrumentation" "^0.214.0" + "@opentelemetry/semantic-conventions" "^1.33.0" + +"@opentelemetry/instrumentation-mysql2@0.60.0": + version "0.60.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-mysql2/-/instrumentation-mysql2-0.60.0.tgz#10eddc3f933a80f11e334ae31c67e9d1156373ca" + integrity sha512-m/5d3bxQALllCzezYDk/6vajh0tj5OijMMvOZGr+qN1NMXm1dzMNwyJ0gNZW7Fo3YFRyj/jJMxIw+W7d525dlw== + dependencies: + "@opentelemetry/instrumentation" "^0.214.0" + "@opentelemetry/semantic-conventions" "^1.33.0" + "@opentelemetry/sql-common" "^0.41.2" + +"@opentelemetry/instrumentation-mysql@0.60.0": + version "0.60.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-mysql/-/instrumentation-mysql-0.60.0.tgz#e8e13b60f8d8fe8d0f4941f200ae3e4a4e5e4a3c" + integrity sha512-08pO8GFPEIz2zquKDGteBZDNmwketdgH8hTe9rVYgW9kCJXq1Psj3wPQGx+VaX4ZJKCfPeoLMYup9+cxHvZyVQ== + dependencies: + "@opentelemetry/instrumentation" "^0.214.0" + "@opentelemetry/semantic-conventions" "^1.33.0" + "@types/mysql" "2.15.27" + +"@opentelemetry/instrumentation-pg@0.66.0": + version "0.66.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-pg/-/instrumentation-pg-0.66.0.tgz#78d16b50dc4c5d851015823611a46243d63a88fb" + integrity sha512-KxfLGXBb7k2ueaPJfq2GXBDXBly8P+SpR/4Mj410hhNgmQF3sCqwXvUBQxZQkDAmsdBAoenM+yV1LhtsMRamcA== + dependencies: + "@opentelemetry/core" "^2.0.0" + "@opentelemetry/instrumentation" "^0.214.0" + "@opentelemetry/semantic-conventions" "^1.34.0" + "@opentelemetry/sql-common" "^0.41.2" + "@types/pg" "8.15.6" + "@types/pg-pool" "2.0.7" + +"@opentelemetry/instrumentation-redis@0.62.0": + version "0.62.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-redis/-/instrumentation-redis-0.62.0.tgz#ecde90337fa49fec8d243bcbb8d470ce1a9ee7a1" + integrity sha512-y3pPpot7WzR/8JtHcYlTYsyY8g+pbFhAqbwAuG5bLPnR6v6pt1rQc0DpH0OlGP/9CZbWBP+Zhwp9yFoygf/ZXQ== + dependencies: + "@opentelemetry/instrumentation" "^0.214.0" + "@opentelemetry/redis-common" "^0.38.2" + "@opentelemetry/semantic-conventions" "^1.27.0" + +"@opentelemetry/instrumentation-tedious@0.33.0": + version "0.33.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-tedious/-/instrumentation-tedious-0.33.0.tgz#00f6698f8afae1b350bf0c463a59eeae3c8d25d7" + integrity sha512-Q6WQwAD01MMTub31GlejoiFACYNw26J426wyjvU7by7fDIr2nZXNW4vhTGs7i7F0TnXBO3xN688g1tdUgYwJ5w== + dependencies: + "@opentelemetry/instrumentation" "^0.214.0" + "@opentelemetry/semantic-conventions" "^1.33.0" + "@types/tedious" "^4.0.14" + +"@opentelemetry/instrumentation-undici@0.24.0": + version "0.24.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-undici/-/instrumentation-undici-0.24.0.tgz#6ad41245012742899294edf65aa79fd190369094" + integrity sha512-oKzZ3uvqP17sV0EsoQcJgjEfIp0kiZRbYu/eD8p13Cbahumf8lb/xpYeNr/hfAJ4owzEtIDcGIjprfLcYbIKBQ== + dependencies: + "@opentelemetry/core" "^2.0.0" + "@opentelemetry/instrumentation" "^0.214.0" + "@opentelemetry/semantic-conventions" "^1.24.0" + +"@opentelemetry/instrumentation@0.214.0", "@opentelemetry/instrumentation@^0.214.0": + version "0.214.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation/-/instrumentation-0.214.0.tgz#2649e8a29a8c4748bc583d35281c80632f046e25" + integrity sha512-MHqEX5Dk59cqVah5LiARMACku7jXSVk9iVDWOea4x3cr7VfdByeDCURK6o1lntT1JS/Tsovw01UJrBhN3/uC5w== + dependencies: + "@opentelemetry/api-logs" "0.214.0" + import-in-the-middle "^3.0.0" + require-in-the-middle "^8.0.0" + +"@opentelemetry/instrumentation@^0.207.0": + version "0.207.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation/-/instrumentation-0.207.0.tgz#1a5a921c04f171ff28096fa320af713f3c87ec14" + integrity sha512-y6eeli9+TLKnznrR8AZlQMSJT7wILpXH+6EYq5Vf/4Ao+huI7EedxQHwRgVUOMLFbe7VFDvHJrX9/f4lcwnJsA== + dependencies: + "@opentelemetry/api-logs" "0.207.0" + import-in-the-middle "^2.0.0" + require-in-the-middle "^8.0.0" + +"@opentelemetry/instrumentation@^0.212.0": + version "0.212.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation/-/instrumentation-0.212.0.tgz#238b6e3e2131217ff4acfe7e8e7b6ce1f0ac0ba0" + integrity sha512-IyXmpNnifNouMOe0I/gX7ENfv2ZCNdYTF0FpCsoBcpbIHzk81Ww9rQTYTnvghszCg7qGrIhNvWC8dhEifgX9Jg== + dependencies: + "@opentelemetry/api-logs" "0.212.0" + import-in-the-middle "^2.0.6" + require-in-the-middle "^8.0.0" + +"@opentelemetry/redis-common@^0.38.2": + version "0.38.2" + resolved "https://registry.yarnpkg.com/@opentelemetry/redis-common/-/redis-common-0.38.2.tgz#cefa4f3e79db1cd54f19e233b7dfb56621143955" + integrity sha512-1BCcU93iwSRZvDAgwUxC/DV4T/406SkMfxGqu5ojc3AvNI+I9GhV7v0J1HljsczuuhcnFLYqD5VmwVXfCGHzxA== + +"@opentelemetry/resources@2.6.1", "@opentelemetry/resources@^2.6.1": + version "2.6.1" + resolved "https://registry.yarnpkg.com/@opentelemetry/resources/-/resources-2.6.1.tgz#e1b02772c5f65c0e074d59e4743188f7575e97c7" + integrity sha512-lID/vxSuKWXM55XhAKNoYXu9Cutoq5hFdkbTdI/zDKQktXzcWBVhNsOkiZFTMU9UtEWuGRNe0HUgmsFldIdxVA== + dependencies: + "@opentelemetry/core" "2.6.1" + "@opentelemetry/semantic-conventions" "^1.29.0" + +"@opentelemetry/sdk-trace-base@^2.6.1": + version "2.6.1" + resolved "https://registry.yarnpkg.com/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.6.1.tgz#ed353062be4c28a0649247ad369654020c29bfce" + integrity sha512-r86ut4T1e8vNwB35CqCcKd45yzqH6/6Wzvpk2/cZB8PsPLlZFTvrh8yfOS3CYZYcUmAx4hHTZJ8AO8Dj8nrdhw== + dependencies: + "@opentelemetry/core" "2.6.1" + "@opentelemetry/resources" "2.6.1" + "@opentelemetry/semantic-conventions" "^1.29.0" + +"@opentelemetry/semantic-conventions@^1.24.0", "@opentelemetry/semantic-conventions@^1.27.0", "@opentelemetry/semantic-conventions@^1.28.0", "@opentelemetry/semantic-conventions@^1.29.0", "@opentelemetry/semantic-conventions@^1.30.0", "@opentelemetry/semantic-conventions@^1.33.0", "@opentelemetry/semantic-conventions@^1.33.1", "@opentelemetry/semantic-conventions@^1.34.0", "@opentelemetry/semantic-conventions@^1.36.0", "@opentelemetry/semantic-conventions@^1.40.0": + version "1.40.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/semantic-conventions/-/semantic-conventions-1.40.0.tgz#10b2944ca559386590683392022a897eefd011d3" + integrity sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw== + +"@opentelemetry/sql-common@^0.41.2": + version "0.41.2" + resolved "https://registry.yarnpkg.com/@opentelemetry/sql-common/-/sql-common-0.41.2.tgz#7f4a14166cfd6c9ffe89096db1cc75eaf6443b19" + integrity sha512-4mhWm3Z8z+i508zQJ7r6Xi7y4mmoJpdvH0fZPFRkWrdp5fq7hhZ2HhYokEOLkfqSMgPR4Z9EyB3DBkbKGOqZiQ== + dependencies: + "@opentelemetry/core" "^2.0.0" + +"@prisma/instrumentation@7.6.0": + version "7.6.0" + resolved "https://registry.yarnpkg.com/@prisma/instrumentation/-/instrumentation-7.6.0.tgz#22a4ea3e9d8cdc57cbaa0e26ccf10cb8db854549" + integrity sha512-ZPW2gRiwpPzEfgeZgaekhqXrbW+Y2RJKHVqUmlhZhKzRNCcvR6DykzylDrynpArKKRQtLxoZy36fK7U0p3pdgQ== + dependencies: + "@opentelemetry/instrumentation" "^0.207.0" + "@protobufjs/aspromise@^1.1.1", "@protobufjs/aspromise@^1.1.2": version "1.1.2" resolved "https://registry.yarnpkg.com/@protobufjs/aspromise/-/aspromise-1.1.2.tgz#9b8b0cc663d669a7d8f6f5d0893a14d348f30fbf" @@ -2656,6 +2963,118 @@ resolved "https://registry.yarnpkg.com/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz#60de891bb126abfdc5410fdc6166aca065f10a0c" integrity sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg== +"@sentry-internal/browser-utils@10.47.0": + version "10.47.0" + resolved "https://registry.yarnpkg.com/@sentry-internal/browser-utils/-/browser-utils-10.47.0.tgz#452320155e241c4fec44bccfe698effb453d31b4" + integrity sha512-bVFRAeJWMBcBCvJKIFCMJ1/yQToL4vPGqfmlnDZeypcxkqUDKQ/Y3ziLHXoDL2sx0lagcgU2vH1QhCQ67Aujjw== + dependencies: + "@sentry/core" "10.47.0" + +"@sentry-internal/feedback@10.47.0": + version "10.47.0" + resolved "https://registry.yarnpkg.com/@sentry-internal/feedback/-/feedback-10.47.0.tgz#2a847b821f60802c4ed3d0da980ef57f593afb26" + integrity sha512-pdvMmi4dQpX5S/vAAzrhHPIw3T3HjUgDNgUiCBrlp7N9/6zGO2gNPhUnNekP+CjgI/z0rvf49RLqlDenpNrMOg== + dependencies: + "@sentry/core" "10.47.0" + +"@sentry-internal/replay-canvas@10.47.0": + version "10.47.0" + resolved "https://registry.yarnpkg.com/@sentry-internal/replay-canvas/-/replay-canvas-10.47.0.tgz#157256195f71592cd462fe5a57e71ddbd66e9cff" + integrity sha512-A5OY8friSe6g8WAK4L8IeOPiEd9D3Ps40DzRH5j2f6SUja0t90mKMvHRcRf8zq0d4BkdB+JM7tjOkwxpuv8heA== + dependencies: + "@sentry-internal/replay" "10.47.0" + "@sentry/core" "10.47.0" + +"@sentry-internal/replay@10.47.0": + version "10.47.0" + resolved "https://registry.yarnpkg.com/@sentry-internal/replay/-/replay-10.47.0.tgz#33bb78457ee9731056d2b5a4805328e922b28886" + integrity sha512-ScdovxP7hJxgMt70+7hFvwT02GIaIUAxdEM/YPsayZBeCoAukPW8WiwztJfoKtsfPyKJ5A6f0H3PIxTPcA9Row== + dependencies: + "@sentry-internal/browser-utils" "10.47.0" + "@sentry/core" "10.47.0" + +"@sentry/browser@10.47.0": + version "10.47.0" + resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-10.47.0.tgz#286f5051ca82706c03e7a499b9464453225f3648" + integrity sha512-rC0agZdxKA5XWfL4VwPOr/rJMogXDqZgnVzr93YWpFn9DMZT/7LzxSJVPIJwRUjx3bFEby3PcTa3YaX7pxm1AA== + dependencies: + "@sentry-internal/browser-utils" "10.47.0" + "@sentry-internal/feedback" "10.47.0" + "@sentry-internal/replay" "10.47.0" + "@sentry-internal/replay-canvas" "10.47.0" + "@sentry/core" "10.47.0" + +"@sentry/core@10.47.0": + version "10.47.0" + resolved "https://registry.yarnpkg.com/@sentry/core/-/core-10.47.0.tgz#175d1865f0d762ebe7be3b2a6ec3ece4e5a76a5a" + integrity sha512-nsYRAx3EWezDut+Zl+UwwP07thh9uY7CfSAi2whTdcJl5hu1nSp2z8bba7Vq/MGbNLnazkd3A+GITBEML924JA== + +"@sentry/electron@^7.11.0": + version "7.11.0" + resolved "https://registry.yarnpkg.com/@sentry/electron/-/electron-7.11.0.tgz#39a21578d3a92524748ed7b0574dde0aad65b2dc" + integrity sha512-AKz66R/o/tULOg23zJyQZU2RK2uyV7PRYEWxDeyGDIfJeg+tXN1Zwjf/WuPcpoVE3xsXcCGBReboqMLgff587Q== + dependencies: + "@sentry/browser" "10.47.0" + "@sentry/core" "10.47.0" + "@sentry/node" "10.47.0" + +"@sentry/node-core@10.47.0": + version "10.47.0" + resolved "https://registry.yarnpkg.com/@sentry/node-core/-/node-core-10.47.0.tgz#1a010b8905361138d4c8cd4787431020d5c79629" + integrity sha512-qv6LsqHbkQmd0aQEUox/svRSz26J+l4gGjFOUNEay2armZu9XLD+Ct89jpFgZD5oIPNAj2jraodTRqydXiwS5w== + dependencies: + "@sentry/core" "10.47.0" + "@sentry/opentelemetry" "10.47.0" + import-in-the-middle "^3.0.0" + +"@sentry/node@10.47.0": + version "10.47.0" + resolved "https://registry.yarnpkg.com/@sentry/node/-/node-10.47.0.tgz#6bdbe0ee3a6bb568b402ec16456f14a7c1a98e46" + integrity sha512-R+btqPepv88o635G6HtVewLjqCLUedBg5HBs7Nq1qbbKvyti01uArUF2f+3DsLenk5B9LUNiRlE+frZA44Ahmw== + dependencies: + "@fastify/otel" "0.18.0" + "@opentelemetry/api" "^1.9.1" + "@opentelemetry/context-async-hooks" "^2.6.1" + "@opentelemetry/core" "^2.6.1" + "@opentelemetry/instrumentation" "^0.214.0" + "@opentelemetry/instrumentation-amqplib" "0.61.0" + "@opentelemetry/instrumentation-connect" "0.57.0" + "@opentelemetry/instrumentation-dataloader" "0.31.0" + "@opentelemetry/instrumentation-express" "0.62.0" + "@opentelemetry/instrumentation-fs" "0.33.0" + "@opentelemetry/instrumentation-generic-pool" "0.57.0" + "@opentelemetry/instrumentation-graphql" "0.62.0" + "@opentelemetry/instrumentation-hapi" "0.60.0" + "@opentelemetry/instrumentation-http" "0.214.0" + "@opentelemetry/instrumentation-ioredis" "0.62.0" + "@opentelemetry/instrumentation-kafkajs" "0.23.0" + "@opentelemetry/instrumentation-knex" "0.58.0" + "@opentelemetry/instrumentation-koa" "0.62.0" + "@opentelemetry/instrumentation-lru-memoizer" "0.58.0" + "@opentelemetry/instrumentation-mongodb" "0.67.0" + "@opentelemetry/instrumentation-mongoose" "0.60.0" + "@opentelemetry/instrumentation-mysql" "0.60.0" + "@opentelemetry/instrumentation-mysql2" "0.60.0" + "@opentelemetry/instrumentation-pg" "0.66.0" + "@opentelemetry/instrumentation-redis" "0.62.0" + "@opentelemetry/instrumentation-tedious" "0.33.0" + "@opentelemetry/instrumentation-undici" "0.24.0" + "@opentelemetry/resources" "^2.6.1" + "@opentelemetry/sdk-trace-base" "^2.6.1" + "@opentelemetry/semantic-conventions" "^1.40.0" + "@prisma/instrumentation" "7.6.0" + "@sentry/core" "10.47.0" + "@sentry/node-core" "10.47.0" + "@sentry/opentelemetry" "10.47.0" + import-in-the-middle "^3.0.0" + +"@sentry/opentelemetry@10.47.0": + version "10.47.0" + resolved "https://registry.yarnpkg.com/@sentry/opentelemetry/-/opentelemetry-10.47.0.tgz#499efc362ec9805e7a236a478a47bf3c745c2424" + integrity sha512-f6Hw2lrpCjlOksiosP0Z2jK/+l+21SIdoNglVeG/sttMyx8C8ywONKh0Ha50sFsvB1VaB8n94RKzzf3hkh9V3g== + dependencies: + "@sentry/core" "10.47.0" + "@sindresorhus/is@^4.0.0": version "4.6.0" resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-4.6.0.tgz#3c7c9c46e678feefe7a2e5bb609d3dbd665ffb3f" @@ -2868,6 +3287,13 @@ resolved "https://registry.yarnpkg.com/@types/caseless/-/caseless-0.12.5.tgz#db9468cb1b1b5a925b8f34822f1669df0c5472f5" integrity sha512-hWtVTC2q7hc7xZ/RLbxapMvDMgUnDvKvMOpKal4DrMyfGBUfB1oKaZlIRr6mJL+If3bAP6sV/QneGzF6tJjZDg== +"@types/connect@3.4.38": + version "3.4.38" + resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.38.tgz#5ba7f3bc4fbbdeaff8dded952e5ff2cc53f8d858" + integrity sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug== + dependencies: + "@types/node" "*" + "@types/debug@^4.0.0": version "4.1.13" resolved "https://registry.yarnpkg.com/@types/debug/-/debug-4.1.13.tgz#22d1cc9d542d3593caea764f974306ab36286ee7" @@ -2968,6 +3394,13 @@ dependencies: "@types/node" "*" +"@types/mysql@2.15.27": + version "2.15.27" + resolved "https://registry.yarnpkg.com/@types/mysql/-/mysql-2.15.27.tgz#fb13b0e8614d39d42f40f381217ec3215915f1e9" + integrity sha512-YfWiV16IY0OeBfBCk8+hXKmdTKrKlwKN1MNKAPBu5JYxLwBEZl7QzeEpGnlZb3VMGJrrGmB84gXiH+ofs/TezA== + dependencies: + "@types/node" "*" + "@types/node@*", "@types/node@^25.3.0": version "25.3.0" resolved "https://registry.yarnpkg.com/@types/node/-/node-25.3.0.tgz#749b1bd4058e51b72e22bd41e9eab6ebd0180470" @@ -2996,6 +3429,31 @@ dependencies: undici-types "~7.16.0" +"@types/pg-pool@2.0.7": + version "2.0.7" + resolved "https://registry.yarnpkg.com/@types/pg-pool/-/pg-pool-2.0.7.tgz#c17945a74472d9a3beaf8e66d5aa6fc938328734" + integrity sha512-U4CwmGVQcbEuqpyju8/ptOKg6gEC+Tqsvj2xS9o1g71bUh8twxnC6ZL5rZKCsGN0iyH0CwgUyc9VR5owNQF9Ng== + dependencies: + "@types/pg" "*" + +"@types/pg@*": + version "8.20.0" + resolved "https://registry.yarnpkg.com/@types/pg/-/pg-8.20.0.tgz#8bd03d3ac6b19143a8de7d66a9d13da32cd91526" + integrity sha512-bEPFOaMAHTEP1EzpvHTbmwR8UsFyHSKsRisLIHVMXnpNefSbGA1bD6CVy+qKjGSqmZqNqBDV2azOBo8TgkcVow== + dependencies: + "@types/node" "*" + pg-protocol "*" + pg-types "^2.2.0" + +"@types/pg@8.15.6": + version "8.15.6" + resolved "https://registry.yarnpkg.com/@types/pg/-/pg-8.15.6.tgz#4df7590b9ac557cbe5479e0074ec1540cbddad9b" + integrity sha512-NoaMtzhxOrubeL/7UZuNTrejB4MPAJ0RpxZqXQf2qXuVlTPuG6Y8p4u9dKRaue4yjmC7ZhzVO2/Yyyn25znrPQ== + dependencies: + "@types/node" "*" + pg-protocol "*" + pg-types "^2.2.0" + "@types/react-dom@^19.2.3": version "19.2.3" resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-19.2.3.tgz#c1e305d15a52a3e508d54dca770d202cb63abf2c" @@ -3035,6 +3493,13 @@ resolved "https://registry.yarnpkg.com/@types/statuses/-/statuses-2.0.6.tgz#66748315cc9a96d63403baa8671b2c124f8633aa" integrity sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA== +"@types/tedious@^4.0.14": + version "4.0.14" + resolved "https://registry.yarnpkg.com/@types/tedious/-/tedious-4.0.14.tgz#868118e7a67808258c05158e9cad89ca58a2aec1" + integrity sha512-KHPsfX/FoVbUGbyYvk1q9MMQHLPeRZhRJZdO45Q4YjvFkv4hMNghCWTvy7rdKessBsmtz4euWCWAB6/tVpI1Iw== + dependencies: + "@types/node" "*" + "@types/tough-cookie@*": version "4.0.5" resolved "https://registry.yarnpkg.com/@types/tough-cookie/-/tough-cookie-4.0.5.tgz#cb6e2a691b70cb177c6e3ae9c1d2e8b2ea8cd304" @@ -3394,6 +3859,11 @@ accepts@^2.0.0: mime-types "^3.0.0" negotiator "^1.0.0" +acorn-import-attributes@^1.9.5: + version "1.9.5" + resolved "https://registry.yarnpkg.com/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz#7eb1557b1ba05ef18b5ed0ec67591bfab04688ef" + integrity sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ== + acorn-import-phases@^1.0.3: version "1.0.4" resolved "https://registry.yarnpkg.com/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz#16eb850ba99a056cb7cbfe872ffb8972e18c8bd7" @@ -3735,6 +4205,13 @@ brace-expansion@^5.0.2: dependencies: balanced-match "^4.0.2" +brace-expansion@^5.0.5: + version "5.0.5" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-5.0.5.tgz#dcc3a37116b79f3e1b46db994ced5d570e930fdb" + integrity sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ== + dependencies: + balanced-match "^4.0.2" + braces@^3.0.3: version "3.0.3" resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789" @@ -3919,6 +4396,11 @@ chrome-trace-event@^1.0.2, chrome-trace-event@^1.0.3: resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz#05bffd7ff928465093314708c93bdfa9bd1f0f5b" integrity sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ== +cjs-module-lexer@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/cjs-module-lexer/-/cjs-module-lexer-2.2.0.tgz#b3ca5101843389259ade7d88c77bd06ce55849ca" + integrity sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ== + class-variance-authority@^0.7.1: version "0.7.1" resolved "https://registry.yarnpkg.com/class-variance-authority/-/class-variance-authority-0.7.1.tgz#4008a798a0e4553a781a57ac5177c9fb5d043787" @@ -4216,7 +4698,7 @@ data-view-byte-offset@^1.0.1: es-errors "^1.3.0" is-data-view "^1.0.1" -debug@4, debug@^4.0.0, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.3, debug@^4.3.4, debug@^4.4.0, debug@^4.4.3: +debug@4, debug@^4.0.0, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.3, debug@^4.3.4, debug@^4.3.5, debug@^4.4.0, debug@^4.4.3: version "4.4.3" resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.3.tgz#c6ae432d9bd9662582fce08709b038c58e9e3d6a" integrity sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA== @@ -5301,6 +5783,11 @@ formdata-polyfill@^4.0.10: dependencies: fetch-blob "^3.1.2" +forwarded-parse@2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/forwarded-parse/-/forwarded-parse-2.1.2.tgz#08511eddaaa2ddfd56ba11138eee7df117a09325" + integrity sha512-alTFZZQDKMporBH77856pXgzhEzaUVmLCDk+egLgIgHst3Tpndzz8MnKe+GzRJRfvVdn69HhpW7cmXzvtLvJAw== + forwarded@0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" @@ -5955,6 +6442,26 @@ import-fresh@^3.2.1, import-fresh@^3.3.0: parent-module "^1.0.0" resolve-from "^4.0.0" +import-in-the-middle@^2.0.0, import-in-the-middle@^2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/import-in-the-middle/-/import-in-the-middle-2.0.6.tgz#1972337bfe020d05f6b5e020c13334567436324f" + integrity sha512-3vZV3jX0XRFW3EJDTwzWoZa+RH1b8eTTx6YOCjglrLyPuepwoBti1k3L2dKwdCUrnVEfc5CuRuGstaC/uQJJaw== + dependencies: + acorn "^8.15.0" + acorn-import-attributes "^1.9.5" + cjs-module-lexer "^2.2.0" + module-details-from-path "^1.0.4" + +import-in-the-middle@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/import-in-the-middle/-/import-in-the-middle-3.0.1.tgz#8a0a1230c9b865c0e12698171646ae1e3fff691d" + integrity sha512-pYkiyXVL2Mf3pozdlDGV6NAObxQx13Ae8knZk1UJRJ6uRW/ZRmTGHlQYtrsSl7ubuE5F8CD1z+s1n4RHNuTtuA== + dependencies: + acorn "^8.15.0" + acorn-import-attributes "^1.9.5" + cjs-module-lexer "^2.2.0" + module-details-from-path "^1.0.4" + imurmurhash@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" @@ -7387,6 +7894,13 @@ minimatch@^10.2.2: dependencies: brace-expansion "^5.0.2" +minimatch@^10.2.4: + version "10.2.5" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-10.2.5.tgz#bd48687a0be38ed2961399105600f832095861d1" + integrity sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg== + dependencies: + brace-expansion "^5.0.5" + minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: version "3.1.2" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" @@ -7484,6 +7998,11 @@ mkdirp@^1.0.3, mkdirp@^1.0.4: resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== +module-details-from-path@^1.0.3, module-details-from-path@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/module-details-from-path/-/module-details-from-path-1.0.4.tgz#b662fdcd93f6c83d3f25289da0ce81c8d9685b94" + integrity sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w== + ms@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" @@ -8024,6 +8543,27 @@ pend@~1.2.0: resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50" integrity sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg== +pg-int8@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/pg-int8/-/pg-int8-1.0.1.tgz#943bd463bf5b71b4170115f80f8efc9a0c0eb78c" + integrity sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw== + +pg-protocol@*: + version "1.13.0" + resolved "https://registry.yarnpkg.com/pg-protocol/-/pg-protocol-1.13.0.tgz#fdaf6d020bca590d58bb991b4b16fc448efe0511" + integrity sha512-zzdvXfS6v89r6v7OcFCHfHlyG/wvry1ALxZo4LqgUoy7W9xhBDMaqOuMiF3qEV45VqsN6rdlcehHrfDtlCPc8w== + +pg-types@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/pg-types/-/pg-types-2.2.0.tgz#2d0250d636454f7cfa3b6ae0382fdfa8063254a3" + integrity sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA== + dependencies: + pg-int8 "1.0.1" + postgres-array "~2.0.0" + postgres-bytea "~1.0.0" + postgres-date "~1.0.4" + postgres-interval "^1.1.0" + picocolors@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b" @@ -8080,6 +8620,28 @@ postcss@^8.4.43, postcss@^8.5.6: picocolors "^1.1.1" source-map-js "^1.2.1" +postgres-array@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/postgres-array/-/postgres-array-2.0.0.tgz#48f8fce054fbc69671999329b8834b772652d82e" + integrity sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA== + +postgres-bytea@~1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/postgres-bytea/-/postgres-bytea-1.0.1.tgz#c40b3da0222c500ff1e51c5d7014b60b79697c7a" + integrity sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ== + +postgres-date@~1.0.4: + version "1.0.7" + resolved "https://registry.yarnpkg.com/postgres-date/-/postgres-date-1.0.7.tgz#51bc086006005e5061c591cee727f2531bf641a8" + integrity sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q== + +postgres-interval@^1.1.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/postgres-interval/-/postgres-interval-1.2.0.tgz#b460c82cb1587507788819a06aa0fffdb3544695" + integrity sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ== + dependencies: + xtend "^4.0.0" + postject@^1.0.0-alpha.6: version "1.0.0-alpha.6" resolved "https://registry.yarnpkg.com/postject/-/postject-1.0.0-alpha.6.tgz#9d022332272e2cfce8dea4cfce1ee6dd1b2ee135" @@ -8530,6 +9092,14 @@ require-from-string@^2.0.2: resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909" integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw== +require-in-the-middle@^8.0.0: + version "8.0.1" + resolved "https://registry.yarnpkg.com/require-in-the-middle/-/require-in-the-middle-8.0.1.tgz#dbde2587f669398626d56b20c868ab87bf01cce4" + integrity sha512-QT7FVMXfWOYFbeRBF6nu+I6tr2Tf3u0q8RIEjNob/heKY/nh7drD/k7eeMFmSQgnTtCzLDcCu/XEnpW2wk4xCQ== + dependencies: + debug "^4.3.5" + module-details-from-path "^1.0.3" + resedit@^2.0.0: version "2.0.3" resolved "https://registry.yarnpkg.com/resedit/-/resedit-2.0.3.tgz#5145a9faabca44b917d5636dbe8e67ec7f62c6f2" @@ -10178,6 +10748,11 @@ xmlbuilder@^15.1.1: resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-15.1.1.tgz#9dcdce49eea66d8d10b42cae94a79c3c8d0c2ec5" integrity sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg== +xtend@^4.0.0: + version "4.0.2" + resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" + integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== + y18n@^5.0.5: version "5.0.8" resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55" -- 2.54.0 From 156ef5a204dae7bd45d2001c98885cdea4dcd4fe Mon Sep 17 00:00:00 2001 From: Arjun Patel Date: Mon, 20 Apr 2026 18:40:07 -0700 Subject: [PATCH 06/12] chore: cleanup reexports --- js/src/api/client.ts | 3 --- js/src/features/compose/compose-overlay.tsx | 3 ++- js/src/hooks/use-create-particle.ts | 3 --- js/src/stores/auth-store.ts | 4 ++-- 4 files changed, 4 insertions(+), 9 deletions(-) diff --git a/js/src/api/client.ts b/js/src/api/client.ts index 7040cfa..27ef599 100644 --- a/js/src/api/client.ts +++ b/js/src/api/client.ts @@ -28,9 +28,6 @@ import type { SignInRequest, } from "./types"; -// Re-export for back-compat with existing `import { ApiError } from "@/api/client"`. -export { ApiError }; - interface ApiClientConfig { baseUrl: string; getToken: () => string | null; diff --git a/js/src/features/compose/compose-overlay.tsx b/js/src/features/compose/compose-overlay.tsx index d361902..d90b9eb 100644 --- a/js/src/features/compose/compose-overlay.tsx +++ b/js/src/features/compose/compose-overlay.tsx @@ -1,7 +1,8 @@ import { useCallback, useEffect, useEffectEvent, useRef, useState } from "react"; import { toast } from "sonner"; import { useAuthStore } from "@/stores/auth-store"; -import { QuotaExceededError, useCreateParticle, useCreateStreamParticle } from "@/hooks/use-create-particle"; +import { useCreateParticle, useCreateStreamParticle } from "@/hooks/use-create-particle"; +import { QuotaExceededError } from "@/lib/errors"; import { isUsageExhausted, useInvalidateNetworkUsage, useNetworkUsage } from "@/hooks/use-network-usage"; import { useRecorder } from "@/features/compose/use-recorder"; import { useScreenRecorder } from "@/features/compose/use-screen-recorder"; diff --git a/js/src/hooks/use-create-particle.ts b/js/src/hooks/use-create-particle.ts index c08bc8b..1d9a085 100644 --- a/js/src/hooks/use-create-particle.ts +++ b/js/src/hooks/use-create-particle.ts @@ -10,9 +10,6 @@ import { useInvalidateNetworkUsage, } from "./use-network-usage"; -// Re-export for back-compat with existing `import { QuotaExceededError } from "@/hooks/use-create-particle"`. -export { QuotaExceededError }; - interface CreateParticleParams { // Path to which the new particle will be added as a child path: ParticlePath; diff --git a/js/src/stores/auth-store.ts b/js/src/stores/auth-store.ts index 427157d..9b455b2 100644 --- a/js/src/stores/auth-store.ts +++ b/js/src/stores/auth-store.ts @@ -1,9 +1,9 @@ import { create } from "zustand"; import { signInWithCustomToken, signOut as firebaseSignOut } from "firebase/auth"; -import { apiClient, ApiError } from "@/api/client"; +import { apiClient } from "@/api/client"; import type { Human } from "@/api/types"; import { firebaseAuth } from "@/firebase"; -import { logError } from "@/lib/errors"; +import { logError, ApiError } from "@/lib/errors"; import { useSessionStore } from "./session-store"; async function signInToFirebase() { -- 2.54.0 From 58fdf0cf5dcc0055822537eaf5ea1ccd7fd64fe0 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 21 Apr 2026 01:54:38 +0000 Subject: [PATCH 07/12] fix(errors): use relative import so main-process bundle resolves MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rollup in vite.main.config.ts doesn't know about the `@/` alias (only the renderer configs do). After PR 4 made `lib/errors.ts` reachable from main, its `@/config/env` import broke the main build with: [vite]: Rollup failed to resolve import "@/config/env" from "lib/errors.ts" Swap to a relative import — the module is now genuinely isomorphic. --- js/src/lib/errors.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/src/lib/errors.ts b/js/src/lib/errors.ts index 25d3d79..d35849c 100644 --- a/js/src/lib/errors.ts +++ b/js/src/lib/errors.ts @@ -1,5 +1,5 @@ import { z } from "zod"; -import { appEnv } from "@/config/env"; +import { appEnv } from "../config/env"; export class ApiError extends Error { constructor( -- 2.54.0 From ac6d5906eb7207951501a5c118c51f49492cfec1 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 21 Apr 2026 01:56:53 +0000 Subject: [PATCH 08/12] build(vite): resolve @/ alias in main + preload configs The four renderer vite configs all resolve "@" to ./src; main and preload didn't, so any main-reachable file using @/ imports would break the bundle. Mirror the same alias block in both configs and revert lib/errors.ts + src/main/* to the project's @/ convention. --- js/src/lib/errors.ts | 2 +- js/src/main/ipc-utils.ts | 2 +- js/src/main/sentry.ts | 4 ++-- js/vite.main.config.ts | 6 ++++++ js/vite.preload.config.ts | 6 ++++++ 5 files changed, 16 insertions(+), 4 deletions(-) diff --git a/js/src/lib/errors.ts b/js/src/lib/errors.ts index d35849c..25d3d79 100644 --- a/js/src/lib/errors.ts +++ b/js/src/lib/errors.ts @@ -1,5 +1,5 @@ import { z } from "zod"; -import { appEnv } from "../config/env"; +import { appEnv } from "@/config/env"; export class ApiError extends Error { constructor( diff --git a/js/src/main/ipc-utils.ts b/js/src/main/ipc-utils.ts index 94b4b8a..3b14cbe 100644 --- a/js/src/main/ipc-utils.ts +++ b/js/src/main/ipc-utils.ts @@ -1,5 +1,5 @@ import { ipcMain, type IpcMainInvokeEvent } from "electron"; -import { reportError } from "../lib/errors"; +import { reportError } from "@/lib/errors"; type InvokeHandler = ( event: IpcMainInvokeEvent, diff --git a/js/src/main/sentry.ts b/js/src/main/sentry.ts index 213e966..8b724ed 100644 --- a/js/src/main/sentry.ts +++ b/js/src/main/sentry.ts @@ -1,7 +1,7 @@ import { app } from "electron"; import * as Sentry from "@sentry/electron/main"; -import { appConfig } from "../config/env"; -import { installErrorSinks } from "../lib/errors"; +import { appConfig } from "@/config/env"; +import { installErrorSinks } from "@/lib/errors"; /** * Initialise Sentry for the main process. Captures uncaught exceptions from diff --git a/js/vite.main.config.ts b/js/vite.main.config.ts index fd869a2..c7ef493 100644 --- a/js/vite.main.config.ts +++ b/js/vite.main.config.ts @@ -1,7 +1,13 @@ +import path from "path"; import { defineConfig } from 'vite'; import { defineEnv } from './vite.env'; // https://vitejs.dev/config export default defineConfig({ + resolve: { + alias: { + "@": path.resolve(__dirname, "./src"), + }, + }, define: defineEnv, }); diff --git a/js/vite.preload.config.ts b/js/vite.preload.config.ts index fd869a2..c7ef493 100644 --- a/js/vite.preload.config.ts +++ b/js/vite.preload.config.ts @@ -1,7 +1,13 @@ +import path from "path"; import { defineConfig } from 'vite'; import { defineEnv } from './vite.env'; // https://vitejs.dev/config export default defineConfig({ + resolve: { + alias: { + "@": path.resolve(__dirname, "./src"), + }, + }, define: defineEnv, }); -- 2.54.0 From 4fd43ed5d5543154f7d8530f9697d0d2a259ffd4 Mon Sep 17 00:00:00 2001 From: Arjun from Flowy Labs Date: Mon, 20 Apr 2026 18:58:29 -0700 Subject: [PATCH 09/12] fix invalid tsconfig field --- js/tsconfig.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/tsconfig.json b/js/tsconfig.json index 15a561d..8e0caad 100644 --- a/js/tsconfig.json +++ b/js/tsconfig.json @@ -12,7 +12,7 @@ "resolveJsonModule": true, "jsx": "react-jsx", "strict": true, - "baseUrl": ".", + "baseUrl": "./", "paths": { "@/*": ["./src/*"] } -- 2.54.0 From cc16310d12f13911541ec37a01177630e3b47e1e Mon Sep 17 00:00:00 2001 From: Arjun Patel Date: Mon, 20 Apr 2026 19:05:51 -0700 Subject: [PATCH 10/12] comment --- js/src/hooks/use-create-particle.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/src/hooks/use-create-particle.ts b/js/src/hooks/use-create-particle.ts index 1d9a085..4094479 100644 --- a/js/src/hooks/use-create-particle.ts +++ b/js/src/hooks/use-create-particle.ts @@ -30,7 +30,7 @@ export function useCreateParticle() { mutationFn: async (params: CreateParticleParams) => { const { networkId } = parseParticlePath(params.path); - // Containers aren't counted server-side, so we don't block them. + // Containers aren't counted server-side, so we block them here if (!CONTAINER_TYPES.has(params.type)) { const cached = qc.getQueryData(networkUsageQueryKey(networkId)); if (isUsageExhausted(cached)) { -- 2.54.0 From ed84b29c6b24611838c481fdf21980d11f7d88fd Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 21 Apr 2026 02:19:14 +0000 Subject: [PATCH 11/12] feat(errors): fallback handlers for unhandled rejections and errors Sentry's global integrations natively capture window.onerror / unhandledrejection (renderer) and uncaughtException / unhandledRejection (main). When a DSN is configured we let Sentry own those paths to avoid double-capturing. When it isn't (dev, unconfigured prod), we attach minimal listeners that route through reportError so stray promise rejections and uncaught errors at least hit the console and the facade. This closes the one gap where a bare click-handler promise rejection (no try/catch, not a useMutation) would otherwise be invisible. --- js/src/lib/sentry.ts | 49 +++++++++++++++++++++++++++---------------- js/src/main/sentry.ts | 47 ++++++++++++++++++++++++----------------- 2 files changed, 59 insertions(+), 37 deletions(-) diff --git a/js/src/lib/sentry.ts b/js/src/lib/sentry.ts index 284a35c..41b6812 100644 --- a/js/src/lib/sentry.ts +++ b/js/src/lib/sentry.ts @@ -1,28 +1,41 @@ import * as Sentry from "@sentry/electron/renderer"; import { appConfig } from "@/config/env"; -import { installErrorSinks } from "@/lib/errors"; +import { installErrorSinks, reportError } from "@/lib/errors"; /** - * Initialise Sentry for a renderer process. No-ops when `sentryDsn` is empty - * so dev builds and unconfigured envs stay quiet. + * Initialise renderer error handling. When a DSN is configured, Sentry's + * own global handlers capture uncaught exceptions and unhandled promise + * rejections. Otherwise we install a minimal fallback so stray rejections + * aren't silent in dev / unconfigured envs. */ export function initSentryRenderer(): void { - if (!appConfig.sentryDsn) return; + if (appConfig.sentryDsn) { + Sentry.init({ + dsn: appConfig.sentryDsn, + tracesSampleRate: 0, + }); - Sentry.init({ - dsn: appConfig.sentryDsn, - tracesSampleRate: 0, + installErrorSinks({ + capture: (err, context) => + Sentry.captureException(err, { extra: context }), + breadcrumb: (err, context) => + Sentry.addBreadcrumb({ + category: "error", + level: "error", + message: err instanceof Error ? err.message : String(err), + data: context, + }), + }); + return; + } + + window.addEventListener("unhandledrejection", (event) => { + reportError(event.reason, { scope: "unhandledrejection" }); }); - - installErrorSinks({ - capture: (err, context) => - Sentry.captureException(err, { extra: context }), - breadcrumb: (err, context) => - Sentry.addBreadcrumb({ - category: "error", - level: "error", - message: err instanceof Error ? err.message : String(err), - data: context, - }), + window.addEventListener("error", (event) => { + reportError(event.error ?? new Error(event.message), { + scope: "window.error", + }); }); } + diff --git a/js/src/main/sentry.ts b/js/src/main/sentry.ts index 8b724ed..ce8aa8c 100644 --- a/js/src/main/sentry.ts +++ b/js/src/main/sentry.ts @@ -1,30 +1,39 @@ import { app } from "electron"; import * as Sentry from "@sentry/electron/main"; import { appConfig } from "@/config/env"; -import { installErrorSinks } from "@/lib/errors"; +import { installErrorSinks, reportError } from "@/lib/errors"; /** - * Initialise Sentry for the main process. Captures uncaught exceptions from - * the Node side and the crash reporter. Safe to call before `app.whenReady`. + * Initialise main-process error handling. Sentry owns uncaught-exception + * capture when a DSN is configured; otherwise we install a minimal Node + * fallback so rejections aren't silent. Safe to call before `app.whenReady`. */ export function initSentryMain(): void { - if (!appConfig.sentryDsn) return; + if (appConfig.sentryDsn) { + Sentry.init({ + dsn: appConfig.sentryDsn, + tracesSampleRate: 0, + release: app.getVersion(), + }); - Sentry.init({ - dsn: appConfig.sentryDsn, - tracesSampleRate: 0, - release: app.getVersion(), + installErrorSinks({ + capture: (err, context) => + Sentry.captureException(err, { extra: context }), + breadcrumb: (err, context) => + Sentry.addBreadcrumb({ + category: "error", + level: "error", + message: err instanceof Error ? err.message : String(err), + data: context, + }), + }); + return; + } + + process.on("uncaughtException", (err) => { + reportError(err, { scope: "uncaughtException" }); }); - - installErrorSinks({ - capture: (err, context) => - Sentry.captureException(err, { extra: context }), - breadcrumb: (err, context) => - Sentry.addBreadcrumb({ - category: "error", - level: "error", - message: err instanceof Error ? err.message : String(err), - data: context, - }), + process.on("unhandledRejection", (reason) => { + reportError(reason, { scope: "unhandledRejection" }); }); } -- 2.54.0 From 13fd6cbbad319c27fbad42ef48e4b2de113ebf03 Mon Sep 17 00:00:00 2001 From: Arjun Patel Date: Mon, 20 Apr 2026 19:21:38 -0700 Subject: [PATCH 12/12] Revert "feat(errors): fallback handlers for unhandled rejections and errors" This reverts commit ed84b29c6b24611838c481fdf21980d11f7d88fd. --- js/src/lib/sentry.ts | 49 ++++++++++++++++--------------------------- js/src/main/sentry.ts | 47 +++++++++++++++++------------------------ 2 files changed, 37 insertions(+), 59 deletions(-) diff --git a/js/src/lib/sentry.ts b/js/src/lib/sentry.ts index 41b6812..284a35c 100644 --- a/js/src/lib/sentry.ts +++ b/js/src/lib/sentry.ts @@ -1,41 +1,28 @@ import * as Sentry from "@sentry/electron/renderer"; import { appConfig } from "@/config/env"; -import { installErrorSinks, reportError } from "@/lib/errors"; +import { installErrorSinks } from "@/lib/errors"; /** - * Initialise renderer error handling. When a DSN is configured, Sentry's - * own global handlers capture uncaught exceptions and unhandled promise - * rejections. Otherwise we install a minimal fallback so stray rejections - * aren't silent in dev / unconfigured envs. + * Initialise Sentry for a renderer process. No-ops when `sentryDsn` is empty + * so dev builds and unconfigured envs stay quiet. */ export function initSentryRenderer(): void { - if (appConfig.sentryDsn) { - Sentry.init({ - dsn: appConfig.sentryDsn, - tracesSampleRate: 0, - }); + if (!appConfig.sentryDsn) return; - installErrorSinks({ - capture: (err, context) => - Sentry.captureException(err, { extra: context }), - breadcrumb: (err, context) => - Sentry.addBreadcrumb({ - category: "error", - level: "error", - message: err instanceof Error ? err.message : String(err), - data: context, - }), - }); - return; - } - - window.addEventListener("unhandledrejection", (event) => { - reportError(event.reason, { scope: "unhandledrejection" }); + Sentry.init({ + dsn: appConfig.sentryDsn, + tracesSampleRate: 0, }); - window.addEventListener("error", (event) => { - reportError(event.error ?? new Error(event.message), { - scope: "window.error", - }); + + installErrorSinks({ + capture: (err, context) => + Sentry.captureException(err, { extra: context }), + breadcrumb: (err, context) => + Sentry.addBreadcrumb({ + category: "error", + level: "error", + message: err instanceof Error ? err.message : String(err), + data: context, + }), }); } - diff --git a/js/src/main/sentry.ts b/js/src/main/sentry.ts index ce8aa8c..8b724ed 100644 --- a/js/src/main/sentry.ts +++ b/js/src/main/sentry.ts @@ -1,39 +1,30 @@ import { app } from "electron"; import * as Sentry from "@sentry/electron/main"; import { appConfig } from "@/config/env"; -import { installErrorSinks, reportError } from "@/lib/errors"; +import { installErrorSinks } from "@/lib/errors"; /** - * Initialise main-process error handling. Sentry owns uncaught-exception - * capture when a DSN is configured; otherwise we install a minimal Node - * fallback so rejections aren't silent. Safe to call before `app.whenReady`. + * Initialise Sentry for the main process. Captures uncaught exceptions from + * the Node side and the crash reporter. Safe to call before `app.whenReady`. */ export function initSentryMain(): void { - if (appConfig.sentryDsn) { - Sentry.init({ - dsn: appConfig.sentryDsn, - tracesSampleRate: 0, - release: app.getVersion(), - }); + if (!appConfig.sentryDsn) return; - installErrorSinks({ - capture: (err, context) => - Sentry.captureException(err, { extra: context }), - breadcrumb: (err, context) => - Sentry.addBreadcrumb({ - category: "error", - level: "error", - message: err instanceof Error ? err.message : String(err), - data: context, - }), - }); - return; - } - - process.on("uncaughtException", (err) => { - reportError(err, { scope: "uncaughtException" }); + Sentry.init({ + dsn: appConfig.sentryDsn, + tracesSampleRate: 0, + release: app.getVersion(), }); - process.on("unhandledRejection", (reason) => { - reportError(reason, { scope: "unhandledRejection" }); + + installErrorSinks({ + capture: (err, context) => + Sentry.captureException(err, { extra: context }), + breadcrumb: (err, context) => + Sentry.addBreadcrumb({ + category: "error", + level: "error", + message: err instanceof Error ? err.message : String(err), + data: context, + }), }); } -- 2.54.0