From cb3f032d6532046ae06e445f33e7813590ed8c57 Mon Sep 17 00:00:00 2001 From: Joseph Replin Date: Tue, 24 Feb 2026 16:52:09 -0600 Subject: [PATCH 1/8] Enhance logging: scoped loggers, levels, colors --- js/enums/logLevelEnum.js | 10 +- js/logging.js | 282 +++++++++++++++++++++++++++++++++++---- 2 files changed, 263 insertions(+), 29 deletions(-) diff --git a/js/enums/logLevelEnum.js b/js/enums/logLevelEnum.js index d73c8bb1..72cac07c 100644 --- a/js/enums/logLevelEnum.js +++ b/js/enums/logLevelEnum.js @@ -1,7 +1,15 @@ -// Used to determine if log call should be printed based on log level +/** + * Ordered log levels used to determine whether a log call should be printed. + * Levels are compared ordinally — a configured level of `WARN` will suppress + * `DEBUG`, `INFO`, and `SUCCESS` output. + * @file + * @module core/js/enums/logLevelEnum + * @enum {number} + */ const LOG_LEVEL = ENUM([ 'DEBUG', 'INFO', + 'SUCCESS', 'WARN', 'ERROR', 'FATAL' diff --git a/js/logging.js b/js/logging.js index f2c7f1ae..ea4843a0 100644 --- a/js/logging.js +++ b/js/logging.js @@ -1,6 +1,34 @@ +/** + * @file Core logging service providing levelled console output, scoped plugin + * loggers, and event hooks for error-reporting integrations. + * @module core/js/logging + */ import Adapt from 'core/js/adapt'; import LOG_LEVEL from 'core/js/enums/logLevelEnum'; +/** + * @typedef {Object} ScopedLogger + * @property {Function} debug - Log at DEBUG level with plugin prefix + * @property {Function} info - Log at INFO level with plugin prefix + * @property {Function} success - Log at SUCCESS level with plugin prefix + * @property {Function} warn - Log at WARN level with plugin prefix + * @property {Function} error - Log at ERROR level with plugin prefix + * @property {Function} fatal - Log at FATAL level with plugin prefix + */ + +/** + * @classdesc Singleton logging service. Wraps `console` output with log-level + * filtering, coloured scoped output for plugins, and once-only deduplication + * for deprecation and removal warnings. + * @fires module:core/js/logging~log + * @fires module:core/js/logging~log:debug + * @fires module:core/js/logging~log:info + * @fires module:core/js/logging~log:success + * @fires module:core/js/logging~log:warn + * @fires module:core/js/logging~log:error + * @fires module:core/js/logging~log:fatal + * @fires module:core/js/logging~log:ready + */ class Logging extends Backbone.Controller { initialize() { @@ -8,9 +36,11 @@ class Logging extends Backbone.Controller { _isEnabled: true, _level: LOG_LEVEL.INFO.asLowerCase, // Default log level _console: true, // Log to console - _warnFirstOnly: true // Show only first of identical removed and deprecated warnings + _warnFirstOnly: true, // Show only first of identical removed and deprecated warnings + _colors: true // Enable colored console output }; this._warned = {}; + this._scopedLoggers = {}; this.listenToOnce(Adapt, 'configModel:dataLoaded', this.onLoadConfigData); } @@ -27,18 +57,24 @@ class Logging extends Backbone.Controller { loadConfig() { if (Adapt.config.has('_logging')) { - this._config = Adapt.config.get('_logging'); + const courseConfig = Adapt.config.get('_logging'); + // Merge course config with defaults instead of replacing + this._config = Object.assign({}, this._config, courseConfig); } - this.checkQueryStringOverride(); + this._checkQueryStringOverride(); } - checkQueryStringOverride() { + /** + * Checks the page query string for a `loglevel` override and applies it + * to the active config if a valid level is found. + * @private + */ + _checkQueryStringOverride() { - // Override default log level with level present in query string - const matches = window.location.search.match(/[?&]loglevel=([a-z]*)/i); - if (!matches || matches.length < 2) return; + const matches = window.location.search.match(/[?&]loglevel=([a-z0-9]+)/i); + if (!matches || !matches[1]) return; const override = LOG_LEVEL(matches[1].toUpperCase()); if (!override) return; @@ -48,36 +84,127 @@ class Logging extends Backbone.Controller { } + /** + * Logs a message at DEBUG level. + * @param {...*} args - Values to log + */ debug(...args) { this._log(LOG_LEVEL.DEBUG, args); } + /** + * Logs a message at INFO level. + * @param {...*} args - Values to log + */ info(...args) { this._log(LOG_LEVEL.INFO, args); } + /** + * Logs a message at SUCCESS level. + * @param {...*} args - Values to log + */ + success(...args) { + this._log(LOG_LEVEL.SUCCESS, args); + } + + /** + * Logs a message at WARN level. + * @param {...*} args - Values to log + */ warn(...args) { this._log(LOG_LEVEL.WARN, args); } + /** + * Logs a message at ERROR level. + * @param {...*} args - Values to log + */ error(...args) { this._log(LOG_LEVEL.ERROR, args); } + /** + * Logs a message at FATAL level. + * @param {...*} args - Values to log + */ fatal(...args) { this._log(LOG_LEVEL.FATAL, args); } + /** + * Creates a cached, namespaced logger for a plugin or module. + * Every message is prefixed `[source]` in the console and coloured by level + * when `_colors` is enabled. Repeated calls with the same `source` return + * the same cached instance. + * @param {string} source - Cache key and default display name (e.g. `'xAPI'`, `'spoor'`) + * @param {string} [name] - Optional display label; only applied on first call for a given source + * @returns {ScopedLogger} Scoped logger instance + * @throws {Error} If source is not a non-empty string + * @example + * const logger = logging.scope('MyPlugin'); + * logger.success('Data loaded'); + * logger.error('Connection failed', err); + * @example + * const logger = logging.scope('MyPlugin', 'Feature-X'); + * logger.warn('Retrying…'); + */ + scope(source, name) { + if (!source || typeof source !== 'string') { + throw new Error('logging.scope() requires a source name string parameter'); + } + + const displayName = name || source; + + // Return cached scoped logger if it exists + if (this._scopedLoggers[source]) { + if (name && this._scopedLoggers[source]._displayName !== displayName) { + this.warn(`logging.scope('${source}'): already cached with a different display name, ignoring '${name}'`); + } + return this._scopedLoggers[source]; + } + + // Create new scoped logger + const scopedLogger = { + _displayName: displayName, + debug: (...args) => this._log(LOG_LEVEL.DEBUG, args, displayName), + info: (...args) => this._log(LOG_LEVEL.INFO, args, displayName), + success: (...args) => this._log(LOG_LEVEL.SUCCESS, args, displayName), + warn: (...args) => this._log(LOG_LEVEL.WARN, args, displayName), + error: (...args) => this._log(LOG_LEVEL.ERROR, args, displayName), + fatal: (...args) => this._log(LOG_LEVEL.FATAL, args, displayName) + }; + + // Cache the scoped logger + this._scopedLoggers[source] = scopedLogger; + + return scopedLogger; + } + + /** + * Logs a one-time WARN message prefixed with `REMOVED`. + * Use when an API or feature has been removed entirely. + * @example + * logging.removed('myPlugin.oldMethod(), use myPlugin.newMethod() instead'); + */ removed(...args) { - args = ['REMOVED'].concat(args); - this.warnOnce(...args); + this.warnOnce('REMOVED', ...args); } + /** + * Logs a one-time WARN message prefixed with `DEPRECATED`. + * Use when an API or feature still works but should no longer be used. + * @example + * logging.deprecated('myPlugin.oldProp, use myPlugin.newProp instead'); + */ deprecated(...args) { - args = ['DEPRECATED'].concat(args); - this.warnOnce(...args); + this.warnOnce('DEPRECATED', ...args); } + /** + * Logs a WARN message only the first time it is called with a given set of arguments. + * Subsequent calls with identical arguments are silently discarded when `_warnFirstOnly` is enabled. + */ warnOnce(...args) { if (this._hasWarned(args)) { return; @@ -85,40 +212,139 @@ class Logging extends Backbone.Controller { this._log(LOG_LEVEL.WARN, args); } - _log(level, data) { - - const isEnabled = (this._config._isEnabled); + /** + * Core log dispatch. Checks enabled state and level filter, then delegates + * to console output and fires public log events. + * @param {*} level - LOG_LEVEL enum value + * @param {Array} data - Arguments to log + * @param {string|null} [source] - Optional source/plugin name + * @fires module:core/js/logging~log + * @fires module:core/js/logging~log:debug + * @fires module:core/js/logging~log:info + * @fires module:core/js/logging~log:success + * @fires module:core/js/logging~log:warn + * @fires module:core/js/logging~log:error + * @fires module:core/js/logging~log:fatal + * @private + */ + _log(level, data, source = null) { + + const isEnabled = this._config._isEnabled; if (!isEnabled) return; - const configLevel = LOG_LEVEL(this._config._level.toUpperCase()); + const configLevel = LOG_LEVEL((this._config._level ?? LOG_LEVEL.INFO.asLowerCase).toUpperCase()); - const isLogLevelAllowed = (level >= configLevel); + const isLogLevelAllowed = level >= configLevel; if (!isLogLevelAllowed) return; - this._logToConsole(level, data); + this._logToConsole(level, data, source); // Allow error reporting plugins to hook and report to logging systems - this.trigger('log', level, data); - this.trigger('log:' + level.asLowerCase, level, data); + this.trigger('log', level, data, source); + this.trigger('log:' + level.asLowerCase, level, data, source); } - _logToConsole(level, data) { - - const shouldLogToConsole = (this._config._console); + /** + * Writes a log entry to the browser console, applying coloured CSS styling + * for scoped loggers when `_colors` is enabled. + * @param {*} level - LOG_LEVEL enum value + * @param {Array} data - Arguments to log + * @param {string|null} [source] - Optional source/plugin name + * @private + */ + _logToConsole(level, data, source = null) { + + const shouldLogToConsole = this._config._console; if (!shouldLogToConsole) return; - const log = [level.asUpperCase + ':']; - data && log.push(...data); + const useColors = this._config._colors && source; + const prefix = source ? `[${source}]` : level.asUpperCase + ':'; + + if (useColors) { + // Use colored output for scoped loggers - format entire message as string + const color = this._getColorForLevel(level); + const message = data.map(item => this._serializeArg(item)).join(' '); + const consoleMethod = this._getConsoleMethod(level); - // is there a matching console method we can use e.g. console.error()? - if (console[level.asLowerCase]) { - console[level.asLowerCase](...log); + console[consoleMethod](`%c${prefix} ${message}`, `background: WhiteSmoke; color: ${color}`); } else { - console.log(...log); + // Standard output + const log = [prefix]; + if (data && data.length > 0) { + log.push(...data); + } + + const consoleMethod = this._getConsoleMethod(level); + if (typeof console[consoleMethod] === 'function') { + console[consoleMethod](...log); + } else { + console.log(...log); + } + } + } + + /** + * Converts a single log argument to a string, safely serialising objects + * and truncating oversized JSON to prevent console spam. + * @param {*} item - Value to serialise + * @returns {string} String representation of the value + * @private + */ + _serializeArg(item) { + if (typeof item !== 'object' || item === null) return String(item); + try { + const str = JSON.stringify(item, null, 2); + // Cap output length to prevent console spam + return str.length > 500 ? str.substring(0, 500) + '...' : str; + } catch { + return '[Circular or non-serializable object]'; } } + /** + * Returns a CSS named colour for the given log level. + * @param {*} level - LOG_LEVEL enum value + * @returns {string} CSS colour name + * @private + */ + _getColorForLevel(level) { + const colors = { + debug: 'RoyalBlue', + info: 'Indigo', + success: 'DarkGreen', + warn: 'Chocolate', + error: 'Crimson', + fatal: 'DarkRed' + }; + return colors[level.asLowerCase] || 'black'; + } + + /** + * Returns the `console` method name appropriate for the given log level. + * @param {*} level - LOG_LEVEL enum value + * @returns {string} Console method name (e.g. `'warn'`, `'error'`) + * @private + */ + _getConsoleMethod(level) { + const mapping = { + [LOG_LEVEL.DEBUG.asLowerCase]: 'debug', + [LOG_LEVEL.INFO.asLowerCase]: 'info', + [LOG_LEVEL.SUCCESS.asLowerCase]: 'log', + [LOG_LEVEL.WARN.asLowerCase]: 'warn', + [LOG_LEVEL.ERROR.asLowerCase]: 'error', + [LOG_LEVEL.FATAL.asLowerCase]: 'error' + }; + return mapping[level.asLowerCase] || 'log'; + } + + /** + * Checks whether an identical set of arguments has already been logged + * via `warnOnce`. Records the hash on first call. + * @param {Array} args - Arguments to check + * @returns {boolean} `true` if these arguments have already been warned + * @private + */ _hasWarned(args) { if (!this._config._warnFirstOnly) { return false; From db9e91da0167ccc5464932bdf71ec737fb00b150 Mon Sep 17 00:00:00 2001 From: Joseph Replin Date: Tue, 24 Feb 2026 17:20:00 -0600 Subject: [PATCH 2/8] Add 'success' log level and _colors option --- schema/config.model.schema | 8 ++++++++ schema/config.schema.json | 6 ++++++ 2 files changed, 14 insertions(+) diff --git a/schema/config.model.schema b/schema/config.model.schema index dc98b78e..f3b01fee 100644 --- a/schema/config.model.schema +++ b/schema/config.model.schema @@ -425,6 +425,7 @@ "options": [ "debug", "info", + "success", "warn", "error", "fatal" @@ -444,6 +445,13 @@ "inputType": "Checkbox", "validators": [], "title": "Show only first deprecated and removed warnings?" + }, + "_colors": { + "type": "boolean", + "default": true, + "inputType": "Checkbox", + "validators": [], + "title": "Enable colored console output?" } } }, diff --git a/schema/config.schema.json b/schema/config.schema.json index 14139f2e..5d968349 100644 --- a/schema/config.schema.json +++ b/schema/config.schema.json @@ -377,6 +377,7 @@ "enum": [ "debug", "info", + "success", "warn", "error", "fatal" @@ -392,6 +393,11 @@ "type": "boolean", "title": "Suppress subsequent deprecation warnings", "default": true + }, + "_colors": { + "type": "boolean", + "title": "Enable colored console output", + "default": true } } }, From a2314966d311bc52359d2151b5293c6552597b8c Mon Sep 17 00:00:00 2001 From: Joseph Replin Date: Wed, 13 May 2026 13:50:07 -0500 Subject: [PATCH 3/8] Improve logging JSDoc and comments Add and refine JSDoc across logging files: add file/module header to logLevelEnum, include usage example in logging.js, mark Logging class with @class and @extends, simplify @fires tags to short event names, and add docblocks for onLoadConfigData and loadConfig. Also document args for removed/deprecated/warnOnce methods and update _log event annotations. These changes improve documentation, readability and tooling support for the logging service. --- js/enums/logLevelEnum.js | 7 ++++-- js/logging.js | 50 ++++++++++++++++++++++++++++------------ 2 files changed, 40 insertions(+), 17 deletions(-) diff --git a/js/enums/logLevelEnum.js b/js/enums/logLevelEnum.js index 72cac07c..f250780e 100644 --- a/js/enums/logLevelEnum.js +++ b/js/enums/logLevelEnum.js @@ -1,9 +1,12 @@ +/** + * @file Ordered log-level enum used by the logging service. + * @module core/js/enums/logLevelEnum + */ + /** * Ordered log levels used to determine whether a log call should be printed. * Levels are compared ordinally — a configured level of `WARN` will suppress * `DEBUG`, `INFO`, and `SUCCESS` output. - * @file - * @module core/js/enums/logLevelEnum * @enum {number} */ const LOG_LEVEL = ENUM([ diff --git a/js/logging.js b/js/logging.js index ea4843a0..b39b386b 100644 --- a/js/logging.js +++ b/js/logging.js @@ -2,6 +2,11 @@ * @file Core logging service providing levelled console output, scoped plugin * loggers, and event hooks for error-reporting integrations. * @module core/js/logging + * @example + * import logging from 'core/js/logging'; + * logging.info('Course ready'); + * const logger = logging.scope('MyPlugin'); + * logger.warn('Something unexpected happened'); */ import Adapt from 'core/js/adapt'; import LOG_LEVEL from 'core/js/enums/logLevelEnum'; @@ -17,17 +22,19 @@ import LOG_LEVEL from 'core/js/enums/logLevelEnum'; */ /** + * @class Logging * @classdesc Singleton logging service. Wraps `console` output with log-level * filtering, coloured scoped output for plugins, and once-only deduplication * for deprecation and removal warnings. - * @fires module:core/js/logging~log - * @fires module:core/js/logging~log:debug - * @fires module:core/js/logging~log:info - * @fires module:core/js/logging~log:success - * @fires module:core/js/logging~log:warn - * @fires module:core/js/logging~log:error - * @fires module:core/js/logging~log:fatal - * @fires module:core/js/logging~log:ready + * @fires log + * @fires log:debug + * @fires log:info + * @fires log:success + * @fires log:warn + * @fires log:error + * @fires log:fatal + * @fires log:ready + * @extends {Backbone.Controller} */ class Logging extends Backbone.Controller { @@ -44,6 +51,12 @@ class Logging extends Backbone.Controller { this.listenToOnce(Adapt, 'configModel:dataLoaded', this.onLoadConfigData); } + /** + * Handles the `configModel:dataLoaded` event. Loads logging config from the + * course config model and fires `log:ready` to signal that the service is + * fully configured. + * @fires log:ready + */ onLoadConfigData() { this.loadConfig(); @@ -54,6 +67,10 @@ class Logging extends Backbone.Controller { } + /** + * Reads `_logging` config from the course config model and merges it with + * the default config. Also checks for a `loglevel` query string override. + */ loadConfig() { if (Adapt.config.has('_logging')) { @@ -184,6 +201,7 @@ class Logging extends Backbone.Controller { /** * Logs a one-time WARN message prefixed with `REMOVED`. * Use when an API or feature has been removed entirely. + * @param {...*} args - Values to log * @example * logging.removed('myPlugin.oldMethod(), use myPlugin.newMethod() instead'); */ @@ -194,6 +212,7 @@ class Logging extends Backbone.Controller { /** * Logs a one-time WARN message prefixed with `DEPRECATED`. * Use when an API or feature still works but should no longer be used. + * @param {...*} args - Values to log * @example * logging.deprecated('myPlugin.oldProp, use myPlugin.newProp instead'); */ @@ -204,6 +223,7 @@ class Logging extends Backbone.Controller { /** * Logs a WARN message only the first time it is called with a given set of arguments. * Subsequent calls with identical arguments are silently discarded when `_warnFirstOnly` is enabled. + * @param {...*} args - Values to log */ warnOnce(...args) { if (this._hasWarned(args)) { @@ -218,13 +238,13 @@ class Logging extends Backbone.Controller { * @param {*} level - LOG_LEVEL enum value * @param {Array} data - Arguments to log * @param {string|null} [source] - Optional source/plugin name - * @fires module:core/js/logging~log - * @fires module:core/js/logging~log:debug - * @fires module:core/js/logging~log:info - * @fires module:core/js/logging~log:success - * @fires module:core/js/logging~log:warn - * @fires module:core/js/logging~log:error - * @fires module:core/js/logging~log:fatal + * @fires log + * @fires log:debug + * @fires log:info + * @fires log:success + * @fires log:warn + * @fires log:error + * @fires log:fatal * @private */ _log(level, data, source = null) { From a37e5a107fda54426246fdd65fe8202fb2e9f770 Mon Sep 17 00:00:00 2001 From: Joseph Replin Date: Wed, 13 May 2026 13:59:18 -0500 Subject: [PATCH 4/8] Simplify logging.scope and note config timing Document that course `_logging` is applied at `configModel:dataLoaded` and that logs emitted before that use constructor defaults. Remove the optional `name` parameter and `_displayName` handling from `logging.scope`; scoped loggers are now cached by source and always use the source string as the console display name. This simplifies the API, avoids inconsistent display-name warnings for cached scopes, and clarifies why early log output may not reflect course-configured settings. --- js/logging.js | 34 ++++++++++++++++------------------ 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/js/logging.js b/js/logging.js index b39b386b..3bfd1510 100644 --- a/js/logging.js +++ b/js/logging.js @@ -26,6 +26,10 @@ import LOG_LEVEL from 'core/js/enums/logLevelEnum'; * @classdesc Singleton logging service. Wraps `console` output with log-level * filtering, coloured scoped output for plugins, and once-only deduplication * for deprecation and removal warnings. + * + * **Note:** Course config (`_logging`) is applied at `configModel:dataLoaded`. + * Any log calls made before that event use the constructor defaults, so early + * output may not reflect the configured level, colors, or console settings. * @fires log * @fires log:debug * @fires log:info @@ -70,6 +74,10 @@ class Logging extends Backbone.Controller { /** * Reads `_logging` config from the course config model and merges it with * the default config. Also checks for a `loglevel` query string override. + * + * **Note:** This runs at `configModel:dataLoaded`. Logs emitted before this + * point use constructor defaults, so `_colors`, `_level`, and `_console` may + * differ from the course-configured values for early output. */ loadConfig() { @@ -154,42 +162,32 @@ class Logging extends Backbone.Controller { * Every message is prefixed `[source]` in the console and coloured by level * when `_colors` is enabled. Repeated calls with the same `source` return * the same cached instance. - * @param {string} source - Cache key and default display name (e.g. `'xAPI'`, `'spoor'`) - * @param {string} [name] - Optional display label; only applied on first call for a given source + * @param {string} source - Cache key and console display name (e.g. `'xAPI'`, `'spoor'`) * @returns {ScopedLogger} Scoped logger instance * @throws {Error} If source is not a non-empty string * @example * const logger = logging.scope('MyPlugin'); * logger.success('Data loaded'); * logger.error('Connection failed', err); - * @example - * const logger = logging.scope('MyPlugin', 'Feature-X'); - * logger.warn('Retrying…'); */ - scope(source, name) { + scope(source) { if (!source || typeof source !== 'string') { throw new Error('logging.scope() requires a source name string parameter'); } - const displayName = name || source; - // Return cached scoped logger if it exists if (this._scopedLoggers[source]) { - if (name && this._scopedLoggers[source]._displayName !== displayName) { - this.warn(`logging.scope('${source}'): already cached with a different display name, ignoring '${name}'`); - } return this._scopedLoggers[source]; } // Create new scoped logger const scopedLogger = { - _displayName: displayName, - debug: (...args) => this._log(LOG_LEVEL.DEBUG, args, displayName), - info: (...args) => this._log(LOG_LEVEL.INFO, args, displayName), - success: (...args) => this._log(LOG_LEVEL.SUCCESS, args, displayName), - warn: (...args) => this._log(LOG_LEVEL.WARN, args, displayName), - error: (...args) => this._log(LOG_LEVEL.ERROR, args, displayName), - fatal: (...args) => this._log(LOG_LEVEL.FATAL, args, displayName) + debug: (...args) => this._log(LOG_LEVEL.DEBUG, args, source), + info: (...args) => this._log(LOG_LEVEL.INFO, args, source), + success: (...args) => this._log(LOG_LEVEL.SUCCESS, args, source), + warn: (...args) => this._log(LOG_LEVEL.WARN, args, source), + error: (...args) => this._log(LOG_LEVEL.ERROR, args, source), + fatal: (...args) => this._log(LOG_LEVEL.FATAL, args, source) }; // Cache the scoped logger From 2dccf5781b208f9d868c70799302dca0c3f23440 Mon Sep 17 00:00:00 2001 From: Joseph Replin Date: Wed, 13 May 2026 14:04:35 -0500 Subject: [PATCH 5/8] Simplify colored logging output, pass raw args When useColors is enabled, log output now prints a CSS-styled prefix and spreads the original arguments into the console method instead of pre-serializing and joining them. This preserves native object inspection in devtools, avoids costly JSON serialization and truncation, and reduces console spam/overhead. Removes the _serializeArg helper and its truncation/serialization logic. --- js/logging.js | 23 +---------------------- 1 file changed, 1 insertion(+), 22 deletions(-) diff --git a/js/logging.js b/js/logging.js index 3bfd1510..bd1676f3 100644 --- a/js/logging.js +++ b/js/logging.js @@ -280,12 +280,9 @@ class Logging extends Backbone.Controller { const prefix = source ? `[${source}]` : level.asUpperCase + ':'; if (useColors) { - // Use colored output for scoped loggers - format entire message as string const color = this._getColorForLevel(level); - const message = data.map(item => this._serializeArg(item)).join(' '); const consoleMethod = this._getConsoleMethod(level); - - console[consoleMethod](`%c${prefix} ${message}`, `background: WhiteSmoke; color: ${color}`); + console[consoleMethod](`%c${prefix}`, `background: WhiteSmoke; color: ${color}`, ...data); } else { // Standard output const log = [prefix]; @@ -302,24 +299,6 @@ class Logging extends Backbone.Controller { } } - /** - * Converts a single log argument to a string, safely serialising objects - * and truncating oversized JSON to prevent console spam. - * @param {*} item - Value to serialise - * @returns {string} String representation of the value - * @private - */ - _serializeArg(item) { - if (typeof item !== 'object' || item === null) return String(item); - try { - const str = JSON.stringify(item, null, 2); - // Cap output length to prevent console spam - return str.length > 500 ? str.substring(0, 500) + '...' : str; - } catch { - return '[Circular or non-serializable object]'; - } - } - /** * Returns a CSS named colour for the given log level. * @param {*} level - LOG_LEVEL enum value From 1c819069635d76d7486ace3af30f810f3209370a Mon Sep 17 00:00:00 2001 From: Joseph Replin Date: Wed, 13 May 2026 14:08:33 -0500 Subject: [PATCH 6/8] Remove colored console output from Logging Remove support for colorized console styling and the internal _colors config. Simplifies Logging by always prefixing messages with a source or level and using the appropriate console method (falling back to console.log). Removes _getColorForLevel and related branching, and updates comments to reflect that console output is no longer colorized. --- js/logging.js | 55 +++++++++++---------------------------------------- 1 file changed, 11 insertions(+), 44 deletions(-) diff --git a/js/logging.js b/js/logging.js index bd1676f3..6b06c02b 100644 --- a/js/logging.js +++ b/js/logging.js @@ -24,12 +24,12 @@ import LOG_LEVEL from 'core/js/enums/logLevelEnum'; /** * @class Logging * @classdesc Singleton logging service. Wraps `console` output with log-level - * filtering, coloured scoped output for plugins, and once-only deduplication + * filtering, levelled scoped output for plugins, and once-only deduplication * for deprecation and removal warnings. * * **Note:** Course config (`_logging`) is applied at `configModel:dataLoaded`. * Any log calls made before that event use the constructor defaults, so early - * output may not reflect the configured level, colors, or console settings. + * output may not reflect the configured level or console settings. * @fires log * @fires log:debug * @fires log:info @@ -47,8 +47,7 @@ class Logging extends Backbone.Controller { _isEnabled: true, _level: LOG_LEVEL.INFO.asLowerCase, // Default log level _console: true, // Log to console - _warnFirstOnly: true, // Show only first of identical removed and deprecated warnings - _colors: true // Enable colored console output + _warnFirstOnly: true // Show only first of identical removed and deprecated warnings }; this._warned = {}; this._scopedLoggers = {}; @@ -76,8 +75,8 @@ class Logging extends Backbone.Controller { * the default config. Also checks for a `loglevel` query string override. * * **Note:** This runs at `configModel:dataLoaded`. Logs emitted before this - * point use constructor defaults, so `_colors`, `_level`, and `_console` may - * differ from the course-configured values for early output. + * point use constructor defaults, so `_level` and `_console` may differ from + * the course-configured values for early output. */ loadConfig() { @@ -159,8 +158,7 @@ class Logging extends Backbone.Controller { /** * Creates a cached, namespaced logger for a plugin or module. - * Every message is prefixed `[source]` in the console and coloured by level - * when `_colors` is enabled. Repeated calls with the same `source` return + * Every message is prefixed `[source]` in the console. Repeated calls with the same `source` return * the same cached instance. * @param {string} source - Cache key and console display name (e.g. `'xAPI'`, `'spoor'`) * @returns {ScopedLogger} Scoped logger instance @@ -264,8 +262,7 @@ class Logging extends Backbone.Controller { } /** - * Writes a log entry to the browser console, applying coloured CSS styling - * for scoped loggers when `_colors` is enabled. + * Writes a log entry to the browser console. * @param {*} level - LOG_LEVEL enum value * @param {Array} data - Arguments to log * @param {string|null} [source] - Optional source/plugin name @@ -276,45 +273,15 @@ class Logging extends Backbone.Controller { const shouldLogToConsole = this._config._console; if (!shouldLogToConsole) return; - const useColors = this._config._colors && source; const prefix = source ? `[${source}]` : level.asUpperCase + ':'; + const consoleMethod = this._getConsoleMethod(level); - if (useColors) { - const color = this._getColorForLevel(level); - const consoleMethod = this._getConsoleMethod(level); - console[consoleMethod](`%c${prefix}`, `background: WhiteSmoke; color: ${color}`, ...data); + if (typeof console[consoleMethod] === 'function') { + console[consoleMethod](prefix, ...data); } else { - // Standard output - const log = [prefix]; - if (data && data.length > 0) { - log.push(...data); - } - - const consoleMethod = this._getConsoleMethod(level); - if (typeof console[consoleMethod] === 'function') { - console[consoleMethod](...log); - } else { - console.log(...log); - } + console.log(prefix, ...data); } - } - /** - * Returns a CSS named colour for the given log level. - * @param {*} level - LOG_LEVEL enum value - * @returns {string} CSS colour name - * @private - */ - _getColorForLevel(level) { - const colors = { - debug: 'RoyalBlue', - info: 'Indigo', - success: 'DarkGreen', - warn: 'Chocolate', - error: 'Crimson', - fatal: 'DarkRed' - }; - return colors[level.asLowerCase] || 'black'; } /** From 358730f8267849794df66a0352f6dcfaffbbe38e Mon Sep 17 00:00:00 2001 From: Joseph Replin Date: Wed, 13 May 2026 14:21:42 -0500 Subject: [PATCH 7/8] Refactor logging: console mapping & checks Add a CONSOLE_METHOD constant to centralize console method mapping and use it in _getConsoleMethod; add @fires JSDoc tags to each level method (debug, info, success, warn, error, fatal); simplify _log by inlining the isEnabled and level-comparison checks and using nullish coalescing for the default config level. Purely refactoring for clarity and maintainability; no intended behavior changes. --- js/logging.js | 39 +++++++++++++++++++++++++-------------- 1 file changed, 25 insertions(+), 14 deletions(-) diff --git a/js/logging.js b/js/logging.js index 6b06c02b..cf88ebdd 100644 --- a/js/logging.js +++ b/js/logging.js @@ -11,6 +11,15 @@ import Adapt from 'core/js/adapt'; import LOG_LEVEL from 'core/js/enums/logLevelEnum'; +const CONSOLE_METHOD = { + [LOG_LEVEL.DEBUG.asLowerCase]: 'debug', + [LOG_LEVEL.INFO.asLowerCase]: 'info', + [LOG_LEVEL.SUCCESS.asLowerCase]: 'log', + [LOG_LEVEL.WARN.asLowerCase]: 'warn', + [LOG_LEVEL.ERROR.asLowerCase]: 'error', + [LOG_LEVEL.FATAL.asLowerCase]: 'error' +}; + /** * @typedef {Object} ScopedLogger * @property {Function} debug - Log at DEBUG level with plugin prefix @@ -111,6 +120,8 @@ class Logging extends Backbone.Controller { /** * Logs a message at DEBUG level. * @param {...*} args - Values to log + * @fires log + * @fires log:debug */ debug(...args) { this._log(LOG_LEVEL.DEBUG, args); @@ -119,6 +130,8 @@ class Logging extends Backbone.Controller { /** * Logs a message at INFO level. * @param {...*} args - Values to log + * @fires log + * @fires log:info */ info(...args) { this._log(LOG_LEVEL.INFO, args); @@ -127,6 +140,8 @@ class Logging extends Backbone.Controller { /** * Logs a message at SUCCESS level. * @param {...*} args - Values to log + * @fires log + * @fires log:success */ success(...args) { this._log(LOG_LEVEL.SUCCESS, args); @@ -135,6 +150,8 @@ class Logging extends Backbone.Controller { /** * Logs a message at WARN level. * @param {...*} args - Values to log + * @fires log + * @fires log:warn */ warn(...args) { this._log(LOG_LEVEL.WARN, args); @@ -143,6 +160,8 @@ class Logging extends Backbone.Controller { /** * Logs a message at ERROR level. * @param {...*} args - Values to log + * @fires log + * @fires log:error */ error(...args) { this._log(LOG_LEVEL.ERROR, args); @@ -151,6 +170,8 @@ class Logging extends Backbone.Controller { /** * Logs a message at FATAL level. * @param {...*} args - Values to log + * @fires log + * @fires log:fatal */ fatal(...args) { this._log(LOG_LEVEL.FATAL, args); @@ -158,7 +179,7 @@ class Logging extends Backbone.Controller { /** * Creates a cached, namespaced logger for a plugin or module. - * Every message is prefixed `[source]` in the console. Repeated calls with the same `source` return + * Every message is prefixed `[source]` in the console. Repeated calls with the same `source` return * the same cached instance. * @param {string} source - Cache key and console display name (e.g. `'xAPI'`, `'spoor'`) * @returns {ScopedLogger} Scoped logger instance @@ -245,13 +266,11 @@ class Logging extends Backbone.Controller { */ _log(level, data, source = null) { - const isEnabled = this._config._isEnabled; - if (!isEnabled) return; + if (!this._config._isEnabled) return; const configLevel = LOG_LEVEL((this._config._level ?? LOG_LEVEL.INFO.asLowerCase).toUpperCase()); - const isLogLevelAllowed = level >= configLevel; - if (!isLogLevelAllowed) return; + if (level < configLevel) return; this._logToConsole(level, data, source); @@ -291,15 +310,7 @@ class Logging extends Backbone.Controller { * @private */ _getConsoleMethod(level) { - const mapping = { - [LOG_LEVEL.DEBUG.asLowerCase]: 'debug', - [LOG_LEVEL.INFO.asLowerCase]: 'info', - [LOG_LEVEL.SUCCESS.asLowerCase]: 'log', - [LOG_LEVEL.WARN.asLowerCase]: 'warn', - [LOG_LEVEL.ERROR.asLowerCase]: 'error', - [LOG_LEVEL.FATAL.asLowerCase]: 'error' - }; - return mapping[level.asLowerCase] || 'log'; + return CONSOLE_METHOD[level.asLowerCase] ?? 'log'; } /** From 0ba0f2298fe407ddf4fa07eba987d5041bee4e75 Mon Sep 17 00:00:00 2001 From: Joseph Replin Date: Wed, 13 May 2026 14:28:43 -0500 Subject: [PATCH 8/8] Remove _colors option from config schemas Delete the deprecated `_colors` boolean setting from schema/config.model.schema and schema/config.schema.json. This removes the now-unused configuration for enabling colored console output. --- schema/config.model.schema | 7 ------- schema/config.schema.json | 5 ----- 2 files changed, 12 deletions(-) diff --git a/schema/config.model.schema b/schema/config.model.schema index f3b01fee..977713ca 100644 --- a/schema/config.model.schema +++ b/schema/config.model.schema @@ -445,13 +445,6 @@ "inputType": "Checkbox", "validators": [], "title": "Show only first deprecated and removed warnings?" - }, - "_colors": { - "type": "boolean", - "default": true, - "inputType": "Checkbox", - "validators": [], - "title": "Enable colored console output?" } } }, diff --git a/schema/config.schema.json b/schema/config.schema.json index 5d968349..45c2155b 100644 --- a/schema/config.schema.json +++ b/schema/config.schema.json @@ -393,11 +393,6 @@ "type": "boolean", "title": "Suppress subsequent deprecation warnings", "default": true - }, - "_colors": { - "type": "boolean", - "title": "Enable colored console output", - "default": true } } },