Error Tracking & Observability

NMT has errors that can originate from four different layers — frontend, backend, Gelato relay, and on-chain contracts. Understanding how errors surface (and don't surface) across these layers is important for debugging.

Error Flow Across Layers

A single user action (e.g., "deposit into Aave") can fail at any point in this chain:

Frontend                    Backend / Gelato              On-Chain
────────                    ──────────────               ────────
1. User clicks
   "Deposit"
       │
2. Simulate tx
   (dry run)
       │ fail? → show error inline
       │
3. Sign EIP-712
       │
4. Submit to ──────────►  5. Gelato receives
   Gelato Relay               task
       │                       │
6. Poll task  ◄────────    7. Gelato simulates ──────► 8. Contract executes
   status                      │ fail? → Cancelled          │ revert? → ExecReverted
       │                       │ pass? → broadcasts          │ pass? → ExecSuccess
       │                       │                             │
9. Show result
   to user

The challenge: errors at step 7 or 8 show up as a generic "Gelato task failed" in step 6 unless you dig into the task's lastCheckMessage.

Layer-by-Layer Status

Frontend — Sentry (Partial)

Client-side Sentry is active. Server-side and edge are configured but disabled.

Layer Status Notes
Client (browser) Active 10% trace sampling, production only
Server (Node.js) Disabled sentry.server.config.ts is commented out
Edge (middleware) Disabled sentry.edge.config.ts is commented out

What gets tracked automatically:

  • Unhandled errors via global-error.tsx error boundary
  • Page navigations and web vitals (LCP, FID, CLS)

What doesn't get tracked:

  • DeFi transaction steps (sign, simulate, relay, poll) — no Sentry spans
  • API route performance — server-side Sentry is off
  • Transaction-specific context (Safe address, task ID, delegate)

Config files:

File Purpose
frontend-app/src/instrumentation-client.ts Client-side Sentry init (active)
sentry.server.config.ts Server-side Sentry init (disabled)
sentry.edge.config.ts Edge runtime Sentry init (disabled)
frontend-app/src/app/global-error.tsx Global error boundary

Backend — Tracing Only (No Sentry)

The backend has structured logging via the tracing crate but Sentry is not initialized despite being a dependency.

What works:

  • Structured logging with tracing-subscriber and environment-based filtering
  • Correlation IDs — every request gets x-correlation-id and x-request-id headers, attached to tracing spans
  • Request-level context: method, URI, status, duration logged per request

What doesn't work:

  • Sentry is in Cargo.toml but never initialized — errors only go to stdout/logs
  • Worker errors (subscription renewals) are logged locally but not reported to any monitoring service
  • No alerts when error rates spike

Gelato — Poll-Based (No Persistence)

When a Gelato task fails, the error reason is in the lastCheckMessage field of the task status response. Both frontend and backend poll for this.

What works:

  • Frontend polls task status and shows errors to the user
  • Backend logs task failures with state and lastCheckMessage
  • Failed subscription renewals are stored in subscription_transaction with is_successful = false

What's missing:

  • Gelato task IDs are not stored in the database — if polling is interrupted, the task is lost
  • lastCheckMessage is logged but not persisted for later analysis
  • No correlation between Gelato task ID and the originating user action

Smart Contracts — Custom Errors

Contracts use Solidity custom errors for gas efficiency and clarity:

PermissionDenied          — delegate lacks required permission
CallExecutionFailed       — external protocol call reverted
ZeroCallsArr              — empty call batch
InvalidDelegateSignature  — EIP-712 signature doesn't match
SignatureExpired           — deadline has passed

How they surface:

  • The frontend decodes contract reverts using viem's ContractFunctionRevertedError
  • The bundler service specifically handles SimulationResult errors to extract per-call success/failure
  • Beyond error name and args, there's limited context

Pre-Submission Simulation

The system simulates transactions before broadcasting to catch failures early. This is the most effective error prevention mechanism.

Frontend Simulation

The bundler service calls simulateExecuteAndRevert() on the DelegateBundler contract before submitting to Gelato:

simulateExecuteAndRevert(delegate, deadline, callsArr, signature)
    → reverts with SimulationResult(bool[] successArr)
    → frontend decodes which calls would fail
    → if any fail, transaction is blocked before submission

This catches permission errors, insufficient balances, and protocol-specific reverts before spending any gas.

Other services (subscription manager, DeFi actions) use viem's simulateContract() for the same purpose — dry-run the call and check for reverts.

Backend Simulation

The backend does not simulate independently. It relies on:

  1. canPayForSubscription() — a read-only check before submitting
  2. Gelato's own simulation — Gelato simulates every sponsored call before broadcasting

Gap: if on-chain state changes between the canPay check and Gelato's simulation, the task gets cancelled.

Correlating Errors Across Layers

The backend generates correlation IDs, but they're not fully wired through the system:

Identifier Generated By Available In Persisted?
x-correlation-id Backend middleware Request/response headers No
x-request-id Backend middleware Tracing spans No
Gelato task ID Gelato API Frontend polling, backend polling Only for subscription txs (indirectly via tx_hash)
Transaction hash Blockchain Stored in subscription_transaction Yes
Safe address User All layers Yes

What you can correlate today: Safe address is the primary key that links frontend actions → backend renewals → on-chain state. Transaction hashes link database records to on-chain transactions.

What you can't easily correlate: A frontend error to a specific backend log entry, or a Gelato task failure to the API request that triggered it.

Current Gaps

Active work
Improving error tracking and observability is an active workstream. The gaps below are known and being addressed incrementally.
Gap Impact Affected Layer Status
Backend Sentry not initialized Worker errors (subscription failures) go to logs only — no alerts, no dashboard Backend Being addressed
Server-side Sentry disabled API route errors and Next.js server actions not reported Frontend Being addressed
Gelato task IDs not stored Can't investigate failed tasks after the fact Backend + Frontend Known
lastCheckMessage not persisted Gelato's specific failure reason is lost after polling Backend Known
No transaction-level Sentry spans Can't trace sign → simulate → relay → poll as a single operation Frontend Being addressed
Console logging in frontend Transaction errors go to browser console, not Sentry Frontend Being addressed
No error rate alerting Spikes in subscription failures or transaction errors go unnoticed All Known

Key Files

Component Location
Sentry client init frontend-app/src/instrumentation-client.ts
Sentry server init (disabled) frontend-app/sentry.server.config.ts
Global error boundary frontend-app/src/app/global-error.tsx
Backend tracing setup backend/apps/server/src/main.rs
Correlation ID middleware backend/crates/api/src/middleware/correlation_id.rs
Request ID middleware backend/crates/api/src/middleware/request_id.rs
Backend API error types backend/crates/api/src/error.rs
Bundler simulation frontend-app/src/features/delegations/services/bundler/service.ts
Gelato task polling (backend) backend/crates/workers/src/subscription/service.rs
Gelato task polling (frontend) frontend-app/src/features/delegations/services/sponsored-tx-builder.ts

results matching ""

    No results matching ""