feat: integrate Sentry for error tracking and enhance user notifications

- Added Sentry integration for error tracking across various components, including error boundaries and user actions.
- Updated global error handling to capture exceptions and provide detailed feedback to users.
- Enhanced user notifications with toast messages for actions such as credit management, image generation, and canvas exports.
- Improved user experience by displaying relevant messages during interactions, ensuring better visibility of system states and errors.
This commit is contained in:
Matthias
2026-03-27 18:14:04 +01:00
parent 5da0204163
commit 2f89465e82
35 changed files with 2822 additions and 186 deletions

View File

@@ -10,6 +10,8 @@ import BaseNodeWrapper from "./base-node-wrapper";
import { DEFAULT_MODEL_ID, getModel } from "@/lib/ai-models";
import { classifyError, type AiErrorCategory } from "@/lib/ai-errors";
import { DEFAULT_ASPECT_RATIO } from "@/lib/image-formats";
import { toast } from "@/lib/toast";
import { msg } from "@/lib/toast-messages";
import {
Loader2,
AlertCircle,
@@ -106,16 +108,30 @@ export default function AiImageNode({
}
}
await generateImage({
canvasId,
nodeId: id as Id<"nodes">,
prompt,
referenceStorageId,
model: nodeData.model ?? DEFAULT_MODEL_ID,
aspectRatio: nodeData.aspectRatio ?? DEFAULT_ASPECT_RATIO,
});
const modelId = nodeData.model ?? DEFAULT_MODEL_ID;
const regenCreditCost = getModel(modelId)?.creditCost ?? 4;
await toast.promise(
generateImage({
canvasId,
nodeId: id as Id<"nodes">,
prompt,
referenceStorageId,
model: modelId,
aspectRatio: nodeData.aspectRatio ?? DEFAULT_ASPECT_RATIO,
}),
{
loading: msg.ai.generating.title,
success: msg.ai.generated.title,
error: msg.ai.generationFailed.title,
description: {
success: msg.ai.generatedDesc(regenCreditCost),
error: msg.ai.creditsNotCharged,
},
},
);
} catch (err) {
setLocalError(err instanceof Error ? err.message : "Generation failed");
setLocalError(err instanceof Error ? err.message : msg.ai.generationFailed.title);
} finally {
setIsGenerating(false);
}

View File

@@ -8,6 +8,8 @@ import { api } from "@/convex/_generated/api";
import type { Id } from "@/convex/_generated/dataModel";
import { useDebouncedCallback } from "@/hooks/use-debounced-callback";
import BaseNodeWrapper from "./base-node-wrapper";
import { toast } from "@/lib/toast";
import { msg } from "@/lib/toast-messages";
interface FrameNodeData {
label?: string;
@@ -43,16 +45,29 @@ export default function FrameNode({ id, data, selected, width, height }: NodePro
try {
const result = await exportFrame({ frameNodeId: id as Id<"nodes"> });
const a = document.createElement("a");
a.href = result.url;
a.download = result.filename;
a.click();
const fileLabel = `${label.trim() || "frame"}.png`;
toast.action(msg.export.frameExported.title, {
description: fileLabel,
label: msg.export.download,
onClick: () => {
window.open(result.url, "_blank", "noopener,noreferrer");
},
successLabel: msg.export.downloaded,
type: "success",
});
} catch (error) {
setExportError(error instanceof Error ? error.message : "Export failed");
const m = error instanceof Error ? error.message : "";
if (m.includes("No images found")) {
toast.error(msg.export.frameEmpty.title, msg.export.frameEmpty.desc);
setExportError(msg.export.frameEmpty.desc);
} else {
toast.error(msg.export.exportFailed.title, m || undefined);
setExportError(m || msg.export.exportFailed.title);
}
} finally {
setIsExporting(false);
}
}, [exportFrame, id, isExporting]);
}, [exportFrame, id, isExporting, label]);
const frameW = Math.round(width ?? 400);
const frameH = Math.round(height ?? 300);

View File

@@ -13,6 +13,15 @@ import { api } from "@/convex/_generated/api";
import type { Id } from "@/convex/_generated/dataModel";
import Image from "next/image";
import BaseNodeWrapper from "./base-node-wrapper";
import { toast } from "@/lib/toast";
import { msg } from "@/lib/toast-messages";
const ALLOWED_IMAGE_TYPES = new Set([
"image/png",
"image/jpeg",
"image/webp",
]);
const MAX_IMAGE_BYTES = 10 * 1024 * 1024;
type ImageNodeData = {
storageId?: string;
@@ -34,7 +43,21 @@ export default function ImageNode({ id, data, selected }: NodeProps<ImageNode>)
const uploadFile = useCallback(
async (file: File) => {
if (!file.type.startsWith("image/")) return;
if (!ALLOWED_IMAGE_TYPES.has(file.type)) {
const { title, desc } = msg.canvas.uploadFormatError(
file.type || file.name.split(".").pop() || "—",
);
toast.error(title, desc);
return;
}
if (file.size > MAX_IMAGE_BYTES) {
const { title, desc } = msg.canvas.uploadSizeError(
Math.round(MAX_IMAGE_BYTES / (1024 * 1024)),
);
toast.error(title, desc);
return;
}
setIsUploading(true);
try {
@@ -59,8 +82,13 @@ export default function ImageNode({ id, data, selected }: NodeProps<ImageNode>)
mimeType: file.type,
},
});
toast.success(msg.canvas.imageUploaded.title);
} catch (err) {
console.error("Upload failed:", err);
toast.error(
msg.canvas.uploadFailed.title,
err instanceof Error ? err.message : undefined,
);
} finally {
setIsUploading(false);
}

View File

@@ -1,5 +1,6 @@
"use client";
import * as Sentry from "@sentry/nextjs";
import type { ErrorInfo, ReactNode } from "react";
import { Component } from "react";
@@ -29,6 +30,11 @@ export class NodeErrorBoundary extends Component<
}
override componentDidCatch(error: Error, errorInfo: ErrorInfo) {
Sentry.captureException(error, {
tags: { nodeType: this.props.nodeType },
extra: { componentStack: errorInfo.componentStack },
});
console.error("Node rendering error", {
nodeType: this.props.nodeType,
error,

View File

@@ -34,6 +34,9 @@ import {
SelectValue,
} from "@/components/ui/select";
import { Sparkles, Loader2, Coins } from "lucide-react";
import { useRouter } from "next/navigation";
import { toast } from "@/lib/toast";
import { msg } from "@/lib/toast-messages";
type PromptNodeData = {
prompt?: string;
@@ -52,6 +55,7 @@ export default function PromptNode({
selected,
}: NodeProps<PromptNode>) {
const nodeData = data as PromptNodeData;
const router = useRouter();
const { getEdges, getNode } = useReactFlow();
const [prompt, setPrompt] = useState(nodeData.prompt ?? "");
@@ -151,6 +155,21 @@ export default function PromptNode({
const handleGenerate = useCallback(async () => {
if (!effectivePrompt.trim() || isGenerating) return;
if (availableCredits !== null && !hasEnoughCredits) {
const { title, desc } = msg.ai.insufficientCredits(
creditCost,
availableCredits,
);
toast.action(title, {
description: desc,
label: msg.billing.topUp,
onClick: () => router.push("/settings/billing"),
type: "warning",
});
return;
}
setError(null);
setIsGenerating(true);
@@ -214,16 +233,27 @@ export default function PromptNode({
targetHandle: "prompt-in",
});
await generateImage({
canvasId,
nodeId: aiNodeId,
prompt: promptToUse,
referenceStorageId,
model: DEFAULT_MODEL_ID,
aspectRatio,
});
await toast.promise(
generateImage({
canvasId,
nodeId: aiNodeId,
prompt: promptToUse,
referenceStorageId,
model: DEFAULT_MODEL_ID,
aspectRatio,
}),
{
loading: msg.ai.generating.title,
success: msg.ai.generated.title,
error: msg.ai.generationFailed.title,
description: {
success: msg.ai.generatedDesc(creditCost),
error: msg.ai.creditsNotCharged,
},
},
);
} catch (err) {
setError(err instanceof Error ? err.message : "Bildgenerierung fehlgeschlagen");
setError(err instanceof Error ? err.message : msg.ai.generationFailed.title);
} finally {
setIsGenerating(false);
}
@@ -239,6 +269,10 @@ export default function PromptNode({
createNodeWithIntersection,
createEdge,
generateImage,
creditCost,
availableCredits,
hasEnoughCredits,
router,
]);
return (
@@ -328,14 +362,11 @@ export default function PromptNode({
type="button"
onClick={() => void handleGenerate()}
disabled={
!effectivePrompt.trim() ||
isGenerating ||
balance === undefined ||
(availableCredits !== null && !hasEnoughCredits)
!effectivePrompt.trim() || isGenerating || balance === undefined
}
className={`nodrag flex items-center justify-center gap-2 rounded-md px-3 py-2 text-sm font-medium transition-colors disabled:cursor-not-allowed ${
availableCredits !== null && !hasEnoughCredits
? "bg-muted text-muted-foreground"
? "bg-amber-600/90 text-white hover:bg-amber-600"
: "bg-violet-600 text-white hover:bg-violet-700 disabled:opacity-50"
}`}
>