1
2 Comments

My auto-follow-up feature silently did nothing — because the ID it needed didn't exist yet

My headline feature — automatic follow-ups in the same email thread — was quietly a no-op for every brand-new email. It took me a while to see why, because nothing errored. It just... didn't follow up.

Here's the trap.

To send a follow-up in the SAME conversation, I need Gmail's thread ID. My extension intercepts the email at send time and grabs what it can. Problem: at that exact moment, Gmail hasn't assigned a thread ID yet — it's assigned after the message is actually delivered. So for a fresh email, the thread ID I captured was empty.

My old logic saw an empty thread ID and permanently skipped that email with reason "no_thread." Which means: the feature worked fine in tests (where I fed it a thread ID) and was dead on arrival for every real first email a user sends. The worst kind of bug — green tests, broken product.

The fix had to respect a hard privacy constraint, which made it interesting. EmailKnow only holds the gmail.metadata scope — read email HEADERS, never bodies. And that scope explicitly forbids two things: the q search parameter and reading message content. So I couldn't just search "find the message I just sent."

So the backend backfills the thread ID the honest, allowed way, on the Cron that runs every 17 minutes:

  • Take emails that still have no thread ID, aren't replied yet, still have a pending follow-up, and were sent within the last 24h.
  • List the SENT mailbox with labelIds=SENT and format=metadata (headers only — no q param, no body, ever).
  • Match by recipient + subject + a time window. If several match, take the one closest to the send moment.
  • Write the thread ID back (idempotently: only where it's still null).

And the key behavior change: an email that's due for follow-up but still has no thread ID no longer gets thrown away as "no_thread." It stays pending and retries next cycle, until it either resolves or ages out of the 24h window. Failing safe, not failing silent.

The lesson I keep relearning: a test that passes because YOU supplied the perfect input isn't testing the real world. The real world hands you an empty string at the worst possible moment.

What's your best "green tests, dead feature" story?

— building EmailKnow in public, #3

on August 21, 2026
  1. 1

    This is a perfect example of why your measurement system determines what problems you can actually find. Silent failures are the worst kind - the system is broken but your metrics don't tell you. You see "feature shipped" and move on.

    The code ran. No errors logged. No crash. Your monitoring said everything was fine. But your actual outcome was zero. That's the gap between what you measure and what's actually happening.

    This is why teams with good observability move faster - they measure "what did the user actually experience" not just "did the code run." Silent failures live in that gap. They're the reason your iteration speed feels slower than it should be. You shipped it weeks ago and only found out now.

    The fix is measurement clarity before you ship. "This feature triggers when X arrives. Did X arrive? If X arrived, did the feature execute? What was the outcome?" That's three questions your future self needs answered immediately, not three weeks later.

    1. 1

      Exactly. I was measuring whether the cron ran and whether anything threw, not whether a follow-up actually landed in the right thread. Your three questions are basically the observability spec I should've written before the feature spec.