91 lines
2.8 KiB
TypeScript
91 lines
2.8 KiB
TypeScript
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";
|
|
}
|
|
}
|
|
|
|
function normalizeMessage(message: string): string {
|
|
return message.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<string, unknown>;
|
|
type ErrorSink = (err: unknown, context?: ErrorContext) => void;
|
|
|
|
// Sentry (or any observability backend) installs itself via `installErrorSinks`
|
|
// from App.tsx. 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);
|
|
}
|