DEV Community

Jonathan
Jonathan

Posted on

Use Artifacts to Debug Email APIs Faster

Email API smoke tests often fail in a boring, expensive way. The request went through, the inbox maybe received something, and the CI log still leaves the team guessing. I see this a lot in GitHub Actions pipelines: the step output says "timeout" and everybody has to reconstruct what the run actually tried to do.

What helped most on teams I support was not another retry loop. It was treating every email-related check like a tiny evidence pipeline. Each run should publish a compact artifact bundle with the request payload, timing, inbox metadata, and the exact message match result. Once we did that, triage got way less annoying and a bit more honest.

If you have already worked through session-bound email change checks or used reproducible email check diffs, this pattern fits right after them. The focus here is speed: when a check fails at 2 AM, the next person should not need to rerun the workflow just to understand the shape of the problem.

Why email API failures take too long to triage

Most flaky email checks are not truly mysterious. They are just under-instrumented.

The workflow sends an API request, waits for a message, and logs a single generic failure. That leaves out the details that matter:

  • which recipient address was used
  • when the app said it queued the email
  • what subject or headers the poller expected
  • whether the inbox was empty, stale, or almost right

Without those details, the post-failure discussion gets weirdly speculative. Someone blames rate limits, someone blames the provider, and someone else starts searching for tempail mail or tepm mail com because the symptom looks familiar. Sometimes that guess is right, but more often the run simply did not save enough evidence.

The artifact bundle that changes the game

The fix is small: create one artifact directory per workflow run and put the same four files in it every time.

  1. request.json with the sanitized API request and correlation id.
  2. poll-result.json with the inbox id, wait duration, and matched message summary.
  3. timeline.txt with human-readable timestamps for trigger, first poll, last poll, and finish.
  4. debug.md with one short explanation of what the workflow expected and what it actually saw.

That bundle is enough to answer most first-pass questions. It also keeps the CI log shorter, which I like a lot. Logs are useful for scanning, but artifacts are better for evidence you may need ten minutes later.

One more benefit: artifact bundles make it obvious when the contract is too fuzzy. If poll-result.json has three possible matches and your script still says "success", thats a product bug in the test helper, not a platform mystery.

A small GitHub Actions pattern

This is the shape I keep coming back to:

- name: Trigger email flow
  run: node scripts/send-check.js > .tmp/request.json

- name: Wait for message
  run: node scripts/wait-for-email.js > .tmp/poll-result.json

- name: Build timeline
  run: node scripts/write-timeline.js > .tmp/timeline.txt

- name: Summarize debug state
  if: always()
  run: node scripts/write-debug-summary.js > .tmp/debug.md

- name: Upload email evidence
  if: always()
  uses: actions/upload-artifact@v4
  with:
    name: email-check-${{ github.run_id }}
    path: .tmp/
Enter fullscreen mode Exit fullscreen mode

There is nothing fancy there, which is the point. The workflow keeps moving fast, but every run leaves behind a receipt. When the check fails, I open the artifact first and the raw log second. That order matters because it avoids chasing noise from unrelated setup steps.

For teams using a burner email in non-production API checks, this also creates a cleaner boundary between "provider issue" and "our matcher logic is too loose." You can inspect the saved poll result and see whether the run never received mail, received the wrong message, or matched something stale. Thats a much better conversation.

What to save for every failing run

I would keep this checklist brutally consistent:

  • the correlation id sent to the app
  • the exact recipient or inbox alias
  • the expected subject fragment
  • the receive-after timestamp used by the poller
  • the first 10 lines of the matched body, if policy allows it
  • the normalized reason code for failure

That last field is the sneaky important one. "Timeout" is too broad. "timeout_no_messages" versus "timeout_only_stale_messages" versus "timeout_subject_mismatch" gives you a useful next action right away. It sounds small, but it saves a suprising amount of time.

I also prefer saving a short Markdown summary because humans read it faster than JSON when they are half-distracted. Two or three sentences are enough:

Expected a verification email within 60s after request `req_8421`.
Inbox received 2 messages, both older than the run start time.
Result: timeout_only_stale_messages.
Enter fullscreen mode Exit fullscreen mode

That is usually the moment the bug stops feeling random.

Q&A

Should artifacts be uploaded on successful runs too?

Usually yes, but keep them small and set a short retention period. Success artifacts help when a failure starts showing up only every few days, and you want one known-good example nearby.

Do I need full email bodies in the artifact?

Not always. Metadata, snippet text, and matcher fields are often enough. Save less by default, then expand only if the team truly needs more detail.

Is this only for GitHub Actions?

Nope. The same idea works in any CI system. GitHub Actions just makes the artifact habit easy, so it is a nice place to start even if your stack is otherwise pretty plain.

Top comments (0)