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.tsxerror 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-subscriberand environment-based filtering - Correlation IDs — every request gets
x-correlation-idandx-request-idheaders, attached to tracing spans - Request-level context: method, URI, status, duration logged per request
What doesn't work:
- Sentry is in
Cargo.tomlbut 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_transactionwithis_successful = false
What's missing:
- Gelato task IDs are not stored in the database — if polling is interrupted, the task is lost
lastCheckMessageis 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
SimulationResulterrors 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:
canPayForSubscription()— a read-only check before submitting- 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
| 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 |