WooCommerce 10.9 puts email logging in core: every transactional email gets a line under the source transactional-emails in the usual wc-logs location, marked sent, failed, disabled or skipped. When a send dies, the line gets the real PHPMailer error string. Job Thomas announced it on the WooCommerce developer blog, and the motivation checks out, since their email troubleshooting doc alone gets walked through in over 1% of WooCommerce support interactions. My reaction is relief with a caveat. I’ve installed a logging plugin for exactly this job on nearly every store I’ve handed over, and it’s one more plugin a client can delete during a cleanup.
What WooCommerce email logging writes
A new EmailLogger class listens on four hooks: woocommerce_email_sent, woocommerce_email_disabled, woocommerce_email_skipped, and wp_mail_failed for the low-level reason, such as a failed SMTP connect(). Successful sends log at INFO, failures at WARNING, and disabled or skipped sends at NOTICE, so a store with the threshold at error and above only records failures, which is what I want in production. Everything goes through wc_get_logger(), so the existing handler, retention and threshold settings under WooCommerce > Status > Logs apply. There’s no new storage layer.
Order pages get breadcrumbs too. Open an order and you see the emails Woo attempted for it and whether they arrived. Disabled and skipped sends stay out on purpose, to keep the notes lean. The recipient field holds a username or guest rather than an address, and PHPMailer strings get scrubbed by regex, so a log is safe to paste into a support ticket. Between this and the WordPress 6.9 email reliability work, diagnosing mail on client stores is getting easier.
Plugin, hand-rolled hook, or core
WP Mail Logging and similar plugins capture every wp_mail() call, with full headers, the rendered HTML and a resend button. For debugging a broken template on staging that’s still the fastest tool, because you can read the actual email and send it to yourself again.
The hand-rolled option is a few lines on wp_mail_failed that write the WP_Error message to a file. I’ve used it as a stopgap. It covers the whole site, which core logging does not, but you get no structure, no retention and no admin UI.
Core logging sits between the two. It’s structured, respects your log settings, and adds the order-level view, but it only sees sends that go through WC_Email. A membership plugin or an invoicing extension calling wp_mail() directly stays invisible to it. The team also flags a race: wp_mail_failed is a global hook, so another plugin’s failure inside the window around a Woo send can land on the wrong line. Only the readable reason can drift; email_type and status stay accurate. I doubt it matters much on a store where Woo is the only thing mailing.
So it depends on the site. On staging, or with a template bug, use a full logging plugin. If third-party plugins mail directly, keep the small wp_mail_failed listener. For a live store in steady state, core logging is enough, and it’s already there.
If you’re not sure which case a store falls into, this is the sort of check I do on maintenance visits: find out what’s mailing what, then set the logging to match.
The new logging filters
10.9 adds two filters. woocommerce_email_log_enabled controls whether an email is logged, including per type, and woocommerce_email_log_context hands you the context array before it’s written. Per-type is the one I’d use, for instance to silence customer invoice lines on a store that already tracks those in an ERP:
add_filter( 'woocommerce_email_log_enabled', function ( $enabled, $email_id ) {
return 'customer_invoice' === $email_id ? false : $enabled;
}, 10, 2 );
The global off switch lives under WooCommerce > Status > Logs in the Settings tab, next to the retention controls it already had.
This week on client stores
I’m setting the threshold to warning, confirming the retention window, and leaving core logging on. A full logging plugin stays in reserve for the day someone needs the rendered HTML, and it comes off the site when that session ends.