import { z } from "zod"; import { appEnv } from "@/config/env"; 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. 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) { 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(); } 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; type ErrorSink = (err: unknown, context?: ErrorContext) => void; // 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 captured. */ export function reportError(err: unknown, context?: ErrorContext): void { // eslint-disable-next-line no-console console.error("[error]", err, context ?? {}); captureSink?.(err, context); }