Skip to main content

Documentation Index

Fetch the complete documentation index at: https://grantmaster.dev/llms.txt

Use this file to discover all available pages before exploring further.

Error Handling & Logging

StatusUpdatedCovered Files
🟢 Stable2026-04-05src/core/errorHandler.ts, src/core/BaseService.ts, packages/shared/src/errors/

Standard Error Pattern: ServiceResult

GrantMaster avoids throwing raw exceptions in business logic. Instead, we use the IServiceResult<T> pattern to return a predictable response.

Structure

interface IServiceResult<T> {
  success: boolean;
  data?: T;
  error?: {
    code: string;        // e.g., 'PERMISSION_DENIED'
    message: string;     // Human readable for devs
    details?: any;       // Raw error or context
    severity: 'low' | 'medium' | 'high' | 'critical';
  };
}

Usage in Services

Use the withServiceResult wrapper in BaseService to automatically catch errors and log them.
async myAction() {
  return this.withServiceResult(async () => {
    // If this throws, it is caught and wrapped
    const data = await someAsyncWork();
    return data;
  }, { operation: 'myAction' });
}

Error Classifications

CodeTypeAction
VALIDATION_ERRORUserShow field errors in UI.
PERMISSION_DENIEDSecurityRedirect or show “Access Denied” toast.
NOT_FOUNDDataShow 404 or empty state.
INTERNAL_ERRORSystemAlert Sentry and show “Something went wrong”.
TENANT_MISMATCHCriticalImmediate session termination + High-priority log.

Logging & Monitoring

1. Application Logs (Structured Logger)

We use a structured logger (src/lib/logger.ts, re-exported via src/utils/logger.ts) which:
  • Emits structured logs to Cloud Logging (server) or console (client)
  • Adds Sentry breadcrumbs for info/warn
  • Captures exceptions in logger.error()
Log levels:
  • Debug: Only in development.
  • Info: Major business events (Project started, user joined).
  • Warn: Recoverable issues (also breadcrumbs; optionally captureMessage when explicitly requested).
  • Error/Fatal: Caught exceptions.

2. Audit Logging (Firestore)

Critical business changes are persisted to the systemEvents collection via BaseService.logSuccess.
  • Retention: Permanent (compliance requirement).
  • Fields: userId, organizationId, action, payload, timestamp.

3. Sentry Integration

Frontend Sentry is initialized in src/index.tsx and catches:
  • React render crashes (via error boundaries)
  • Manual reports via logger.error() and handleError()
  • Performance warnings via Sentry.captureMessage() where explicitly used
Cloud Functions initialize Sentry via functions/src/core/sentry.ts (imported by functions/src/index.ts).

Error Code Catalog

ErrorSeverity Enum

Defined in packages/shared/src/errors/AppError.ts. Determines routing and alerting thresholds.
ValueMeaningSentry LevelToast Type
INFOInformational, no action neededinfoinfo
WARNINGMay need attention, not criticalwarningwarning
ERRORPrevents operation, recoverableerrorerror
CRITICALRequires immediate attentionfatalerror

ErrorCategory Enum

Defined in packages/shared/src/errors/AppError.ts. Used for grouping, filtering, and Sentry fingerprinting.
CategoryValueDescription
VALIDATIONvalidationInput/form validation failures
AUTHENTICATIONauthenticationLogin failures, expired/invalid tokens
AUTHORIZATIONauthorizationPermission denied, insufficient role
NOT_FOUNDnot_foundRequested resource does not exist
NETWORKnetworkFetch/connection failures
FIRESTOREfirestoreFirestore read/write/query failures
STORAGEstorageFirebase Storage upload/download failures
RATE_LIMITrate_limitAPI throttling, quota exceeded
BUSINESS_LOGICbusiness_logicDomain-specific validation (e.g., budget exceeded)
EXTERNAL_APIexternal_apiThird-party API failures (Stripe, Postmark, HubSpot)
AI_SERVICEai_serviceGemini/Genkit/embedding failures
EMAIL_SERVICEemail_servicePostmark send/template failures
UNKNOWNunknownCatch-all for unrecognized errors

Error Class Hierarchy

All classes live in packages/shared/src/errors/ and are re-exported from src/errors/. The hierarchy has two branches below AppError: UserError (displayed to users, severity WARNING) and SystemError (generic “something went wrong” message, severity ERROR).
AppError (abstract)
├── UserError
│   ├── ValidationError          — Input validation failures
│   ├── AuthorizationError       — Permission denied (userMessage: "You do not have permission…")
│   ├── NotFoundError            — Resource not found (accepts resource name + id)
│   ├── RateLimitError           — API throttling (accepts optional retryAfter seconds)
│   └── BusinessLogicError       — Domain rule violations (userMessage = message)

└── SystemError
    ├── AuthenticationError      — Login/token failures
    ├── FirestoreError           — Firestore operation failures (accepts operation + original error)
    ├── StorageError             — Firebase Storage failures (accepts operation + original error)
    ├── NetworkError             — Fetch/connectivity failures
    ├── ExternalAPIError         — Third-party API failures (accepts service name + original error)
    ├── AIServiceError           — Gemini/Genkit failures (accepts operation + original error)
    └── EmailServiceError        — Postmark failures (accepts operation + original error)

Domain-Specific Error Classes

Defined in packages/shared/src/errors/domain.ts. These extend Error directly (not AppError) and carry domain-specific metadata with toJSON() serialization.
ClassFieldsUse Case
RateLimitExceededErrorlimit, used, window, retryAfterQuota enforcement with detailed usage info
TenantIsolationErrorattemptedOrganizationId, userOrganizationId, operation, timestampCritical security error — cross-tenant access attempt
FeatureNotAvailableErrorfeature, currentTier, requiredTier, upgradeUrlSubscription tier gating

Standalone Error Class

ClassLocationUse Case
RuntimeEnvValidationErrorsrc/lib/runtimeEnv.tsThrown at boot when required VITE_* env vars are missing or invalid. Carries an issues: string[] array.

Sentry Integration Details

Frontend (src/lib/sentryBrowser.ts)

Sentry is loaded lazily via dynamic import('@sentry/react') and only in production when VITE_SENTRY_DSN is set. Initialization happens in src/bootstrap.tsx. Configuration:
SettingValue
tracesSampleRate0.1 (10%)
profilesSampleRate0.1 (10%)
replaysSessionSampleRate0.1 (10%)
replaysOnErrorSampleRate1.0 (100% on errors)
sendDefaultPiifalse
environmentimport.meta.env.MODE
release__SENTRY_RELEASE__ (injected at build time)
Data scrubbing: The beforeSend hook replaces Authorization headers with [Scrubbed]. Integrations: browserTracingIntegration, replayIntegration. Exported helpers (all async, no-op when Sentry is not loaded):
FunctionPurpose
captureSentryException(error, context?)Report an error to Sentry
captureSentryMessage(message, context?)Report a message
addSentryBreadcrumb(breadcrumb)Add navigation/action breadcrumb
setSentryBrowserUser(user)Set user context (id, email)
setSentryBrowserTag(key, value)Set global tag
setSentryBrowserContext(key, context)Set structured context
showSentryReportDialog(eventId?)Open user feedback dialog
startSentryInactiveSpan(options)Start a performance span
setSentryMeasurement(name, value, unit)Record a custom measurement
recordSentryDistribution(name, value, options)Record a metrics distribution

Cloud Functions (functions/src/core/sentry.ts)

Uses @sentry/node, also lazy-loaded to avoid blocking Firebase CLI’s function-discovery process. Disabled when FUNCTIONS_EMULATOR=true. Key differences from frontend:
  • Uses getIsolationScope() for per-request tenant/user tagging (organizationId, authType, requestId, userId).
  • withSentryIsolationScope(scope, fn) wraps an async operation with scoped tags.
  • captureException() calls Sentry.flush() (default 2 s timeout) to ensure delivery before the function terminates.
  • Release is set from SENTRY_RELEASE or GITHUB_SHA (uploaded via CI).

What Gets Sent to Sentry (Filtering Rules)

The ErrorHandler.shouldCaptureInSentry() method in src/core/errorHandler.ts applies these filters:
  1. Never in development (import.meta.env.DEV).
  2. Never for INFO or WARNING severity.
  3. Never for user-error categories: VALIDATION, NOT_FOUND, AUTHORIZATION.
  4. Everything else (system errors, Firestore errors, network errors, external API errors, AI service errors) is captured.
Sentry tags applied per error:
  • category — the ErrorCategory value
  • severity — the ErrorSeverity value
  • error_class — the constructor name (e.g., FirestoreError)
Fingerprinting: [error.category, error.message] — groups similar errors together.

Error Boundaries in React

Three error boundary components provide layered crash isolation:

1. SentryErrorBoundary (src/components/SentryErrorBoundary.tsx)

  • Scope: Wraps the entire <App /> in src/bootstrap.tsx — the outermost catch.
  • Behavior: Captures to Sentry, shows full-page “Something went wrong” card with “Reload Application” button and “Report feedback” link (opens Sentry.showReportDialog).
  • Displays: Sentry event ID in footer for support reference.

2. RouteErrorBoundary (src/components/ui/RouteErrorBoundary.tsx)

  • Scope: Wraps individual routes and page sections.
  • Isolation levels: 'route' (full-page fallback, default) or 'section' (compact inline card).
  • Behavior: Captures to Sentry with isolationLevel context. Supports custom fallbackComponent prop. “Try Again” resets state; “Go to Overview” navigates to /overview.
  • Dev mode: Shows error message in a code block.

3. ErrorBoundary (src/components/ui/ErrorBoundary.tsx)

  • Scope: Wraps individual components or widget areas.
  • Features: Accepts context string (sent as Sentry tag errorBoundary), onError callback, onReset handler, optional fallback ReactNode, showDetails toggle.
  • Dev mode: Shows full error details and component stack trace.
  • Recovery: “Try Again” button with repeated-failure warning.

4. WidgetErrorBoundary (inline in src/components/widgets/WidgetDashboardBase.tsx)

  • Scope: Wraps individual dashboard widgets.
  • Purpose: Prevents a single broken widget from crashing the entire dashboard.

Error Handling Patterns by Layer

Service Layer

Services use BaseService.withServiceResult() to wrap async operations. Errors are caught, logged via the service’s structured logger, and returned as { success: false, error: string }. Services do not show toasts or capture to Sentry directly.
// In a service extending BaseService
async archiveProject(id: string): Promise<ServiceResult> {
  return this.withServiceResult(
    async () => {
      await this.update(id, { archived: true });
    },
    { operation: 'archiveProject', entityId: id }
  );
}
For throwing typed errors within services:
import { NotFoundError, BusinessLogicError } from '@grantmaster/shared/errors';

if (!project) throw new NotFoundError('Project', projectId);
if (project.budget <= 0) throw new BusinessLogicError('Budget must be positive');

Hook Layer

Two hooks provide error handling for React components: useErrorHandler (src/hooks/useErrorHandler.ts):
  • handleError(error, context?, options?) — logs, toasts, and captures to Sentry.
  • handleErrorSilent(error, context?) — logs and captures but no toast.
  • withErrorBoundary(fn, context?) — wraps an async function with automatic error handling.
useAsyncAction (src/hooks/useAsyncAction.ts):
  • Wraps an async action with loading state, error state, duplicate-execution prevention, success/error toasts, and automatic handleError integration.
const { execute, isLoading } = useAsyncAction({
  action: () => projectService.archiveProject(id),
  successMessage: 'Project archived',
  errorContext: { operation: 'archiveProject', projectId: id },
});

Component Layer

Components should:
  1. Wrap risky subtrees with <ErrorBoundary context="...">.
  2. Use useErrorHandler() for imperative error handling in event handlers.
  3. Use useAsyncAction() for button-triggered async operations.
  4. Check ServiceResult.success before accessing .data.
function ProjectActions({ projectId }: Props) {
  const { handleError } = useErrorHandler();

  const handleArchive = async () => {
    const result = await projectService.archiveProject(projectId);
    if (!result.success) {
      // Error already logged by service — optionally show custom UI
      return;
    }
    toast.success('Project archived');
  };

  return (
    <ErrorBoundary context="Project Actions">
      <Button onClick={handleArchive}>Archive</Button>
    </ErrorBoundary>
  );
}

tRPC / Cloud Functions Layer

Cloud Functions use functions/src/core/sentry.ts:
  • Wrap request handlers with withSentryIsolationScope(scope, fn) for per-request user/org tagging.
  • Call captureException(error, { scope, extra, tags }) for unrecoverable errors.
  • Sentry flushes before function termination to avoid dropped events.

Maintenance

Update this document when:
  • Adding a new global error code.
  • Adding or removing an error class from the hierarchy.
  • Changing the severity levels.
  • Modifying Sentry filtering rules.
  • Adding a new error boundary.
  • Switching logging providers.