Appearance
Hooks & filters
The plugin's public surface is intentionally small: one action for writing entries, and four filters for shaping behavior. Everything is prefixed order_trail.
Action: order_trail
Write a custom audit note through the plugin's pipeline (prefix applied, no actor suffix — compose your own text):
php
do_action( 'order_trail', $order_id, 'Contract #42 attached' );The message may contain safe HTML such as links. If the plugin is inactive the action is simply a no-op, so calling code needs no class_exists guard (though it may want a fallback if the note is essential).
Filter: order_trail_note_prefix
Prefix prepended to every note the plugin writes. Default: '' (no prefix).
php
add_filter( 'order_trail_note_prefix', function () {
return '[Audit] ';
} );Filter: order_trail_ignored_props
Order fields excluded from change logging. Receives the default list and the order being saved:
php
add_filter( 'order_trail_ignored_props', function ( $props, $order ) {
$props[] = 'transaction_id';
return $props;
}, 10, 2 );Removing entries from the list works too — for example, dropping 'status' would add a field-diff line for status changes (normally left entirely to WooCommerce's own note).
Filter: order_trail_watched_meta
Order meta keys to diff and log on save. Empty by default — meta logging is strictly opt-in, because meta payloads are unbounded and most keys are machine state, not audit material. Entries may be plain keys (shown verbatim in the note) or key => label pairs (the label is shown instead):
php
add_filter( 'order_trail_watched_meta', function ( $keys, $order ) {
$keys[] = '_delivery_date'; // note shows the key verbatim
$keys['_po_number'] = 'PO Number'; // note shows the readable label
return $keys;
}, 10, 2 );Order updated - _delivery_date: 2026-08-01 -> 2026-08-15 [by mike]
Order updated - PO Number: PO-1001 -> PO-2002 [by mike]Only changes made through the order object and saved with it are seen ($order->update_meta_data() + $order->save(), which is also what admin and REST edits use). Low-level writes that bypass the order CRUD — direct update_post_meta() calls or raw SQL — are not caught.
Filter: order_trail_email_notes
Master switch for the built-in order-email notes. Default true. Return false when another email logger writes richer notes, to avoid duplicates:
php
add_filter( 'order_trail_email_notes', '__return_false' );Filter: order_trail_email_note
Adjust the email note text before it is written. Receives the message, order, WooCommerce email ID, and the WC_Email instance:
php
add_filter( 'order_trail_email_note', function ( $message, $order, $email_id, $email ) {
return $message . ' (' . $email_id . ')';
}, 10, 4 );Integrating a custom email logger
A site that stores full email copies (in a custom post type, for example) can take over email notes entirely while keeping them in the audit trail — disable the built-in notes and write richer ones through the action:
php
// In the email logger, once the copy is stored:
add_filter( 'order_trail_email_notes', '__return_false' );
$note = sprintf(
'Email sent to %s: "%s" <a href="%s">View email</a>',
esc_html( $recipient ),
esc_html( $subject ),
esc_url( $view_url )
);
do_action( 'order_trail', $order_id, $note );The dependency points from the site to the plugin — the plugin never needs to know the site's code exists.
PHP API
Two public methods complement the hooks:
| Method | Purpose |
|---|---|
Order_Trail::reset_request_state() | Clear per-request suppression sets in long-running processes (queue workers, test suites). |
Order_Trail::log_email_sent( $return, $email_id, $email ) | The woocommerce_email_sent handler, callable directly for custom mailers that mimic WC_Email. |