Raise runtime failures to Sentry
Sentry's tracing layer turns error events into issues and keeps warn
and below as breadcrumbs. Failed polls and server-refused filings,
moves, and deletions logged at warn, and 500s weren't logged at all,
so none of them ever reached Sentry. A dead token would fail the poll
every 30s without anyone hearing about it.

All of these now log at error. JMAP call rejections and dropped or
held writes stay at warn: they show up as breadcrumbs on the poll
failure they lead to, or they are races and ACLs working as intended.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
change mnrlwtktwxsrtvyvttoowutnqwvrplxu
commit 1f58df8b0efc244626253f8f693335b2bb230a4e
author Alpha Chen <alpha@kejadlen.dev>
date
parent qzxpplnw
diff --git a/src/bin/docket/poll.rs b/src/bin/docket/poll.rs
index c7d1a6a..63dc448 100644
--- a/src/bin/docket/poll.rs
+++ b/src/bin/docket/poll.rs
@@ -13,7 +13,8 @@ use docket::store::Store;
 const INTERVAL: Duration = Duration::from_secs(30);
 
 /// Runs the loop in the background. Errors are logged and retried on
-/// the next tick — a blip must not take syncing down with it.
+/// the next tick — a blip must not take syncing down with it — at
+/// error level, so each failure reaches Sentry.
 pub fn spawn(client: Client, credential: Credential, store: Store, sync: Sync) {
     tokio::spawn(async move {
         let name = credential.name.clone();
@@ -28,7 +29,7 @@ pub fn spawn(client: Client, credential: Credential, store: Store, sync: Sync) {
                     tracing::info!(credential = %name, ?counts, "poll");
                 }
                 Err(err) => {
-                    tracing::warn!(credential = %name, %err, "poll failed; retrying next tick");
+                    tracing::error!(credential = %name, %err, "poll failed; retrying next tick");
                 }
             }
         }
diff --git a/src/error.rs b/src/error.rs
index fae5fcf..31760b4 100644
--- a/src/error.rs
+++ b/src/error.rs
@@ -42,6 +42,11 @@ impl IntoResponse for Error {
             Error::Forbidden(_) => StatusCode::FORBIDDEN,
             Error::BadRequest(_) => StatusCode::BAD_REQUEST,
         };
+        // A 500 is Docket's fault, so it goes to Sentry as an issue;
+        // the rest are the request's.
+        if status.is_server_error() {
+            tracing::error!(err = %self, "request failed");
+        }
         (status, self.to_string()).into_response()
     }
 }
diff --git a/src/jmap.rs b/src/jmap.rs
index 1b05c72..f6c920a 100644
--- a/src/jmap.rs
+++ b/src/jmap.rs
@@ -1567,7 +1567,7 @@ impl Client {
                 .set_emails(&session.api_url, token, account_id, &update)
                 .await?;
             for (id, error) in &reply.not_updated {
-                tracing::warn!(jmap_id = %id, %error, "the server refused a filing");
+                tracing::error!(jmap_id = %id, %error, "the server refused a filing");
             }
             filed = filed.saturating_add(reply.updated.len());
             // Refused ids clear too: the server's verdict stands, and
@@ -1652,7 +1652,7 @@ impl Client {
                 .set_emails(&session.api_url, token, account_id, &update)
                 .await?;
             for (id, error) in &reply.not_updated {
-                tracing::warn!(jmap_id = %id, %error, "the server refused a move");
+                tracing::error!(jmap_id = %id, %error, "the server refused a move");
             }
             moved = moved.saturating_add(reply.updated.len());
             // Refused ids clear too: the server's verdict stands, and
@@ -1693,7 +1693,7 @@ impl Client {
                 .destroy_emails(&session.api_url, token, account_id, chunk)
                 .await?;
             for (id, error) in &reply.not_destroyed {
-                tracing::warn!(jmap_id = %id, %error, "the server refused a deletion");
+                tracing::error!(jmap_id = %id, %error, "the server refused a deletion");
             }
             deleted = deleted.saturating_add(reply.destroyed.len());
             // Refused ids clear too: the server's verdict stands, and