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); }