Debugging & Logging
Flavor ships a single, unified logger shared by all three products — the theme, the WP eCommerce Core plugin, and Flavor Core. Everything you log from anywhere lands as structured JSON Lines in wp-content/flavor-logs/, and is read through one screen: Flavor → Debug (the Debug Viewer).
This page is for developers writing code inside the ecosystem — theme modules, plugin services, gateways, ERP listeners, blocks, React admin apps. For the operator-facing side (turning the Viewer on, filtering, exporting logs to support), see the customer guide: Debug System.
The logging helpers are an internal / first-party developer surface. The signatures and behavior below are accurate for building on Flavor itself; they are not a frozen public API contract.
The house rule: never call error_log() directly
There is exactly one sanctioned way to log, per context:
| You are writing… | Use | Never |
|---|---|---|
Theme code (flavor-starter/) | flavor_log() | error_log(), var_dump(), wp_debug_log() |
Plugin code (wp-ecommerce-core/) | wpec_log() | error_log() |
Flavor Core code (flavor-core/) | flavor_core_log() | error_log() |
| Browser JS (admin or storefront) | window.flavorLog() | console.log() left in shipped code |
Direct error_log() calls bypass the unified logger — they won't appear in the Viewer, won't be channelled, won't be redacted, and won't respect the clean gate. Leaving error_log() (or console.log) in shipped code is caught by tooling (audit.ps1 Check 20). If you have legacy prefixed error_log() output you can't remove yet, the bridge can capture it — but new code should call the helpers.
Independent of the logger: do not ship flavor_log() calls that fire on every request as a way to "leave breadcrumbs." Log meaningful events (failures, state transitions, integration responses) — not per-request noise.
The four helpers
All four ultimately call the same Flavor_Core_Logger. They differ in argument order and defaults, so read the signatures carefully.
PHP — theme
flavor_log(mixed $message, string $level = 'debug', string $channel = 'theme'): void
flavor_log('Cart hydrated', 'debug', 'theme.module.cart');
flavor_log($order, 'info', 'theme.checkout'); // arrays/objects are print_r'd automatically
Source: flavor-starter/inc/core/helpers.php:63.
PHP — plugin
wpec_log(mixed $message, string $level = 'debug', string $channel = 'plugin'): void
wpec_log('Stripe charge declined', 'warning', 'plugin.gateway.stripe');
wpec_log($apiResponse, 'error', 'plugin.erp.invoicing');
Source: wp-ecommerce-core/includes/helpers.php:91.
PHP — Flavor Core (different argument order — read this twice)
flavor_core_log(
string $level,
string $message,
array $context = [],
string $source = 'core',
string $channel = ''
): void
flavor_core_log(
'error',
'License refresh failed',
['http_status' => 503, 'endpoint' => $url], // structured context (redacted, see below)
'core',
'core.licensing'
);
Source: flavor-core/includes/class-logger.php:83 (the global shortcut is declared alongside the logger class, not in debug-helpers.php).
flavor_log() and wpec_log() take (message, level, channel).
flavor_core_log() takes (level, message, context, source, channel) — level first, message second, and it is the only helper that accepts a structured $context array.
Passing one order to the other function is a real, easy mistake: an array meant for $context lands in a string $channel parameter and throws a TypeError. If you need a $context array, you must use flavor_core_log() (or the underlying Flavor_Core_Logger::instance()->log()); flavor_log() / wpec_log() deliberately take no context.
The two theme/plugin helpers are thin conveniences that forward to the logger with an empty context and a fixed source label ('theme' / 'plugin'). Under the hood everything routes through:
Flavor_Core_Logger::instance()->log($level, $message, $context, $source, $channel);
If you need the full five-argument shape (context + custom source) from theme or plugin code, call the logger directly rather than reaching for flavor_core_log() — the $source label is what tags the entry's origin in the Viewer.
JavaScript — window.flavorLog()
window.flavorLog(level, message, context, channel);
window.flavorLog('error', 'Checkout step failed', { step: 3, cartId }, 'react.checkout');
- Argument order mirrors
flavor_core_log():(level, message, context, channel)— level first. - Defaults:
level→'info',channel→'react'. - It POSTs the event to the debug REST sink (
.../debug/js-log); the server then writes it through the same logger. - The bridge script also auto-captures uncaught errors (
window.onerror→ channeljs.error) and unhandled promise rejections (unhandledrejection→ channeljs.rejection), so runtime failures reach the Viewer without any manual call. - It is only enqueued when
FLAVOR_DEBUGis on AND the current user is an administrator; when the gate is off,window.flavorLogis a no-op that isn't even loaded. It fails silently and never throws into the host app.
Source: flavor-core/assets/js/js-bridge.js:88.
Levels
The logger accepts the full PSR-3 vocabulary, in severity order:
debug · info · notice · warning · error · critical · alert · emergency
In day-to-day code you'll almost always use the everyday subset: debug, info, warning, error, critical. Pick by intent:
| Level | Use for |
|---|---|
debug | Verbose developer breadcrumbs while chasing a problem |
info | Notable-but-normal events (a sync ran, a webhook was received) |
warning | Something recoverable went wrong (retry, fallback taken) |
error | An operation failed and the user/system is affected |
critical | A failure that needs immediate attention |
Unknown level strings are not filtered out (defensive), but stick to the vocabulary above so severity filtering and thresholds work.
Channels — hierarchical, dot-namespaced
Every entry carries a channel: a dotted, hierarchical label describing where it came from. The first segment is a reserved top-level namespace:
| Top-level | Owner |
|---|---|
theme.* | Theme code (modules, builder, storefront) |
plugin.* | Plugin code (commerce + ERP) |
core.* | Flavor Core (licensing, updates, AI, debug) |
rest.* | REST request/response layer |
react.* | Browser code you logged deliberately |
js.* | Auto-captured browser errors/rejections |
php.* | PHP warnings/notices/fatals (via the bridge) |
Add as many sub-segments as are useful — plugin.gateway.stripe, plugin.erp.invoicing, theme.module.cart, core.updates. The Viewer treats them as a tree: filtering by plugin shows everything under it, plugin.gateway.stripe narrows to one gateway. (Channel convention: KB #273.)
Naming guidance for module/gateway authors: start your channel with the reserved namespace that owns your code, then your subsystem, then the specific area — e.g. a theme module for wishlists logs to theme.module.wishlist. A plugin gateway logs to plugin.gateway.<slug>. This keeps a store's Debug Viewer filterable by product and by feature.
The clean gate — nothing writes unless debug is on
The logger is gated by a single master switch. When it's off, every helper is a no-op: zero disk I/O, no log folder created, no overhead. You never need to wrap a log call in a if (debug) {…} check — the helper already short-circuits.
Debug is on when either is true (Flavor_Core_Logger::isDebugOn(), class-logger.php:195):
// 1. wp-config.php constant (define wins over the option)
define('FLAVOR_DEBUG', true);
…or the admin toggle at Flavor → Debug → Settings, which persists the theme option flavor_debug_enabled (read via flavor_get_option()). (KB #272 — there is no "errors always log anyway" fallback; off means off. Hard PHP fatals still reach PHP's own error.log as usual.)
Optional filters
You can narrow what gets written without changing any log call:
// Only warning and above
define('FLAVOR_DEBUG_LEVEL', 'warning'); // or option 'flavor_debug_level'
// Only these channels (comma-separated, supports foo.* wildcard and universal *)
define('FLAVOR_DEBUG_CHANNELS', 'plugin.*,rest'); // or option 'flavor_debug_channels'
FLAVOR_DEBUG_LEVELsets a severity threshold — entries below it are dropped (isLevelActive()).FLAVOR_DEBUG_CHANNELSis a whitelist of channel patterns —plugin.*matches the whole plugin subtree, a barerestmatches exactly,*matches everything (isChannelActive()).
Both accept a define() (wins) or the persisted option. With neither set: all levels, all channels.
Context and automatic redaction
Only flavor_core_log() / Flavor_Core_Logger::log() / window.flavorLog() accept a structured $context array — attach the useful shape of the event (ids, statuses, endpoints) rather than string-concatenating it into the message.
Sensitive keys are auto-redacted before anything is written, recursively through nested arrays. Keys matched (case-insensitive substring) include password, secret, token, api_key, auth, cookie, session, card_number, cvv, iban, plus myDATA/gateway internals like subscription-key and aade-user-id. Their values are replaced with a redaction marker. (Definition: Flavor_Core_Logger::SENSITIVE_KEYS, class-logger.php:131.)
You still shouldn't log raw secrets deliberately — but the redaction layer means an accidental ['authorization' => $header] won't leak into a log a customer later exports to support. Entries are also size-capped (~10 KB each) and truncated beyond that.
Entry schema (one JSON object per line in wp-content/flavor-logs/flavor-YYYY-MM-DD.log):
{"ts":"2026-04-24T14:32:15.123Z","level":"error","channel":"plugin.gateway.stripe",
"source":"plugin","msg":"Card declined","site":"https://…","ctx":{"order_id":12345}}
Files rotate at 10 MB (into -NN suffixes), roll daily, and prune after ~30 days.
The error_log() bridge
For capturing output the helpers don't produce — legacy prefixed error_log() calls, PHP warnings/notices/fatals, and REST 500s that WordPress swallowed — there's an opt-in bridge. It requires both the master gate on and an explicit define:
define('FLAVOR_DEBUG', true);
define('FLAVOR_DEBUG_BRIDGE', true);
What it does:
- Tails the PHP
error_logsink (whateverini_get('error_log')points at) on shutdown, classifies each new line, and dispatches it to the logger. It reads the sink rather than redirecting it, so it works on managed hosts where the log path is hard-locked byphp_admin_value. - Classifies lines into channels: prefixed Flavor output (
[FLAVOR]/[WPEC]/[FC]) →theme.*/plugin.*/core.*; PHP errors →php.warning,php.notice,php.deprecated,php.fatal, etc.; anything else →php.generic. - Captures hard fatals (
E_ERROR,E_PARSE, …) viaerror_get_last()on shutdown and REST 500s viarest_request_after_callbacks— the two paths a plain sink-tail can't see. - Baselines to end-of-file on first activation, so turning it on doesn't replay historical backlog (only errors after activation appear).
Most first-party code should never need the bridge — call the helpers directly. Reach for it when you're chasing an error that originates outside the ecosystem (WP core, a third-party plugin) or in legacy code you haven't migrated yet. (Bridge design: KB #269/#271; flavor-core/includes/debug-helpers.php.)
The debug REST API
The Viewer is a React SPA backed by the flavor-core/v1 namespace. These endpoints are also the programmatic surface if you're building tooling around the logs. All require the manage_options capability and a valid REST nonce (X-WP-Nonce).
Base: /wp-json/flavor-core/v1/
| Method | Route | Purpose |
|---|---|---|
GET | /debug/entries | Filtered entries. Params: level, channel (both comma-separated, wildcard-aware), search, before, after, range (hour/24h/7d/30d), limit (default 500, max 5000). |
GET | /debug/stats | Entry counts, disk usage, whether debug is on. |
GET / POST | /debug/settings | Read / write runtime debug settings (the on toggle, level, channels). |
DELETE | /debug/clear | Wipe all log files. |
GET | /debug/download | Stream a ZIP of the log folder (what "export to support" produces). |
GET | /debug/diagnostics | Environment/self-check diagnostics. |
POST | /debug/test | Write sample entries across levels/channels (for verifying the pipeline). |
POST | /debug/js-log | The sink window.flavorLog() POSTs to. Rate-limited to 100 events/min per admin; message capped at 1 KB, context at 4 KB. |
Source: flavor-core/includes/class-debug-api.php.
Quick reference
// Theme — (message, level, channel)
flavor_log('msg', 'warning', 'theme.module.cart');
// Plugin — (message, level, channel)
wpec_log('msg', 'error', 'plugin.gateway.stripe');
// Core — (level, message, context, source, channel) ← different order + context
flavor_core_log('info', 'msg', ['ctx' => 1], 'core', 'core.updates');
// JS — (level, message, context, channel)
window.flavorLog('error', 'msg', { ctx: 1 }, 'react.checkout');
Gate: define('FLAVOR_DEBUG', true) or Flavor → Debug → Settings. Off = nothing writes.
Related
- Debug System — the customer-facing guide to reading and exporting logs in the Viewer.
- REST API Overview — the three Flavor REST namespaces, including
flavor-core/v1.