Writing

Three production bugs Sentry found in its first eight hours

October 8, 2026 · Amit Solanki

In August 2026 I put the first live demo of my restaurant ordering platform on a real server. The platform, now called Platestead, is a Rails and Spree application that takes a restaurant’s online orders, sends them to its point-of-sale system, and books delivery through DoorDash and Uber Direct. The demo served a demonstration restaurant, with no real merchant behind it, but it ran the way a production system does: its own server and database, scheduled background jobs, and live connections to outside services.

Three days after it went live, I added Sentry, an error-monitoring service that records every exception an application raises in production, including the ones no user ever sees. Within eight hours it had reported three bugs. None of them had shown any symptom: every deploy succeeded, every page loaded, every health check passed.

The three don’t share a root cause. What they share is more useful to notice: none of them was going to be caught by the tests a team normally writes, because each one only shows up once the code is running in its real environment. Two of them can’t be represented in a test at all.

Bug one: an email that was never going anywhere

Order #31 on the live demo tried to send its confirmation email. Spree::OrderMailer#confirm_email threw Errno::ECONNREFUSED, connecting to localhost port 25.

The app’s own config/environments/production.rb already supports SMTP via environment variables - a comment says plainly it “Works with any SMTP provider (Resend, Postmark, Mailgun, SendGrid, SES, etc.)” That configuration block only activates when SMTP_HOST is present. It had never been set. Not misconfigured - absent. ActionMailer’s real behavior when SMTP isn’t explicitly configured isn’t an error at boot, or a warning in the logs, or a clear failure the first time it’s exercised in development. It’s a silent fallback to Rails’ default mail delivery settings - an SMTP connection to localhost:25 - which doesn’t exist on this box, or most boxes, and produces a real exception only the instant something actually tries to send an email.

Every deploy, every health check, every page load looked completely fine. The one thing that could have surfaced this was an order actually trying to send an email, and nothing was watching when one did. No test suite would find this either, for a mundane reason: a test environment doesn’t read production’s environment variables, by design. The bug isn’t reachable from inside a test at all; it’s a fact about which environment variable was and wasn’t set on one specific server.

The fix is the boring kind: wire the real values through as deploy secrets. The part worth sitting with is what “boring” actually meant here. The demo had been live for three days before Sentry went in, and in all that time the backend could not have sent a single order confirmation. Nothing reported it until Sentry did.

Bug two: a background job that had never once succeeded

Sentry reported the same NoMethodError firing every hour, eight times in seven hours: undefined method 'prunable' for class SolidCable::Message.

config/recurring.yml scheduled a cleanup task: SolidCable::Message.prunable.in_batches(of: 1000).delete_all, meant to prune old Action Cable messages past their retention window. Reading the actual installed gem - solid_cable 4.0.2, its own app/models/solid_cable/message.rb - the scope is called trimmable. It has never been called prunable, and 4.0.2 is the only version of the gem this project has ever run. Claude wrote that line, early in the build. Later in the same build it also wrote a permission grant broad enough to export the gift card table. Both are the same failure: producing whatever best fits the surrounding shape. prunable is what a pruning scope would be called, in a gem that has scopes like that. It is a plausible name. It has never been the name. Nothing caught it, because nothing was watching a scheduled task’s own success or failure until Sentry was.

The difference is where the two landed. The permission grant was catchable by reading the code, and was caught by reading the code. This one was not. SolidCable::Message.prunable reads as correct unless you go and open the installed gem to check, which nobody does for a cleanup task nobody is watching. It needed production, running, being watched - which is this post’s own argument, arriving a section early.

This is the purest version of the pattern: a recurring background job, running unattended, on a schedule, with zero user-facing surface at all. It doesn’t fail a request. It doesn’t show up in a browser. It doesn’t block a deploy. It just quietly throws, on its own schedule, forever, doing none of the work it was written to do - and the only way anyone would ever find out is something explicitly watching for exceptions that aren’t attached to any request a human is looking at.

A test could have caught this one, in principle: the test environment runs the same gem version as production, so a spec that evaluated every command in recurring.yml would have raised the same error. Nobody writes that spec for a cleanup task, and nobody had. The only thing in a position to notice was something watching production.

Bug three: a library that fails on purpose, quietly, by design

The third one is the strangest, because the code doing the failing isn’t a mistake - it’s working exactly as its authors intended, and that’s the problem.

Spree::Addresses::GeocodeAddressJob fires on every checkout address create or update, calling Geocoder’s default :nominatim provider to resolve latitude/longitude. Nominatim’s usage policy requires a User-Agent header identifying the requester, and none was configured. Re-run against a perfectly valid address, the lookup came back with “Geocoding API’s response was not valid JSON” and returned nil: the service was refusing the request rather than answering it.

Here’s the part that makes this different from the first two bugs: Spree core already anticipated geocoding could fail, and handled it - by catching the error internally and reporting it via Rails.error.report instead of letting it raise. That’s a genuinely reasonable design choice for a non-critical background enrichment step; a failed geocode shouldn’t break checkout. It also means the failure was invisible to literally everything except something explicitly subscribed to Rails’ own error-reporting stream. Verified directly against production: the job had already run sixteen times, across the store’s 27 addresses, and exactly one of those runs showed up in Sentry, because Sentry had only been live for a few hours. Re-running the lookup live failed the same way, which is the only reason to think the other fifteen did too: nothing was watching them.

The fix wasn’t fixing the geocoding - it was noticing that nothing in the app actually reads Spree::Address#latitude/#longitude anywhere. Real delivery-distance and serviceability logic runs entirely through DoorDash Drive and Uber Direct’s own APIs, not Spree’s built-in geocoding. The feature had no consumer. Rather than configure a User-Agent header for a lookup nobody uses, the job gets disabled outright, via Spree’s own first-class Spree::Config[:geocode_addresses] toggle - removing the vestigial feature rather than fixing a config value in service of nothing.

The actual lesson

These three don’t share a root cause, a language feature, or even a category of bug in the usual sense - one’s a missing env var, one’s a misnamed method, one’s a working-as-designed error swallow. What they share is where they live: outside anything the tests a team normally writes ever exercise. A test doesn’t set production’s real environment variables. A test doesn’t run a recurring job on an hourly production schedule for eight real hours. A test doesn’t exercise a third-party library’s own internal Rails.error.report call against a live network dependency’s actual current policy.

Error monitoring doesn’t find the bugs your tests missed. It finds the bugs nobody was ever going to write a test for - because what’s wrong only shows itself in the gap between the code and the specific, real environment it’s actually running in. That gap doesn’t exist until deployment, and neither does any way to observe it, until something is watching production itself.

The checklist

  • Any environment variable a config block only reads conditionally (if ENV["X"].present?) deserves an explicit check that it’s actually set where it needs to be - the silent-fallback failure mode is exactly this shape, and it won’t show up until the feature gated on it is actually used for the first time in that environment.
  • A recurring/scheduled job succeeding at “not crashing the deploy” tells you nothing about whether it’s succeeding at its actual job. If nothing surfaces a scheduled task’s own exceptions, it can fail on every single run, forever, with zero visible symptom.
  • When a library swallows its own errors instead of raising (often a reasonable design choice on its own), that design choice also means your error monitoring is the only thing that can ever tell you it’s failing - confirm you’re actually subscribed to whatever mechanism it reports through (here, Rails.error.report), not just assuming “no exception” means “no problem.”
  • Before fixing a background feature’s broken integration, check whether anything actually consumes its output. Sometimes the right fix is removing the feature, not repairing it.
  • Wire error monitoring in before a feature ships, not after - the geocoding job had run sixteen times, and monitoring saw one of them.

Platestead itself, including what is and isn’t proven yet, is written up in the case study.

Working through something similar?

← All writing