Skip to main content

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.

First-party scope

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…UseNever
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.

No debug logs in production code

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).

Argument order is NOT the same across the three PHP helpers

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 → channel js.error) and unhandled promise rejections (unhandledrejection → channel js.rejection), so runtime failures reach the Viewer without any manual call.
  • It is only enqueued when FLAVOR_DEBUG is on AND the current user is an administrator; when the gate is off, window.flavorLog is 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:

LevelUse for
debugVerbose developer breadcrumbs while chasing a problem
infoNotable-but-normal events (a sync ran, a webhook was received)
warningSomething recoverable went wrong (retry, fallback taken)
errorAn operation failed and the user/system is affected
criticalA 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-levelOwner
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_LEVEL sets a severity threshold — entries below it are dropped (isLevelActive()).
  • FLAVOR_DEBUG_CHANNELS is a whitelist of channel patterns — plugin.* matches the whole plugin subtree, a bare rest matches 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_log sink (whatever ini_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 by php_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, …) via error_get_last() on shutdown and REST 500s via rest_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/

MethodRoutePurpose
GET/debug/entriesFiltered entries. Params: level, channel (both comma-separated, wildcard-aware), search, before, after, range (hour/24h/7d/30d), limit (default 500, max 5000).
GET/debug/statsEntry counts, disk usage, whether debug is on.
GET / POST/debug/settingsRead / write runtime debug settings (the on toggle, level, channels).
DELETE/debug/clearWipe all log files.
GET/debug/downloadStream a ZIP of the log folder (what "export to support" produces).
GET/debug/diagnosticsEnvironment/self-check diagnostics.
POST/debug/testWrite sample entries across levels/channels (for verifying the pipeline).
POST/debug/js-logThe 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.


  • 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.