From cc849fa67985c01b0bbf7463812bc07aefccd248 Mon Sep 17 00:00:00 2001 From: Shubham Gupta Date: Wed, 11 Feb 2026 12:29:48 +0530 Subject: [PATCH] :fix: improve error handling with proper error utils --- bun.lockb | Bin 279694 -> 279694 bytes src/app/(bluerpint-list)/BlueprintList.tsx | 3 +- src/app/[id]/ConnectEmails.tsx | 5 +- src/app/[id]/error.tsx | 43 ++++++++++++ src/app/[id]/page.tsx | 49 ++++++++++++-- src/app/[id]/store.ts | 47 +++++++++++++ src/app/[id]/versions/VersionCard.tsx | 5 +- src/app/[id]/versions/page.tsx | 3 +- src/app/create/[id]/page.tsx | 44 +++++++++++- src/app/create/error.tsx | 43 ++++++++++++ src/app/error.tsx | 44 ++++++++++++ src/lib/analytics.ts | 33 +++++++++ src/lib/errors.ts | 75 +++++++++++++++++++++ 13 files changed, 381 insertions(+), 13 deletions(-) create mode 100644 src/app/[id]/error.tsx create mode 100644 src/app/create/error.tsx create mode 100644 src/app/error.tsx create mode 100644 src/lib/analytics.ts create mode 100644 src/lib/errors.ts diff --git a/bun.lockb b/bun.lockb index fba2c486b50c49af6529af7a6784ab28b9f3f0bd..ad164f448de0cdc2d5e99b2177783349be8f1a69 100755 GIT binary patch delta 170 zcmV;b09F5vi4l&85s)q*JA}iwAe&;AB;Af~nF^KF{CKBrxCV~(ie#02uWzx z)2YDuly4}usLM- ) : error ? ( -
Error loading more blueprints: {error.message}
+
Error loading more blueprints: {getErrorMessage(error)}
) : !hasMore && blueprints.length > 0 ? (
No more blueprints to load
) : blueprints.length === 0 && !isLoading ? ( diff --git a/src/app/[id]/ConnectEmails.tsx b/src/app/[id]/ConnectEmails.tsx index 159d0390..44bfea64 100644 --- a/src/app/[id]/ConnectEmails.tsx +++ b/src/app/[id]/ConnectEmails.tsx @@ -7,6 +7,7 @@ import useGoogleAuth from '../hooks/useGoogleAuth'; import { toast } from 'react-toastify'; import { findOrCreateDSP } from '../utils'; import { useEmailCacheStore } from '@/lib/stores/useEmailCacheStore'; +import { getErrorMessage } from '@/lib/errors'; const ConnectEmails = () => { const { setFile, setStep, setEmlUploadMode } = useProofStore(); @@ -105,7 +106,7 @@ const ConnectEmails = () => { emailCacheStore.clearCache(); setEmlUploadMode('upload'); }) - .catch((err) => toast.error(err.message ?? err)); + .catch((err) => toast.error(getErrorMessage(err))); } }} id="drag-and-drop-emails" @@ -155,7 +156,7 @@ const ConnectEmails = () => { setEmlUploadMode('upload'); }) .catch((err) => { - toast.error(err.message ?? err); + toast.error(getErrorMessage(err)); }); } }} diff --git a/src/app/[id]/error.tsx b/src/app/[id]/error.tsx new file mode 100644 index 00000000..265b62d7 --- /dev/null +++ b/src/app/[id]/error.tsx @@ -0,0 +1,43 @@ +'use client'; + +import { useEffect } from 'react'; + +export default function BlueprintError({ + error, + reset, +}: { + error: Error & { digest?: string }; + reset: () => void; +}) { + useEffect(() => { + console.error('Blueprint page error:', error); + }, [error]); + + return ( +
+
+

Failed to load blueprint

+

+ There was a problem loading this blueprint. It may not exist or there might be a temporary issue. +

+ {error.digest && ( +

Error ID: {error.digest}

+ )} +
+ + + Back to blueprints + +
+
+
+ ); +} diff --git a/src/app/[id]/page.tsx b/src/app/[id]/page.tsx index d309c284..96fcaf17 100644 --- a/src/app/[id]/page.tsx +++ b/src/app/[id]/page.tsx @@ -1,5 +1,5 @@ 'use client'; -import { use, useEffect } from 'react'; +import { use, useEffect, useRef } from 'react'; import Image from 'next/image'; import { getStatusColorLight, getStatusIcon, getStatusName } from '../utils'; import { Button } from '@/components/ui/button'; @@ -19,6 +19,8 @@ import { Blueprint, Status, ZkFramework } from '@zk-email/sdk'; import { toast } from 'react-toastify'; import { useAuthStore } from '@/lib/stores/useAuthStore'; import { initNoirWasm } from '@/lib/utils'; +import { captureEvent, getClientInfo } from '@/lib/analytics'; +import { isAuthError, isNotFoundError, getErrorMessage } from '@/lib/errors'; const Pattern = ({ params }: { params: Promise<{ id: string }> }) => { const { id } = use(params); @@ -42,6 +44,9 @@ const Pattern = ({ params }: { params: Promise<{ id: string }> }) => { : ['Connect emails', 'Select emails', 'View and verify']; let step = searchParams.get('step') || '0'; + const pageStartRef = useRef(performance.now()); + const stepStartRef = useRef(performance.now()); + const lastStepRef = useRef(step); useEffect(() => { reset(); @@ -59,16 +64,52 @@ const Pattern = ({ params }: { params: Promise<{ id: string }> }) => { } }) .catch((err) => { - if (err.toString().includes('401')) { + if (isAuthError(err)) { clearAuth(); return; } console.error(`Failed to get blueprint with id ${id}: `, err); - toast.error('This blueprint could not be found'); + toast.error(isNotFoundError(err) ? 'This blueprint could not be found' : getErrorMessage(err)); router.push('/'); }); }, []); + useEffect(() => { + const now = performance.now(); + if (lastStepRef.current !== step) { + captureEvent('proof_wizard_step_time_spent', { + step: lastStepRef.current, + step_label: steps[parseInt(lastStepRef.current)], + duration_ms: Math.round(now - stepStartRef.current), + blueprint_id: id, + has_external_inputs: !!blueprint?.props.externalInputs?.length, + ...getClientInfo(), + }); + lastStepRef.current = step; + stepStartRef.current = now; + } + }, [step, id, steps, blueprint]); + + useEffect(() => { + return () => { + const now = performance.now(); + captureEvent('proof_wizard_step_time_spent', { + step: lastStepRef.current, + step_label: steps[parseInt(lastStepRef.current)], + duration_ms: Math.round(now - stepStartRef.current), + blueprint_id: id, + has_external_inputs: !!blueprint?.props.externalInputs?.length, + ...getClientInfo(), + }); + captureEvent('proof_wizard_page_time_spent', { + duration_ms: Math.round(now - pageStartRef.current), + blueprint_id: id, + has_external_inputs: !!blueprint?.props.externalInputs?.length, + ...getClientInfo(), + }); + }; + }, [id, steps, blueprint]); + const onCancelCompilation = async () => { if (!blueprint) return; try { @@ -76,7 +117,7 @@ const Pattern = ({ params }: { params: Promise<{ id: string }> }) => { router.push(`/create/${id}`); } catch (err) { console.error('Failed to cancel blueprint compilation: ', err); - toast.error('Failed to cancel blueprint compilation'); + toast.error(`Failed to cancel blueprint compilation: ${getErrorMessage(err)}`); } }; diff --git a/src/app/[id]/store.ts b/src/app/[id]/store.ts index 7cb45166..29086a66 100644 --- a/src/app/[id]/store.ts +++ b/src/app/[id]/store.ts @@ -15,6 +15,7 @@ import { useEmlStore } from '@/lib/stores/useEmlStore'; import sdk from '@/lib/sdk'; import { initNoirWasm } from '@/lib/utils'; import { get, set } from 'idb-keyval'; +import { captureEvent, getClientInfo } from '@/lib/analytics'; export type Step = '0' | '1' | '2' | '3'; export type EmlUploadMode = 'upload' | 'connect'; @@ -159,6 +160,20 @@ export const useProofStore = create()( // Create prover and generate proof const prover = blueprint.createProver({ isLocal }); + const startTime = + typeof performance !== 'undefined' ? performance.now() : Date.now(); + captureEvent('proof_generation_started', { + is_local: isLocal, + zk_framework: blueprint.props.clientZkFramework, + client_zk_framework: blueprint.props.clientZkFramework, + server_zk_framework: blueprint.props.serverZkFramework, + blueprint_id: blueprint.props.id, + blueprint_slug: blueprint.props.slug, + blueprint_version: blueprint.props.version, + external_inputs_count: externalInputs?.length ?? 0, + eml_upload_mode: get().emlUploadMode, + ...getClientInfo(), + }); let proof: Proof; try { let options: GenerateProofOptions = {}; @@ -167,9 +182,41 @@ export const useProofStore = create()( options.noirWasm = noirWasm; } proof = await prover.generateProof(file, externalInputs || [], options); + const endTime = + typeof performance !== 'undefined' ? performance.now() : Date.now(); + captureEvent('proof_generation_succeeded', { + is_local: isLocal, + zk_framework: blueprint.props.clientZkFramework, + client_zk_framework: blueprint.props.clientZkFramework, + server_zk_framework: blueprint.props.serverZkFramework, + blueprint_id: blueprint.props.id, + blueprint_slug: blueprint.props.slug, + blueprint_version: blueprint.props.version, + external_inputs_count: externalInputs?.length ?? 0, + eml_upload_mode: get().emlUploadMode, + duration_ms: Math.round(endTime - startTime), + proof_id: proof.props.id, + ...getClientInfo(), + }); // save proof.props with blueprint.props.id as proof on useProofEmailStore here } catch (err) { console.error('Failed to generate a proof request'); + const endTime = + typeof performance !== 'undefined' ? performance.now() : Date.now(); + captureEvent('proof_generation_failed', { + is_local: isLocal, + zk_framework: blueprint.props.clientZkFramework, + client_zk_framework: blueprint.props.clientZkFramework, + server_zk_framework: blueprint.props.serverZkFramework, + blueprint_id: blueprint.props.id, + blueprint_slug: blueprint.props.slug, + blueprint_version: blueprint.props.version, + external_inputs_count: externalInputs?.length ?? 0, + eml_upload_mode: get().emlUploadMode, + duration_ms: Math.round(endTime - startTime), + error_message: err instanceof Error ? err.message : String(err), + ...getClientInfo(), + }); throw err; } diff --git a/src/app/[id]/versions/VersionCard.tsx b/src/app/[id]/versions/VersionCard.tsx index a6a04671..b72416bc 100644 --- a/src/app/[id]/versions/VersionCard.tsx +++ b/src/app/[id]/versions/VersionCard.tsx @@ -17,6 +17,7 @@ import { getFileContent } from '@/lib/utils'; import { Step } from '../store'; import DragAndDropFile from '@/app/components/DragAndDropFile'; import { useEmlStore } from '@/lib/stores/useEmlStore'; +import { getErrorMessage } from '@/lib/errors'; interface VersionCardProps { blueprint: Blueprint; @@ -78,7 +79,7 @@ const VersionCard = ({ toast.success('Compilation cancelled'); } catch (err) { console.error('Failed to cancel blueprint compilation: ', err); - toast.error('Failed to cancel blueprint compilation'); + toast.error(`Failed to cancel blueprint compilation: ${getErrorMessage(err)}`); } }; @@ -91,7 +92,7 @@ const VersionCard = ({ } } catch (err) { // TODO: Handle different kind of errors, e.g. per field errors - toast.error('Failed to submit blueprint'); + toast.error(`Failed to submit blueprint: ${getErrorMessage(err)}`); console.error('Failed to submit blueprint: ', err); } finally { setIsSaveDraftLoading(false); diff --git a/src/app/[id]/versions/page.tsx b/src/app/[id]/versions/page.tsx index 143943f1..f8762ee0 100644 --- a/src/app/[id]/versions/page.tsx +++ b/src/app/[id]/versions/page.tsx @@ -12,6 +12,7 @@ import { toast } from 'react-toastify'; import Loader from '@/components/ui/loader'; import { useAuthStore } from '@/lib/stores/useAuthStore'; import { getCombinedBlueprintStatus } from '@/app/utils'; +import { getErrorMessage } from '@/lib/errors'; const VersionsPage = ({ params }: { params: Promise<{ id: string }> }) => { const router = useRouter(); @@ -67,7 +68,7 @@ const VersionsPage = ({ params }: { params: Promise<{ id: string }> }) => { } } catch (err) { console.error('Failed to delete blueprint: ', err); - toast.error('Failed to delete blueprint'); + toast.error(`Failed to delete blueprint: ${getErrorMessage(err)}`); } finally { setIsDeleteBlueprintLoading(false); } diff --git a/src/app/create/[id]/page.tsx b/src/app/create/[id]/page.tsx index 359e885e..199bdca8 100644 --- a/src/app/create/[id]/page.tsx +++ b/src/app/create/[id]/page.tsx @@ -2,7 +2,7 @@ import { useCreateBlueprintStore } from './store'; -import { use, useEffect, useState } from 'react'; +import { use, useEffect, useRef, useState } from 'react'; import { Button } from '@/components/ui/button'; import Image from 'next/image'; import { extractEMLDetails, DecomposedRegex, testBlueprint, parseEmail } from '@zk-email/sdk'; @@ -25,6 +25,8 @@ import { debounce } from '@/app/utils'; import ModalGenerator from '@/components/ModalGenerator'; import { useEmlStore } from '@/lib/stores/useEmlStore'; import Loader from '@/components/ui/loader'; +import { captureEvent, getClientInfo } from '@/lib/analytics'; +import { getErrorMessage } from '@/lib/errors'; type Step = '0' | '1' | '2'; @@ -86,6 +88,9 @@ const CreateBlueprint = ({ params }: { params: Promise<{ id: string }> }) => { const searchParams = useSearchParams(); let step = searchParams.get('step') || '0'; + const pageStartRef = useRef(performance.now()); + const stepStartRef = useRef(performance.now()); + const lastStepRef = useRef(step); const setStep = (step: Step, id?: string) => { if (id) { @@ -107,6 +112,39 @@ const CreateBlueprint = ({ params }: { params: Promise<{ id: string }> }) => { }; }, [optOut]); + useEffect(() => { + const now = performance.now(); + if (lastStepRef.current !== step) { + captureEvent('create_blueprint_step_time_spent', { + step: lastStepRef.current, + step_label: steps[parseInt(lastStepRef.current)], + duration_ms: Math.round(now - stepStartRef.current), + blueprint_id: id, + ...getClientInfo(), + }); + lastStepRef.current = step; + stepStartRef.current = now; + } + }, [step, id, steps]); + + useEffect(() => { + return () => { + const now = performance.now(); + captureEvent('create_blueprint_step_time_spent', { + step: lastStepRef.current, + step_label: steps[parseInt(lastStepRef.current)], + duration_ms: Math.round(now - stepStartRef.current), + blueprint_id: id, + ...getClientInfo(), + }); + captureEvent('create_blueprint_page_time_spent', { + duration_ms: Math.round(now - pageStartRef.current), + blueprint_id: id, + ...getClientInfo(), + }); + }; + }, [id, steps]); + useEffect(() => { if (savedEmls[id]) { const parser = new PostalMime(); @@ -133,7 +171,7 @@ const CreateBlueprint = ({ params }: { params: Promise<{ id: string }> }) => { setHasLoadedBlueprint(true); } catch (err) { console.error('Failed to load blueprint:', err); - toast.error('Failed to load blueprint'); + toast.error(getErrorMessage(err)); } finally { setIsBlueprintLoading(false); } @@ -183,7 +221,7 @@ const CreateBlueprint = ({ params }: { params: Promise<{ id: string }> }) => { router.push(`/${blueprintId}`); } catch (error) { console.error('Failed to compile:', error); - toast.error(`Failed to compile blueprint: ${error?.toString()?.replace('Error: ', '')}`); + toast.error(`Failed to compile blueprint: ${getErrorMessage(error)}`); } finally { setIsCompileLoading(false); } diff --git a/src/app/create/error.tsx b/src/app/create/error.tsx new file mode 100644 index 00000000..81b8bc9b --- /dev/null +++ b/src/app/create/error.tsx @@ -0,0 +1,43 @@ +'use client'; + +import { useEffect } from 'react'; + +export default function CreateError({ + error, + reset, +}: { + error: Error & { digest?: string }; + reset: () => void; +}) { + useEffect(() => { + console.error('Create blueprint error:', error); + }, [error]); + + return ( +
+
+

Failed to load blueprint editor

+

+ There was a problem loading the blueprint creation page. Please try again. +

+ {error.digest && ( +

Error ID: {error.digest}

+ )} +
+ + + Back to blueprints + +
+
+
+ ); +} diff --git a/src/app/error.tsx b/src/app/error.tsx new file mode 100644 index 00000000..718f41e8 --- /dev/null +++ b/src/app/error.tsx @@ -0,0 +1,44 @@ +'use client'; + +import { useEffect } from 'react'; + +export default function Error({ + error, + reset, +}: { + error: Error & { digest?: string }; + reset: () => void; +}) { + useEffect(() => { + // Log the error to console for debugging + console.error('Application error:', error); + }, [error]); + + return ( +
+
+

Something went wrong

+

+ An unexpected error occurred. Please try again or contact support if the problem persists. +

+ {error.digest && ( +

Error ID: {error.digest}

+ )} +
+ + + Go home + +
+
+
+ ); +} diff --git a/src/lib/analytics.ts b/src/lib/analytics.ts new file mode 100644 index 00000000..46f9134a --- /dev/null +++ b/src/lib/analytics.ts @@ -0,0 +1,33 @@ +import posthog from 'posthog-js'; + +export type AnalyticsProps = Record; + +export function isAnalyticsOptedOut(): boolean { + if (typeof window === 'undefined') return true; + return window.localStorage.getItem('optOut') === 'true'; +} + +export function getClientInfo(): AnalyticsProps { + if (typeof window === 'undefined') return {}; + const nav = navigator as Navigator & { + deviceMemory?: number; + connection?: { effectiveType?: string }; + }; + + return { + client_user_agent: nav.userAgent, + client_platform: nav.platform, + client_device_memory_gb: nav.deviceMemory, + client_hardware_concurrency: nav.hardwareConcurrency, + client_connection_type: nav.connection?.effectiveType, + client_screen_width: window.screen?.width, + client_screen_height: window.screen?.height, + }; +} + +export function captureEvent(eventName: string, properties: AnalyticsProps = {}) { + if (typeof window === 'undefined') return; + if (isAnalyticsOptedOut()) return; + posthog.capture(eventName, properties); +} + diff --git a/src/lib/errors.ts b/src/lib/errors.ts new file mode 100644 index 00000000..b38343b9 --- /dev/null +++ b/src/lib/errors.ts @@ -0,0 +1,75 @@ +import { ApiError, ErrorCode } from '@zk-email/sdk'; + +/** + * Get a user-friendly error message from an error + */ +export function getErrorMessage(error: unknown): string { + if (error instanceof ApiError) { + // Return specific messages based on error code + switch (error.code) { + case ErrorCode.NOT_FOUND: + case ErrorCode.BLUEPRINT_NOT_FOUND: + return 'The requested resource was not found'; + case ErrorCode.PROOF_NOT_FOUND: + return 'The proof was not found'; + case ErrorCode.UNAUTHORIZED: + return 'Please sign in to continue'; + case ErrorCode.FORBIDDEN: + return 'You do not have permission to perform this action'; + case ErrorCode.INVALID_BODY: + case ErrorCode.INVALID_REQUEST: + return error.message || 'Invalid request'; + case ErrorCode.ALREADY_EXISTS: + return 'This resource already exists'; + case ErrorCode.INTERNAL_ERROR: + return 'An unexpected error occurred. Please try again later.'; + default: + return error.message; + } + } + + if (error instanceof Error) { + return error.message; + } + + if (typeof error === 'string') { + return error; + } + + return 'An unexpected error occurred'; +} + +/** + * Check if an error is a specific API error code + */ +export function isErrorCode(error: unknown, code: string): boolean { + return error instanceof ApiError && error.code === code; +} + +/** + * Check if error is a "not found" error (404) + */ +export function isNotFoundError(error: unknown): boolean { + return error instanceof ApiError && error.isNotFoundError(); +} + +/** + * Check if error is an authentication error (401/403) + */ +export function isAuthError(error: unknown): boolean { + return error instanceof ApiError && error.isAuthError(); +} + +/** + * Check if error is a validation error (400) + */ +export function isValidationError(error: unknown): boolean { + return error instanceof ApiError && error.isValidationError(); +} + +/** + * Check if error is a server error (5xx) + */ +export function isServerError(error: unknown): boolean { + return error instanceof ApiError && error.isServerError(); +}