From 7566a17e6d689a60172204a1ca10115128136fd2 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Wed, 9 Jul 2025 12:02:15 -0300 Subject: [PATCH 01/10] feat: add custom logger support via logger callback option --- src/logger/__tests__/sdkLogger.mock.ts | 1 + src/logger/index.ts | 7 ++++++- src/logger/types.ts | 9 ++++++--- .../settingsValidation/logger/builtinLogger.ts | 5 +++-- src/utils/settingsValidation/logger/commons.ts | 18 +++++++----------- .../logger/pluggableLogger.ts | 10 +++++++--- types/splitio.d.ts | 2 ++ 7 files changed, 32 insertions(+), 20 deletions(-) diff --git a/src/logger/__tests__/sdkLogger.mock.ts b/src/logger/__tests__/sdkLogger.mock.ts index b7c4aa25..e358a760 100644 --- a/src/logger/__tests__/sdkLogger.mock.ts +++ b/src/logger/__tests__/sdkLogger.mock.ts @@ -6,6 +6,7 @@ export const loggerMock = { debug: jest.fn(), info: jest.fn(), setLogLevel: jest.fn(), + setLogger: jest.fn(), mockClear() { this.warn.mockClear(); diff --git a/src/logger/index.ts b/src/logger/index.ts index 662e1f86..0663bd12 100644 --- a/src/logger/index.ts +++ b/src/logger/index.ts @@ -41,6 +41,7 @@ const defaultOptions = { prefix: 'splitio', logLevel: LogLevels.NONE, showLevel: true, + logger(formattedMsg: string) { console.log(formattedMsg); } }; export class Logger implements ILogger { @@ -55,6 +56,10 @@ export class Logger implements ILogger { this.logLevel = LogLevelIndexes[this.options.logLevel]; } + setLogger(logger: (formattedMsg: string, level: SplitIO.LogLevel, msg: string) => void) { + this.options.logger = logger; + } + setLogLevel(logLevel: SplitIO.LogLevel) { this.options.logLevel = logLevel; this.logLevel = LogLevelIndexes[logLevel]; @@ -86,7 +91,7 @@ export class Logger implements ILogger { const formattedText = this._generateLogMessage(level, msg); - console.log(formattedText); + this.options.logger(formattedText, level, msg); } private _generateLogMessage(level: SplitIO.LogLevel, text: string) { diff --git a/src/logger/types.ts b/src/logger/types.ts index 2f05b3ba..2987971d 100644 --- a/src/logger/types.ts +++ b/src/logger/types.ts @@ -1,12 +1,15 @@ import SplitIO from '../../types/splitio'; export interface ILoggerOptions { - prefix?: string, - logLevel?: SplitIO.LogLevel, - showLevel?: boolean, // @TODO remove this param eventually since it is not being set `false` anymore + prefix?: string; + logLevel?: SplitIO.LogLevel; + showLevel?: boolean; // @TODO remove this param eventually since it is not being set `false` anymore + logger?: (formattedMsg: string, level: SplitIO.LogLevel, msg: string) => void; } export interface ILogger extends SplitIO.ILogger { + setLogger(logger: (formattedMsg: string, level: SplitIO.LogLevel, msg: string) => void): void; + debug(msg: any): void; debug(msg: string | number, args?: any[]): void; diff --git a/src/utils/settingsValidation/logger/builtinLogger.ts b/src/utils/settingsValidation/logger/builtinLogger.ts index 4f099c7c..6448d9f0 100644 --- a/src/utils/settingsValidation/logger/builtinLogger.ts +++ b/src/utils/settingsValidation/logger/builtinLogger.ts @@ -40,12 +40,13 @@ if (/^(enabled?|on)/i.test(initialState)) { * @param settings - user config object, with an optional `debug` property of type boolean or string log level. * @returns a logger instance with the log level at `settings.debug`. If `settings.debug` is invalid or not provided, `initialLogLevel` is used. */ -export function validateLogger(settings: { debug: unknown }): ILogger { - const { debug } = settings; +export function validateLogger(settings: { debug: unknown, logger?: unknown }): ILogger { + const { debug, logger } = settings; const logLevel: SplitIO.LogLevel | undefined = debug !== undefined ? getLogLevel(debug) : initialLogLevel; const log = new Logger({ logLevel: logLevel || initialLogLevel }, allCodes); + if (typeof logger === 'function') log.setLogger(logger as (formattedMsg: string, level: SplitIO.LogLevel, msg: string) => void); // @ts-ignore // if logLevel is undefined at this point, it means that settings `debug` value is invalid if (!logLevel) log._log(LogLevels.ERROR, 'Invalid Log Level - No changes to the logs will be applied.'); diff --git a/src/utils/settingsValidation/logger/commons.ts b/src/utils/settingsValidation/logger/commons.ts index 8c11cbbb..f34503df 100644 --- a/src/utils/settingsValidation/logger/commons.ts +++ b/src/utils/settingsValidation/logger/commons.ts @@ -10,15 +10,11 @@ import SplitIO from '../../../../types/splitio'; * @returns LogLevel of the given debugValue or undefined if the provided value is invalid */ export function getLogLevel(debugValue: unknown): SplitIO.LogLevel | undefined { - if (typeof debugValue === 'boolean') { - if (debugValue) { - return LogLevels.DEBUG; - } else { - return LogLevels.NONE; - } - } else if (typeof debugValue === 'string' && isLogLevelString(debugValue)) { - return debugValue; - } else { - return undefined; - } + return typeof debugValue === 'boolean' ? + debugValue ? + LogLevels.DEBUG : + LogLevels.NONE : + typeof debugValue === 'string' && isLogLevelString(debugValue) ? + debugValue : + undefined; } diff --git a/src/utils/settingsValidation/logger/pluggableLogger.ts b/src/utils/settingsValidation/logger/pluggableLogger.ts index 063134c9..05172d5f 100644 --- a/src/utils/settingsValidation/logger/pluggableLogger.ts +++ b/src/utils/settingsValidation/logger/pluggableLogger.ts @@ -17,16 +17,20 @@ let initialLogLevel = LogLevels.NONE; * @returns a logger instance, that might be: the provided logger at `settings.debug`, or one with the given `debug` log level, * or one with NONE log level if `debug` is not defined or invalid. */ -export function validateLogger(settings: { debug: unknown }): ILogger { - const { debug } = settings; +export function validateLogger(settings: { debug: unknown, logger?: unknown }): ILogger { + const { debug, logger } = settings; let logLevel: SplitIO.LogLevel | undefined = initialLogLevel; if (debug !== undefined) { - if (isLogger(debug)) return debug; + if (isLogger(debug)) { + if (typeof logger === 'function') debug.setLogger(logger as (formattedMsg: string, level: SplitIO.LogLevel, msg: string) => void); + return debug; + } logLevel = getLogLevel(settings.debug); } const log = new Logger({ logLevel: logLevel || initialLogLevel }); + if (typeof logger === 'function') log.setLogger(logger as (formattedMsg: string, level: SplitIO.LogLevel, msg: string) => void); // @ts-ignore // `debug` value is invalid if logLevel is undefined at this point if (!logLevel) log._log(LogLevels.ERROR, 'Invalid `debug` value at config. Logs will be disabled.'); diff --git a/types/splitio.d.ts b/types/splitio.d.ts index 377d3234..956a063b 100644 --- a/types/splitio.d.ts +++ b/types/splitio.d.ts @@ -87,6 +87,7 @@ interface ISharedSettings { * Do not change these settings unless you're working an advanced use case, like connecting to the Split proxy. */ urls?: SplitIO.UrlSettings; + logger?: (formattedMsg: string, level: SplitIO.LogLevel, msg: string) => void; } /** * Common settings properties for SDKs with synchronous API (standalone and localhost modes). @@ -559,6 +560,7 @@ declare namespace SplitIO { telemetry: string; }; readonly integrations?: IntegrationFactory[]; + readonly logger?: (formattedMsg: string, level: SplitIO.LogLevel, msg: string) => void; readonly debug: boolean | LogLevel | ILogger; readonly version: string; /** From 77a56de39de81a09ff4825e18f0dcb365ad7d63b Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Wed, 10 Sep 2025 13:48:59 -0300 Subject: [PATCH 02/10] Implement custom logger interface based on console methods --- src/logger/__tests__/index.spec.ts | 7 ----- src/logger/index.ts | 28 +++++++++++-------- src/logger/types.ts | 3 +- .../logger/__tests__/index.spec.ts | 19 ++++++++++--- .../logger/builtinLogger.ts | 6 ++-- .../settingsValidation/logger/commons.ts | 4 +++ .../logger/pluggableLogger.ts | 14 +++++----- types/splitio.d.ts | 13 +++++++-- 8 files changed, 58 insertions(+), 36 deletions(-) diff --git a/src/logger/__tests__/index.spec.ts b/src/logger/__tests__/index.spec.ts index 6e76c0a5..ec6533be 100644 --- a/src/logger/__tests__/index.spec.ts +++ b/src/logger/__tests__/index.spec.ts @@ -16,12 +16,10 @@ test('SPLIT LOGGER / isLogLevelString utility function', () => { expect(isLogLevelString(LOG_LEVELS.DEBUG)).toBe(true); // Calling isLogLevelString should return true with a LOG_LEVELS value expect(isLogLevelString('ERROR')).toBe(true); // Calling isLogLevelString should return true with a string equal to some LOG_LEVELS value expect(isLogLevelString('INVALID LOG LEVEL')).toBe(false); // Calling isLogLevelString should return false with a string not equal to any LOG_LEVELS value - }); test('SPLIT LOGGER / LogLevels exposed mappings', () => { expect(LogLevels).toEqual(LOG_LEVELS); // Exposed log levels should contain the levels we want. - }); test('SPLIT LOGGER / Logger class shape', () => { @@ -94,27 +92,22 @@ function testLogLevels(levelToTest: SplitIO.LogLevel) { // Restore spied object. consoleLogSpy.mockRestore(); - } test('SPLIT LOGGER / Logger class public methods behavior - instance.debug', () => { testLogLevels(LogLevels.DEBUG); - }); test('SPLIT LOGGER / Logger class public methods behavior - instance.info', () => { testLogLevels(LogLevels.INFO); - }); test('SPLIT LOGGER / Logger class public methods behavior - instance.warn', () => { testLogLevels(LogLevels.WARN); - }); test('SPLIT LOGGER / Logger class public methods behavior - instance.error', () => { testLogLevels(LogLevels.ERROR); - }); test('SPLIT LOGGER / _sprintf', () => { diff --git a/src/logger/index.ts b/src/logger/index.ts index 0663bd12..1ec9da83 100644 --- a/src/logger/index.ts +++ b/src/logger/index.ts @@ -41,7 +41,13 @@ const defaultOptions = { prefix: 'splitio', logLevel: LogLevels.NONE, showLevel: true, - logger(formattedMsg: string) { console.log(formattedMsg); } +}; + +const defaultConsoleLogger: SplitIO.Logger = { + debug(message: string) { console.log(message); }, + info(message: string) { console.log(message); }, + warn(message: string) { console.log(message); }, + error(message: string) { console.log(message); } }; export class Logger implements ILogger { @@ -49,15 +55,17 @@ export class Logger implements ILogger { private options: Required; private codes: Map; private logLevel: number; + private logger: SplitIO.Logger; constructor(options?: ILoggerOptions, codes?: Map) { this.options = objectAssign({}, defaultOptions, options); this.codes = codes || new Map(); this.logLevel = LogLevelIndexes[this.options.logLevel]; + this.logger = defaultConsoleLogger; } - setLogger(logger: (formattedMsg: string, level: SplitIO.LogLevel, msg: string) => void) { - this.options.logger = logger; + setLogger(logger: SplitIO.Logger) { + this.logger = logger; } setLogLevel(logLevel: SplitIO.LogLevel) { @@ -66,22 +74,22 @@ export class Logger implements ILogger { } debug(msg: string | number, args?: any[]) { - if (this._shouldLog(LogLevelIndexes.DEBUG)) this._log(LogLevels.DEBUG, msg, args); + if (this._shouldLog(LogLevelIndexes.DEBUG)) this.logger.debug(this._log(LogLevels.DEBUG, msg, args)); } info(msg: string | number, args?: any[]) { - if (this._shouldLog(LogLevelIndexes.INFO)) this._log(LogLevels.INFO, msg, args); + if (this._shouldLog(LogLevelIndexes.INFO)) this.logger.info(this._log(LogLevels.INFO, msg, args)); } warn(msg: string | number, args?: any[]) { - if (this._shouldLog(LogLevelIndexes.WARN)) this._log(LogLevels.WARN, msg, args); + if (this._shouldLog(LogLevelIndexes.WARN)) this.logger.warn(this._log(LogLevels.WARN, msg, args)); } error(msg: string | number, args?: any[]) { - if (this._shouldLog(LogLevelIndexes.ERROR)) this._log(LogLevels.ERROR, msg, args); + if (this._shouldLog(LogLevelIndexes.ERROR)) this.logger.error(this._log(LogLevels.ERROR, msg, args)); } - private _log(level: SplitIO.LogLevel, msg: string | number, args?: any[]) { + private _log(level: SplitIO.LogLevel, msg: string | number, args?: any[]): string { if (typeof msg === 'number') { const format = this.codes.get(msg); msg = format ? _sprintf(format, args) : `Message code ${msg}${args ? ', with args: ' + args.toString() : ''}`; @@ -89,9 +97,7 @@ export class Logger implements ILogger { if (args) msg = _sprintf(msg, args); } - const formattedText = this._generateLogMessage(level, msg); - - this.options.logger(formattedText, level, msg); + return this._generateLogMessage(level, msg); } private _generateLogMessage(level: SplitIO.LogLevel, text: string) { diff --git a/src/logger/types.ts b/src/logger/types.ts index 2987971d..b76afc85 100644 --- a/src/logger/types.ts +++ b/src/logger/types.ts @@ -4,11 +4,10 @@ export interface ILoggerOptions { prefix?: string; logLevel?: SplitIO.LogLevel; showLevel?: boolean; // @TODO remove this param eventually since it is not being set `false` anymore - logger?: (formattedMsg: string, level: SplitIO.LogLevel, msg: string) => void; } export interface ILogger extends SplitIO.ILogger { - setLogger(logger: (formattedMsg: string, level: SplitIO.LogLevel, msg: string) => void): void; + setLogger(logger: SplitIO.Logger): void; debug(msg: any): void; debug(msg: string | number, args?: any[]): void; diff --git a/src/utils/settingsValidation/logger/__tests__/index.spec.ts b/src/utils/settingsValidation/logger/__tests__/index.spec.ts index 3f91c2db..9fc977fb 100644 --- a/src/utils/settingsValidation/logger/__tests__/index.spec.ts +++ b/src/utils/settingsValidation/logger/__tests__/index.spec.ts @@ -3,6 +3,13 @@ import { loggerMock, getLoggerLogLevel } from '../../../../logger/__tests__/sdkL import { validateLogger as pluggableValidateLogger } from '../pluggableLogger'; import { validateLogger as builtinValidateLogger } from '../builtinLogger'; +const customLogger = { + debug: jest.fn(), + info: jest.fn(), + warn: jest.fn(), + error: jest.fn() +}; + const testTargets = [ [pluggableValidateLogger], [builtinValidateLogger] @@ -11,7 +18,10 @@ const testTargets = [ describe('logger validators', () => { const consoleLogSpy = jest.spyOn(global.console, 'log'); - afterEach(() => { consoleLogSpy.mockClear(); }); + afterEach(() => { + consoleLogSpy.mockClear(); + customLogger.error.mockClear(); + }); test.each(testTargets)('returns a NONE logger if `debug` property is not defined or false', (validateLogger) => { // @ts-ignore expect(getLoggerLogLevel(validateLogger({}))).toBe('NONE'); @@ -24,15 +34,16 @@ describe('logger validators', () => { test.each(testTargets)('returns a NONE logger if `debug` property is invalid and logs the error', (validateLogger) => { expect(getLoggerLogLevel(validateLogger({ debug: null }))).toBe('NONE'); expect(getLoggerLogLevel(validateLogger({ debug: 10 }))).toBe('NONE'); - expect(getLoggerLogLevel(validateLogger({ debug: {} }))).toBe('NONE'); + expect(getLoggerLogLevel(validateLogger({ debug: {}, logger: customLogger }))).toBe('NONE'); if (validateLogger === builtinValidateLogger) { // for builtinValidateLogger, a logger cannot be passed as `debug` property expect(getLoggerLogLevel(validateLogger({ debug: loggerMock }))).toBe('NONE'); - expect(consoleLogSpy).toBeCalledTimes(4); - } else { expect(consoleLogSpy).toBeCalledTimes(3); + } else { + expect(consoleLogSpy).toBeCalledTimes(2); } + expect(customLogger.error).toBeCalledTimes(1); }); test.each(testTargets)('returns a logger with the provided log level if `debug` property is true or a string log level', (validateLogger) => { diff --git a/src/utils/settingsValidation/logger/builtinLogger.ts b/src/utils/settingsValidation/logger/builtinLogger.ts index 6448d9f0..1927fc44 100644 --- a/src/utils/settingsValidation/logger/builtinLogger.ts +++ b/src/utils/settingsValidation/logger/builtinLogger.ts @@ -3,7 +3,7 @@ import { ILogger } from '../../../logger/types'; import { isLocalStorageAvailable } from '../../env/isLocalStorageAvailable'; import { isNode } from '../../env/isNode'; import { codesDebug } from '../../../logger/messages/debug'; -import { getLogLevel } from './commons'; +import { getLogLevel, isLogger } from './commons'; import SplitIO from '../../../../types/splitio'; const allCodes = new Map(codesDebug); @@ -46,10 +46,10 @@ export function validateLogger(settings: { debug: unknown, logger?: unknown }): const logLevel: SplitIO.LogLevel | undefined = debug !== undefined ? getLogLevel(debug) : initialLogLevel; const log = new Logger({ logLevel: logLevel || initialLogLevel }, allCodes); - if (typeof logger === 'function') log.setLogger(logger as (formattedMsg: string, level: SplitIO.LogLevel, msg: string) => void); + if (isLogger(logger)) log.setLogger(logger); // @ts-ignore // if logLevel is undefined at this point, it means that settings `debug` value is invalid - if (!logLevel) log._log(LogLevels.ERROR, 'Invalid Log Level - No changes to the logs will be applied.'); + if (!logLevel) log.logger.error(log._log(LogLevels.ERROR, 'Invalid Log Level - No changes to the logs will be applied.')); return log; } diff --git a/src/utils/settingsValidation/logger/commons.ts b/src/utils/settingsValidation/logger/commons.ts index f34503df..fe27151b 100644 --- a/src/utils/settingsValidation/logger/commons.ts +++ b/src/utils/settingsValidation/logger/commons.ts @@ -18,3 +18,7 @@ export function getLogLevel(debugValue: unknown): SplitIO.LogLevel | undefined { debugValue : undefined; } + +export function isLogger(log: any): log is SplitIO.Logger { + return log !== null && typeof log === 'object' && typeof log.debug === 'function' && typeof log.info === 'function' && typeof log.warn === 'function' && typeof log.error === 'function'; +} diff --git a/src/utils/settingsValidation/logger/pluggableLogger.ts b/src/utils/settingsValidation/logger/pluggableLogger.ts index 05172d5f..74602658 100644 --- a/src/utils/settingsValidation/logger/pluggableLogger.ts +++ b/src/utils/settingsValidation/logger/pluggableLogger.ts @@ -1,10 +1,10 @@ import { Logger, LogLevels } from '../../../logger'; import { ILogger } from '../../../logger/types'; import SplitIO from '../../../../types/splitio'; -import { getLogLevel } from './commons'; +import { getLogLevel, isLogger } from './commons'; -function isLogger(log: any): log is ILogger { - return log !== null && typeof log === 'object' && typeof log.debug === 'function' && typeof log.info === 'function' && typeof log.warn === 'function' && typeof log.error === 'function' && typeof log.setLogLevel === 'function'; +function isILogger(log: any): log is ILogger { + return isLogger(log) && typeof (log as any).setLogLevel === 'function'; } // By default it starts disabled. @@ -22,18 +22,18 @@ export function validateLogger(settings: { debug: unknown, logger?: unknown }): let logLevel: SplitIO.LogLevel | undefined = initialLogLevel; if (debug !== undefined) { - if (isLogger(debug)) { - if (typeof logger === 'function') debug.setLogger(logger as (formattedMsg: string, level: SplitIO.LogLevel, msg: string) => void); + if (isILogger(debug)) { + if (isLogger(logger)) debug.setLogger(logger); return debug; } logLevel = getLogLevel(settings.debug); } const log = new Logger({ logLevel: logLevel || initialLogLevel }); - if (typeof logger === 'function') log.setLogger(logger as (formattedMsg: string, level: SplitIO.LogLevel, msg: string) => void); + if (isLogger(logger)) log.setLogger(logger); // @ts-ignore // `debug` value is invalid if logLevel is undefined at this point - if (!logLevel) log._log(LogLevels.ERROR, 'Invalid `debug` value at config. Logs will be disabled.'); + if (!logLevel) log.logger.error(log._log(LogLevels.ERROR, 'Invalid `debug` value at config. Logs will be disabled.')); return log; } diff --git a/types/splitio.d.ts b/types/splitio.d.ts index 52ea1288..6426c728 100644 --- a/types/splitio.d.ts +++ b/types/splitio.d.ts @@ -91,7 +91,7 @@ interface ISharedSettings { * Do not change these settings unless you're working an advanced use case, like connecting to the Split proxy. */ urls?: SplitIO.UrlSettings; - logger?: (formattedMsg: string, level: SplitIO.LogLevel, msg: string) => void; + logger?: SplitIO.Logger; } /** * Common settings properties for SDKs with synchronous API (standalone and localhost modes). @@ -564,7 +564,7 @@ declare namespace SplitIO { telemetry: string; }; readonly integrations?: IntegrationFactory[]; - readonly logger?: (formattedMsg: string, level: SplitIO.LogLevel, msg: string) => void; + readonly logger?: Logger; readonly debug: boolean | LogLevel | ILogger; readonly version: string; /** @@ -595,6 +595,15 @@ declare namespace SplitIO { * Log levels. */ type LogLevel = 'DEBUG' | 'INFO' | 'WARN' | 'ERROR' | 'NONE'; + /** + * Custom logger interface. + */ + interface Logger { + debug(message: string): any; + info(message: string): any; + warn(message: string): any; + error(message: string): any; + } /** * Logger API */ From e9767a46c114fe238b57632ab287b2911d5a0efb Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Mon, 29 Sep 2025 14:32:15 -0300 Subject: [PATCH 03/10] Tests and polishing --- src/logger/index.ts | 46 ++++++----- src/logger/types.ts | 2 +- .../logger/__tests__/index.spec.ts | 79 +++++++++++++++++-- .../logger/builtinLogger.ts | 8 +- .../logger/pluggableLogger.ts | 8 +- types/splitio.d.ts | 5 ++ 6 files changed, 115 insertions(+), 33 deletions(-) diff --git a/src/logger/index.ts b/src/logger/index.ts index 1ec9da83..20047e47 100644 --- a/src/logger/index.ts +++ b/src/logger/index.ts @@ -2,6 +2,7 @@ import { objectAssign } from '../utils/lang/objectAssign'; import { ILoggerOptions, ILogger } from './types'; import { find, isObject } from '../utils/lang'; import SplitIO from '../../types/splitio'; +import { isLogger } from '../utils/settingsValidation/logger/commons'; export const LogLevels: SplitIO.ILoggerAPI['LogLevel'] = { DEBUG: 'DEBUG', @@ -43,29 +44,17 @@ const defaultOptions = { showLevel: true, }; -const defaultConsoleLogger: SplitIO.Logger = { - debug(message: string) { console.log(message); }, - info(message: string) { console.log(message); }, - warn(message: string) { console.log(message); }, - error(message: string) { console.log(message); } -}; - export class Logger implements ILogger { private options: Required; private codes: Map; private logLevel: number; - private logger: SplitIO.Logger; + private logger?: SplitIO.Logger; constructor(options?: ILoggerOptions, codes?: Map) { this.options = objectAssign({}, defaultOptions, options); this.codes = codes || new Map(); this.logLevel = LogLevelIndexes[this.options.logLevel]; - this.logger = defaultConsoleLogger; - } - - setLogger(logger: SplitIO.Logger) { - this.logger = logger; } setLogLevel(logLevel: SplitIO.LogLevel) { @@ -73,23 +62,32 @@ export class Logger implements ILogger { this.logLevel = LogLevelIndexes[logLevel]; } + setLogger(logger?: SplitIO.Logger) { + if (!logger || isLogger(logger)) { + this.logger = logger; + } else { + this._log(LogLevels.ERROR, 'Invalid `logger` instance. It must be an object with `debug`, `info`, `warn` and `error` methods. Defaulting to `console.log`'); + this.logger = undefined; + } + } + debug(msg: string | number, args?: any[]) { - if (this._shouldLog(LogLevelIndexes.DEBUG)) this.logger.debug(this._log(LogLevels.DEBUG, msg, args)); + if (this._shouldLog(LogLevelIndexes.DEBUG)) this._log(LogLevels.DEBUG, msg, args); } info(msg: string | number, args?: any[]) { - if (this._shouldLog(LogLevelIndexes.INFO)) this.logger.info(this._log(LogLevels.INFO, msg, args)); + if (this._shouldLog(LogLevelIndexes.INFO)) this._log(LogLevels.INFO, msg, args); } warn(msg: string | number, args?: any[]) { - if (this._shouldLog(LogLevelIndexes.WARN)) this.logger.warn(this._log(LogLevels.WARN, msg, args)); + if (this._shouldLog(LogLevelIndexes.WARN)) this._log(LogLevels.WARN, msg, args); } error(msg: string | number, args?: any[]) { - if (this._shouldLog(LogLevelIndexes.ERROR)) this.logger.error(this._log(LogLevels.ERROR, msg, args)); + if (this._shouldLog(LogLevelIndexes.ERROR)) this._log(LogLevels.ERROR, msg, args); } - private _log(level: SplitIO.LogLevel, msg: string | number, args?: any[]): string { + private _log(level: SplitIO.LogLevel, msg: string | number, args?: any[]) { if (typeof msg === 'number') { const format = this.codes.get(msg); msg = format ? _sprintf(format, args) : `Message code ${msg}${args ? ', with args: ' + args.toString() : ''}`; @@ -97,7 +95,17 @@ export class Logger implements ILogger { if (args) msg = _sprintf(msg, args); } - return this._generateLogMessage(level, msg); + const formattedText = this._generateLogMessage(level, msg); + + // Do not break on custom logger errors + if (this.logger) { + try { // @ts-expect-error + this.logger[level.toLowerCase()](formattedText); + return; + } catch (e) { /* empty */ } + } + + console.log(formattedText); } private _generateLogMessage(level: SplitIO.LogLevel, text: string) { diff --git a/src/logger/types.ts b/src/logger/types.ts index b76afc85..1cb082fd 100644 --- a/src/logger/types.ts +++ b/src/logger/types.ts @@ -7,7 +7,7 @@ export interface ILoggerOptions { } export interface ILogger extends SplitIO.ILogger { - setLogger(logger: SplitIO.Logger): void; + setLogger(logger?: SplitIO.Logger): void; debug(msg: any): void; debug(msg: string | number, args?: any[]): void; diff --git a/src/utils/settingsValidation/logger/__tests__/index.spec.ts b/src/utils/settingsValidation/logger/__tests__/index.spec.ts index 9fc977fb..59dcd991 100644 --- a/src/utils/settingsValidation/logger/__tests__/index.spec.ts +++ b/src/utils/settingsValidation/logger/__tests__/index.spec.ts @@ -20,6 +20,9 @@ describe('logger validators', () => { const consoleLogSpy = jest.spyOn(global.console, 'log'); afterEach(() => { consoleLogSpy.mockClear(); + customLogger.debug.mockClear(); + customLogger.info.mockClear(); + customLogger.warn.mockClear(); customLogger.error.mockClear(); }); @@ -33,17 +36,16 @@ describe('logger validators', () => { test.each(testTargets)('returns a NONE logger if `debug` property is invalid and logs the error', (validateLogger) => { expect(getLoggerLogLevel(validateLogger({ debug: null }))).toBe('NONE'); - expect(getLoggerLogLevel(validateLogger({ debug: 10 }))).toBe('NONE'); - expect(getLoggerLogLevel(validateLogger({ debug: {}, logger: customLogger }))).toBe('NONE'); + expect(getLoggerLogLevel(validateLogger({ debug: 10, logger: undefined }))).toBe('NONE'); // @ts-expect-error invalid `logger`, ignored because it's falsy + expect(getLoggerLogLevel(validateLogger({ debug: {}, logger: false }))).toBe('NONE'); if (validateLogger === builtinValidateLogger) { // for builtinValidateLogger, a logger cannot be passed as `debug` property expect(getLoggerLogLevel(validateLogger({ debug: loggerMock }))).toBe('NONE'); - expect(consoleLogSpy).toBeCalledTimes(3); + expect(consoleLogSpy).toBeCalledTimes(4); } else { - expect(consoleLogSpy).toBeCalledTimes(2); + expect(consoleLogSpy).toBeCalledTimes(3); } - expect(customLogger.error).toBeCalledTimes(1); }); test.each(testTargets)('returns a logger with the provided log level if `debug` property is true or a string log level', (validateLogger) => { @@ -63,4 +65,71 @@ describe('logger validators', () => { expect(consoleLogSpy).not.toBeCalled(); }); + test.each(testTargets)('uses the provided custom logger if it is valid', (validateLogger) => { + const logger = validateLogger({ debug: true, logger: customLogger }); + + logger.debug('test debug'); + expect(customLogger.debug).toBeCalledWith('[DEBUG] splitio => test debug'); + + logger.info('test info'); + expect(customLogger.info).toBeCalledWith('[INFO] splitio => test info'); + + logger.warn('test warn'); + expect(customLogger.warn).toBeCalledWith('[WARN] splitio => test warn'); + + logger.error('test error'); + expect(customLogger.error).toBeCalledWith('[ERROR] splitio => test error'); + + expect(consoleLogSpy).not.toBeCalled(); + }); + + test.each(testTargets)('uses the default console.log method if the provided custom logger is not valid', (validateLogger) => { + // @ts-expect-error `logger` property is not valid + const logger = validateLogger({ debug: true, logger: {} }); + expect(consoleLogSpy).toBeCalledWith('[ERROR] splitio => Invalid `logger` instance. It must be an object with `debug`, `info`, `warn` and `error` methods. Defaulting to `console.log`'); + + logger.debug('test debug'); + expect(consoleLogSpy).toBeCalledWith('[DEBUG] splitio => test debug'); + + logger.info('test info'); + expect(consoleLogSpy).toBeCalledWith('[INFO] splitio => test info'); + + logger.warn('test warn'); + expect(consoleLogSpy).toBeCalledWith('[WARN] splitio => test warn'); + + logger.error('test error'); + expect(consoleLogSpy).toBeCalledWith('[ERROR] splitio => test error'); + + expect(consoleLogSpy).toBeCalledTimes(5); + }); + + test.each(testTargets)('uses the default console.log method if the provided custom logger throws an error', (validateLogger) => { + const customLoggerWithErrors = { + debug: jest.fn(() => { throw new Error('debug error'); }), + info: jest.fn(() => { throw new Error('info error'); }), + warn: jest.fn(() => { throw new Error('warn error'); }), + error: jest.fn(() => { throw new Error('error error'); }) + }; + + const logger = validateLogger({ debug: true, logger: customLoggerWithErrors }); + + logger.debug('test debug'); + expect(customLoggerWithErrors.debug).toBeCalledWith('[DEBUG] splitio => test debug'); + expect(consoleLogSpy).toBeCalledWith('[DEBUG] splitio => test debug'); + + logger.info('test info'); + expect(customLoggerWithErrors.info).toBeCalledWith('[INFO] splitio => test info'); + expect(consoleLogSpy).toBeCalledWith('[INFO] splitio => test info'); + + logger.warn('test warn'); + expect(customLoggerWithErrors.warn).toBeCalledWith('[WARN] splitio => test warn'); + expect(consoleLogSpy).toBeCalledWith('[WARN] splitio => test warn'); + + logger.error('test error'); + expect(customLoggerWithErrors.error).toBeCalledWith('[ERROR] splitio => test error'); + expect(consoleLogSpy).toBeCalledWith('[ERROR] splitio => test error'); + + expect(consoleLogSpy).toBeCalledTimes(4); + }); + }); diff --git a/src/utils/settingsValidation/logger/builtinLogger.ts b/src/utils/settingsValidation/logger/builtinLogger.ts index 1927fc44..cb66e856 100644 --- a/src/utils/settingsValidation/logger/builtinLogger.ts +++ b/src/utils/settingsValidation/logger/builtinLogger.ts @@ -3,7 +3,7 @@ import { ILogger } from '../../../logger/types'; import { isLocalStorageAvailable } from '../../env/isLocalStorageAvailable'; import { isNode } from '../../env/isNode'; import { codesDebug } from '../../../logger/messages/debug'; -import { getLogLevel, isLogger } from './commons'; +import { getLogLevel } from './commons'; import SplitIO from '../../../../types/splitio'; const allCodes = new Map(codesDebug); @@ -40,16 +40,16 @@ if (/^(enabled?|on)/i.test(initialState)) { * @param settings - user config object, with an optional `debug` property of type boolean or string log level. * @returns a logger instance with the log level at `settings.debug`. If `settings.debug` is invalid or not provided, `initialLogLevel` is used. */ -export function validateLogger(settings: { debug: unknown, logger?: unknown }): ILogger { +export function validateLogger(settings: { debug: unknown, logger?: SplitIO.Logger }): ILogger { const { debug, logger } = settings; const logLevel: SplitIO.LogLevel | undefined = debug !== undefined ? getLogLevel(debug) : initialLogLevel; const log = new Logger({ logLevel: logLevel || initialLogLevel }, allCodes); - if (isLogger(logger)) log.setLogger(logger); + log.setLogger(logger); // @ts-ignore // if logLevel is undefined at this point, it means that settings `debug` value is invalid - if (!logLevel) log.logger.error(log._log(LogLevels.ERROR, 'Invalid Log Level - No changes to the logs will be applied.')); + if (!logLevel) log._log(LogLevels.ERROR, 'Invalid Log Level - No changes to the logs will be applied.'); return log; } diff --git a/src/utils/settingsValidation/logger/pluggableLogger.ts b/src/utils/settingsValidation/logger/pluggableLogger.ts index 74602658..c3357f8f 100644 --- a/src/utils/settingsValidation/logger/pluggableLogger.ts +++ b/src/utils/settingsValidation/logger/pluggableLogger.ts @@ -17,23 +17,23 @@ let initialLogLevel = LogLevels.NONE; * @returns a logger instance, that might be: the provided logger at `settings.debug`, or one with the given `debug` log level, * or one with NONE log level if `debug` is not defined or invalid. */ -export function validateLogger(settings: { debug: unknown, logger?: unknown }): ILogger { +export function validateLogger(settings: { debug: unknown, logger?: SplitIO.Logger }): ILogger { const { debug, logger } = settings; let logLevel: SplitIO.LogLevel | undefined = initialLogLevel; if (debug !== undefined) { if (isILogger(debug)) { - if (isLogger(logger)) debug.setLogger(logger); + debug.setLogger(logger); return debug; } logLevel = getLogLevel(settings.debug); } const log = new Logger({ logLevel: logLevel || initialLogLevel }); - if (isLogger(logger)) log.setLogger(logger); + log.setLogger(logger); // @ts-ignore // `debug` value is invalid if logLevel is undefined at this point - if (!logLevel) log.logger.error(log._log(LogLevels.ERROR, 'Invalid `debug` value at config. Logs will be disabled.')); + if (!logLevel) log._log(LogLevels.ERROR, 'Invalid `debug` value at config. Logs will be disabled.'); return log; } diff --git a/types/splitio.d.ts b/types/splitio.d.ts index ce5c2ce0..5c1df3bc 100644 --- a/types/splitio.d.ts +++ b/types/splitio.d.ts @@ -91,6 +91,9 @@ interface ISharedSettings { * Do not change these settings unless you're working an advanced use case, like connecting to the Split proxy. */ urls?: SplitIO.UrlSettings; + /** + * Custom logger object. If not provided, the SDK will use the default `console.log` method for all log levels. + */ logger?: SplitIO.Logger; } /** @@ -642,6 +645,8 @@ declare namespace SplitIO { disable(): void; /** * Sets a log level for the SDK logs. + * + * @param logLevel - The log level to set. */ setLogLevel(logLevel: LogLevel): void; /** From f3dc0efd58afa69a7fd13d1934bd976f800c3b21 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Tue, 30 Sep 2025 15:33:06 -0300 Subject: [PATCH 04/10] Add setLogger method --- src/logger/sdkLogger.ts | 7 +++++++ types/splitio.d.ts | 6 ++++++ 2 files changed, 13 insertions(+) diff --git a/src/logger/sdkLogger.ts b/src/logger/sdkLogger.ts index 42d9be42..821945de 100644 --- a/src/logger/sdkLogger.ts +++ b/src/logger/sdkLogger.ts @@ -30,6 +30,13 @@ export function createLoggerAPI(log: ILogger): SplitIO.ILoggerAPI { * @param logLevel - Custom LogLevel value. */ setLogLevel, + /** + * Sets a custom logger for the SDK logs. + * @param logger - Custom logger. + */ + setLogger(logger?: ILogger) { + log.setLogger(logger); + }, /** * Disables all the log levels. */ diff --git a/types/splitio.d.ts b/types/splitio.d.ts index 5c1df3bc..38b59e82 100644 --- a/types/splitio.d.ts +++ b/types/splitio.d.ts @@ -649,6 +649,12 @@ declare namespace SplitIO { * @param logLevel - The log level to set. */ setLogLevel(logLevel: LogLevel): void; + /** + * Sets a custom logger for the SDK logs. + * + * @param logger - The custom logger to set, or `undefined` to remove the custom logger and fall back to the default `console.log` method. + */ + setLogger(logger?: Logger): void; /** * Log level constants. Use this to pass them to setLogLevel function. */ From c9109c22bf38766e6c53ddd658c49550af91a482 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Wed, 1 Oct 2025 11:07:19 -0300 Subject: [PATCH 05/10] rc --- CHANGES.txt | 3 +++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index da38cdb1..d47fe969 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,3 +1,6 @@ +2.7.0 (October XX, 2025) + - Added support for custom loggers: added `logger` configuration option and `LoggerAPI.setLogger` method to allow the SDK to use a custom logger. + 2.6.0 (September 18, 2025) - Added `storage.wrapper` configuration option to allow the SDK to use a custom storage wrapper for the storage type `LOCALSTORAGE`. Default value is `window.localStorage`. diff --git a/package-lock.json b/package-lock.json index d40871e0..99001485 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@splitsoftware/splitio-commons", - "version": "2.6.0", + "version": "2.6.1-rc.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@splitsoftware/splitio-commons", - "version": "2.6.0", + "version": "2.6.1-rc.0", "license": "Apache-2.0", "dependencies": { "@types/ioredis": "^4.28.0", diff --git a/package.json b/package.json index 9301b5bf..15fbc29d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio-commons", - "version": "2.6.0", + "version": "2.6.1-rc.0", "description": "Split JavaScript SDK common components", "main": "cjs/index.js", "module": "esm/index.js", From 26d12ef34a744530bde6e120bde0d3f0395b5b9f Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Wed, 1 Oct 2025 17:31:45 -0300 Subject: [PATCH 06/10] Tests --- src/logger/__tests__/sdkLogger.mock.ts | 5 +++++ src/logger/__tests__/sdkLogger.spec.ts | 10 +++++++++- src/logger/index.ts | 14 +++++++++----- .../logger/__tests__/index.spec.ts | 7 +++++++ types/splitio.d.ts | 4 ++++ 5 files changed, 34 insertions(+), 6 deletions(-) diff --git a/src/logger/__tests__/sdkLogger.mock.ts b/src/logger/__tests__/sdkLogger.mock.ts index e358a760..b715461d 100644 --- a/src/logger/__tests__/sdkLogger.mock.ts +++ b/src/logger/__tests__/sdkLogger.mock.ts @@ -14,9 +14,14 @@ export const loggerMock = { this.debug.mockClear(); this.info.mockClear(); this.setLogLevel.mockClear(); + this.setLogger.mockClear(); } }; export function getLoggerLogLevel(logger: any): SplitIO.LogLevel | undefined { if (logger) return logger.options.logLevel; } + +export function getCustomLogger(logger: any): SplitIO.Logger | undefined { + if (logger) return logger.logger; +} diff --git a/src/logger/__tests__/sdkLogger.spec.ts b/src/logger/__tests__/sdkLogger.spec.ts index a69a587c..88b11307 100644 --- a/src/logger/__tests__/sdkLogger.spec.ts +++ b/src/logger/__tests__/sdkLogger.spec.ts @@ -1,6 +1,6 @@ import { createLoggerAPI } from '../sdkLogger'; import { Logger, LogLevels } from '../index'; -import { getLoggerLogLevel } from './sdkLogger.mock'; +import { getLoggerLogLevel, getCustomLogger } from './sdkLogger.mock'; test('LoggerAPI / methods and props', () => { // creates a LoggerAPI instance @@ -26,4 +26,12 @@ test('LoggerAPI / methods and props', () => { expect(API.LogLevel).toEqual(LogLevels); // API object should have LogLevel prop including all available levels. + // valid custom logger + API.setLogger(console); + expect(getCustomLogger(logger)).toBe(console); + + // invalid custom logger + // @ts-expect-error + API.setLogger({}); + expect(getCustomLogger(logger)).toBeUndefined(); }); diff --git a/src/logger/index.ts b/src/logger/index.ts index 20047e47..c591468c 100644 --- a/src/logger/index.ts +++ b/src/logger/index.ts @@ -63,11 +63,15 @@ export class Logger implements ILogger { } setLogger(logger?: SplitIO.Logger) { - if (!logger || isLogger(logger)) { - this.logger = logger; - } else { - this._log(LogLevels.ERROR, 'Invalid `logger` instance. It must be an object with `debug`, `info`, `warn` and `error` methods. Defaulting to `console.log`'); - this.logger = undefined; + if (logger) { + if (isLogger(logger)) { + this.logger = logger; + // If custom logger is set, all logs are either enabled or disabled + if (this.logLevel !== LogLevelIndexes.NONE) this.setLogLevel(LogLevels.DEBUG); + } else { + this._log(LogLevels.ERROR, 'Invalid `logger` instance. It must be an object with `debug`, `info`, `warn` and `error` methods. Defaulting to `console.log`'); + this.logger = undefined; + } } } diff --git a/src/utils/settingsValidation/logger/__tests__/index.spec.ts b/src/utils/settingsValidation/logger/__tests__/index.spec.ts index 59dcd991..0f507891 100644 --- a/src/utils/settingsValidation/logger/__tests__/index.spec.ts +++ b/src/utils/settingsValidation/logger/__tests__/index.spec.ts @@ -56,6 +56,13 @@ describe('logger validators', () => { expect(getLoggerLogLevel(validateLogger({ debug: 'ERROR' }))).toBe('ERROR'); expect(getLoggerLogLevel(validateLogger({ debug: 'NONE' }))).toBe('NONE'); + // When combined with the `logger` option, any log level other than `NONE` (false) will be set to `DEBUG` (true) + expect(getLoggerLogLevel(validateLogger({ debug: 'DEBUG', logger: loggerMock }))).toBe('DEBUG'); + expect(getLoggerLogLevel(validateLogger({ debug: 'INFO', logger: loggerMock }))).toBe('DEBUG'); + expect(getLoggerLogLevel(validateLogger({ debug: 'WARN', logger: loggerMock }))).toBe('DEBUG'); + expect(getLoggerLogLevel(validateLogger({ debug: 'ERROR', logger: loggerMock }))).toBe('DEBUG'); + expect(getLoggerLogLevel(validateLogger({ debug: 'NONE', logger: loggerMock }))).toBe('NONE'); + expect(consoleLogSpy).not.toBeCalled(); }); diff --git a/types/splitio.d.ts b/types/splitio.d.ts index 38b59e82..ebeba4df 100644 --- a/types/splitio.d.ts +++ b/types/splitio.d.ts @@ -145,6 +145,8 @@ interface IPluggableSharedSettings { * config.debug = ErrorLogger() * ``` * + * When combined with the `logger` option, any log level other than `NONE` (false) will be set to `DEBUG` (true), delegating log level control to the custom logger. + * * @defaultValue `false` */ debug?: boolean | SplitIO.LogLevel | SplitIO.ILogger; @@ -168,6 +170,8 @@ interface INonPluggableSharedSettings { * config.debug = 'WARN' * ``` * + * When combined with the `logger` option, any log level other than `NONE` (false) will be set to `DEBUG` (true), delegating log level control to the custom logger. + * * @defaultValue `false` */ debug?: boolean | SplitIO.LogLevel; From f1a4155311ee1c992251f15d68092fe1a6e05b6e Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Fri, 3 Oct 2025 13:49:13 -0300 Subject: [PATCH 07/10] Don't show redundant level when using custom logger --- src/logger/__tests__/index.spec.ts | 33 ++++++++----- src/logger/__tests__/sdkLogger.spec.ts | 4 ++ src/logger/index.ts | 49 ++++++++----------- src/logger/types.ts | 1 - .../logger/__tests__/index.spec.ts | 16 +++--- .../logger/builtinLogger.ts | 4 +- .../logger/pluggableLogger.ts | 4 +- 7 files changed, 57 insertions(+), 54 deletions(-) diff --git a/src/logger/__tests__/index.spec.ts b/src/logger/__tests__/index.spec.ts index ec6533be..55b659a4 100644 --- a/src/logger/__tests__/index.spec.ts +++ b/src/logger/__tests__/index.spec.ts @@ -38,9 +38,9 @@ const LOG_LEVELS_IN_ORDER: SplitIO.LogLevel[] = ['DEBUG', 'INFO', 'WARN', 'ERROR /* Utility function to avoid repeating too much code */ function testLogLevels(levelToTest: SplitIO.LogLevel) { // Builds the expected message. - const buildExpectedMessage = (lvl: string, category: string, msg: string, showLevel?: boolean) => { + const buildExpectedMessage = (lvl: string, category: string, msg: string, useDefaultLogger?: boolean) => { let res = ''; - if (showLevel) res += '[' + lvl + ']' + (lvl.length === 4 ? ' ' : ' '); + if (useDefaultLogger) res += '[' + lvl + ']' + (lvl.length === 4 ? ' ' : ' '); res += category + ' => '; res += msg; return res; @@ -49,24 +49,33 @@ function testLogLevels(levelToTest: SplitIO.LogLevel) { // Spy console.log const consoleLogSpy = jest.spyOn(global.console, 'log'); - // Runs the suite with the given value for showLevel option. - const runTests = (showLevel?: boolean, useCodes?: boolean) => { + // Runs the suite with the given values + const runTests = (useDefaultLogger?: boolean, useCodes?: boolean) => { let logLevelLogsCounter = 0; let testForNoLog = false; const logMethod = levelToTest.toLowerCase(); const logCategory = `test-category-${logMethod}`; - const instance = new Logger({ prefix: logCategory, showLevel }, - useCodes ? new Map([[1, 'Test log for level %s with showLevel: %s %s']]) : undefined); + const instance = new Logger({ prefix: logCategory }, + useCodes ? new Map([[1, 'Test log for level %s with default logger: %s %s']]) : undefined); + if (!useDefaultLogger) { + instance.setLogger({ + debug: console.log, + info: console.log, + warn: console.log, + error: console.log, + }); + } + LOG_LEVELS_IN_ORDER.forEach((logLevel, i) => { - const logMsg = `Test log for level ${levelToTest} with showLevel: ${showLevel} ${logLevelLogsCounter}`; - const expectedMessage = buildExpectedMessage(levelToTest, logCategory, logMsg, showLevel); + const logMsg = `Test log for level ${levelToTest} with default logger: ${useDefaultLogger} ${logLevelLogsCounter}`; + const expectedMessage = buildExpectedMessage(levelToTest, logCategory, logMsg, useDefaultLogger); // Set the logLevel for this iteration. instance.setLogLevel(LogLevels[logLevel]); // Call the method // @ts-ignore - if (useCodes) instance[logMethod](1, [levelToTest, showLevel, logLevelLogsCounter]); // @ts-ignore + if (useCodes) instance[logMethod](1, [levelToTest, useDefaultLogger, logLevelLogsCounter]); // @ts-ignore else instance[logMethod](logMsg); // Assert if console.log was called. const actualMessage = consoleLogSpy.mock.calls[consoleLogSpy.mock.calls.length - 1][0]; @@ -83,11 +92,11 @@ function testLogLevels(levelToTest: SplitIO.LogLevel) { }); }; - // Show logLevel + // Default console.log (Show level in logs) runTests(true); - // Hide logLevel + // Custom logger (Don't show level in logs) runTests(false); - // Hide logLevel and use message codes + // Custom logger (Don't show level in logs) and use message codes runTests(false, true); // Restore spied object. diff --git a/src/logger/__tests__/sdkLogger.spec.ts b/src/logger/__tests__/sdkLogger.spec.ts index 88b11307..5409f554 100644 --- a/src/logger/__tests__/sdkLogger.spec.ts +++ b/src/logger/__tests__/sdkLogger.spec.ts @@ -30,6 +30,10 @@ test('LoggerAPI / methods and props', () => { API.setLogger(console); expect(getCustomLogger(logger)).toBe(console); + // unset custom logger + API.setLogger(undefined); + expect(getCustomLogger(logger)).toBeUndefined(); + // invalid custom logger // @ts-expect-error API.setLogger({}); diff --git a/src/logger/index.ts b/src/logger/index.ts index c591468c..56f3c2de 100644 --- a/src/logger/index.ts +++ b/src/logger/index.ts @@ -20,6 +20,13 @@ const LogLevelIndexes = { NONE: 5 }; +const DEFAULT_LOGGER: SplitIO.Logger = { + debug(msg) { console.log('[DEBUG] ' + msg); }, + info(msg) { console.log('[INFO] ' + msg); }, + warn(msg) { console.log('[WARN] ' + msg); }, + error(msg) { console.log('[ERROR] ' + msg); } +}; + export function isLogLevelString(str: string): str is SplitIO.LogLevel { return !!find(LogLevels, (lvl: string) => str === lvl); } @@ -41,7 +48,6 @@ export function _sprintf(format: string = '', args: any[] = []): string { const defaultOptions = { prefix: 'splitio', logLevel: LogLevels.NONE, - showLevel: true, }; export class Logger implements ILogger { @@ -68,30 +74,32 @@ export class Logger implements ILogger { this.logger = logger; // If custom logger is set, all logs are either enabled or disabled if (this.logLevel !== LogLevelIndexes.NONE) this.setLogLevel(LogLevels.DEBUG); + return; } else { - this._log(LogLevels.ERROR, 'Invalid `logger` instance. It must be an object with `debug`, `info`, `warn` and `error` methods. Defaulting to `console.log`'); - this.logger = undefined; + this.error('Invalid `logger` instance. It must be an object with `debug`, `info`, `warn` and `error` methods. Defaulting to `console.log`'); } } + // unset + this.logger = undefined; } debug(msg: string | number, args?: any[]) { - if (this._shouldLog(LogLevelIndexes.DEBUG)) this._log(LogLevels.DEBUG, msg, args); + if (this._shouldLog(LogLevelIndexes.DEBUG)) this._log('debug', msg, args); } info(msg: string | number, args?: any[]) { - if (this._shouldLog(LogLevelIndexes.INFO)) this._log(LogLevels.INFO, msg, args); + if (this._shouldLog(LogLevelIndexes.INFO)) this._log('info', msg, args); } warn(msg: string | number, args?: any[]) { - if (this._shouldLog(LogLevelIndexes.WARN)) this._log(LogLevels.WARN, msg, args); + if (this._shouldLog(LogLevelIndexes.WARN)) this._log('warn', msg, args); } error(msg: string | number, args?: any[]) { - if (this._shouldLog(LogLevelIndexes.ERROR)) this._log(LogLevels.ERROR, msg, args); + if (this._shouldLog(LogLevelIndexes.ERROR)) this._log('error', msg, args); } - private _log(level: SplitIO.LogLevel, msg: string | number, args?: any[]) { + _log(method: keyof SplitIO.Logger, msg: string | number, args?: any[]) { if (typeof msg === 'number') { const format = this.codes.get(msg); msg = format ? _sprintf(format, args) : `Message code ${msg}${args ? ', with args: ' + args.toString() : ''}`; @@ -99,32 +107,15 @@ export class Logger implements ILogger { if (args) msg = _sprintf(msg, args); } - const formattedText = this._generateLogMessage(level, msg); + if (this.options.prefix) msg = this.options.prefix + ' => ' + msg; - // Do not break on custom logger errors if (this.logger) { - try { // @ts-expect-error - this.logger[level.toLowerCase()](formattedText); + try { + this.logger[method](msg); return; } catch (e) { /* empty */ } } - - console.log(formattedText); - } - - private _generateLogMessage(level: SplitIO.LogLevel, text: string) { - const textPre = ' => '; - let result = ''; - - if (this.options.showLevel) { - result += '[' + level + ']' + (level === LogLevels.INFO || level === LogLevels.WARN ? ' ' : '') + ' '; - } - - if (this.options.prefix) { - result += this.options.prefix + textPre; - } - - return result += text; + DEFAULT_LOGGER[method](msg); } private _shouldLog(level: number) { diff --git a/src/logger/types.ts b/src/logger/types.ts index 1cb082fd..867cd841 100644 --- a/src/logger/types.ts +++ b/src/logger/types.ts @@ -3,7 +3,6 @@ import SplitIO from '../../types/splitio'; export interface ILoggerOptions { prefix?: string; logLevel?: SplitIO.LogLevel; - showLevel?: boolean; // @TODO remove this param eventually since it is not being set `false` anymore } export interface ILogger extends SplitIO.ILogger { diff --git a/src/utils/settingsValidation/logger/__tests__/index.spec.ts b/src/utils/settingsValidation/logger/__tests__/index.spec.ts index 0f507891..9b7e765a 100644 --- a/src/utils/settingsValidation/logger/__tests__/index.spec.ts +++ b/src/utils/settingsValidation/logger/__tests__/index.spec.ts @@ -76,16 +76,16 @@ describe('logger validators', () => { const logger = validateLogger({ debug: true, logger: customLogger }); logger.debug('test debug'); - expect(customLogger.debug).toBeCalledWith('[DEBUG] splitio => test debug'); + expect(customLogger.debug).toBeCalledWith('splitio => test debug'); logger.info('test info'); - expect(customLogger.info).toBeCalledWith('[INFO] splitio => test info'); + expect(customLogger.info).toBeCalledWith('splitio => test info'); logger.warn('test warn'); - expect(customLogger.warn).toBeCalledWith('[WARN] splitio => test warn'); + expect(customLogger.warn).toBeCalledWith('splitio => test warn'); logger.error('test error'); - expect(customLogger.error).toBeCalledWith('[ERROR] splitio => test error'); + expect(customLogger.error).toBeCalledWith('splitio => test error'); expect(consoleLogSpy).not.toBeCalled(); }); @@ -121,19 +121,19 @@ describe('logger validators', () => { const logger = validateLogger({ debug: true, logger: customLoggerWithErrors }); logger.debug('test debug'); - expect(customLoggerWithErrors.debug).toBeCalledWith('[DEBUG] splitio => test debug'); + expect(customLoggerWithErrors.debug).toBeCalledWith('splitio => test debug'); expect(consoleLogSpy).toBeCalledWith('[DEBUG] splitio => test debug'); logger.info('test info'); - expect(customLoggerWithErrors.info).toBeCalledWith('[INFO] splitio => test info'); + expect(customLoggerWithErrors.info).toBeCalledWith('splitio => test info'); expect(consoleLogSpy).toBeCalledWith('[INFO] splitio => test info'); logger.warn('test warn'); - expect(customLoggerWithErrors.warn).toBeCalledWith('[WARN] splitio => test warn'); + expect(customLoggerWithErrors.warn).toBeCalledWith('splitio => test warn'); expect(consoleLogSpy).toBeCalledWith('[WARN] splitio => test warn'); logger.error('test error'); - expect(customLoggerWithErrors.error).toBeCalledWith('[ERROR] splitio => test error'); + expect(customLoggerWithErrors.error).toBeCalledWith('splitio => test error'); expect(consoleLogSpy).toBeCalledWith('[ERROR] splitio => test error'); expect(consoleLogSpy).toBeCalledTimes(4); diff --git a/src/utils/settingsValidation/logger/builtinLogger.ts b/src/utils/settingsValidation/logger/builtinLogger.ts index cb66e856..7fce3078 100644 --- a/src/utils/settingsValidation/logger/builtinLogger.ts +++ b/src/utils/settingsValidation/logger/builtinLogger.ts @@ -48,8 +48,8 @@ export function validateLogger(settings: { debug: unknown, logger?: SplitIO.Logg const log = new Logger({ logLevel: logLevel || initialLogLevel }, allCodes); log.setLogger(logger); - // @ts-ignore // if logLevel is undefined at this point, it means that settings `debug` value is invalid - if (!logLevel) log._log(LogLevels.ERROR, 'Invalid Log Level - No changes to the logs will be applied.'); + // if logLevel is undefined at this point, it means that settings `debug` value is invalid + if (!logLevel) log._log('error', 'Invalid Log Level - No changes to the logs will be applied.'); return log; } diff --git a/src/utils/settingsValidation/logger/pluggableLogger.ts b/src/utils/settingsValidation/logger/pluggableLogger.ts index c3357f8f..22deaf92 100644 --- a/src/utils/settingsValidation/logger/pluggableLogger.ts +++ b/src/utils/settingsValidation/logger/pluggableLogger.ts @@ -32,8 +32,8 @@ export function validateLogger(settings: { debug: unknown, logger?: SplitIO.Logg const log = new Logger({ logLevel: logLevel || initialLogLevel }); log.setLogger(logger); - // @ts-ignore // `debug` value is invalid if logLevel is undefined at this point - if (!logLevel) log._log(LogLevels.ERROR, 'Invalid `debug` value at config. Logs will be disabled.'); + // `debug` value is invalid if logLevel is undefined at this point + if (!logLevel) log._log('error', 'Invalid `debug` value at config. Logs will be disabled.'); return log; } From a6f2bd38d9018dae6d3d6deab5b89396468a1623 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Fri, 3 Oct 2025 15:44:58 -0300 Subject: [PATCH 08/10] rc --- CHANGES.txt | 2 +- package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index d47fe969..b5364214 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,4 +1,4 @@ -2.7.0 (October XX, 2025) +2.7.0 (October 6, 2025) - Added support for custom loggers: added `logger` configuration option and `LoggerAPI.setLogger` method to allow the SDK to use a custom logger. 2.6.0 (September 18, 2025) diff --git a/package-lock.json b/package-lock.json index 99001485..644d03ca 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@splitsoftware/splitio-commons", - "version": "2.6.1-rc.0", + "version": "2.6.1-rc.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@splitsoftware/splitio-commons", - "version": "2.6.1-rc.0", + "version": "2.6.1-rc.1", "license": "Apache-2.0", "dependencies": { "@types/ioredis": "^4.28.0", diff --git a/package.json b/package.json index 15fbc29d..4bef1330 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio-commons", - "version": "2.6.1-rc.0", + "version": "2.6.1-rc.1", "description": "Split JavaScript SDK common components", "main": "cjs/index.js", "module": "esm/index.js", From a7470ddd7120737c75cced3d4e61a413f1f1bbd0 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Mon, 6 Oct 2025 14:21:33 -0300 Subject: [PATCH 09/10] stable version --- package-lock.json | 4 ++-- package.json | 2 +- src/logger/index.ts | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index 644d03ca..f669aefa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@splitsoftware/splitio-commons", - "version": "2.6.1-rc.1", + "version": "2.7.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@splitsoftware/splitio-commons", - "version": "2.6.1-rc.1", + "version": "2.7.0", "license": "Apache-2.0", "dependencies": { "@types/ioredis": "^4.28.0", diff --git a/package.json b/package.json index 4bef1330..4f3fccb4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio-commons", - "version": "2.6.1-rc.1", + "version": "2.7.0", "description": "Split JavaScript SDK common components", "main": "cjs/index.js", "module": "esm/index.js", diff --git a/src/logger/index.ts b/src/logger/index.ts index 56f3c2de..09c3c7a9 100644 --- a/src/logger/index.ts +++ b/src/logger/index.ts @@ -20,7 +20,7 @@ const LogLevelIndexes = { NONE: 5 }; -const DEFAULT_LOGGER: SplitIO.Logger = { +export const DEFAULT_LOGGER: SplitIO.Logger = { debug(msg) { console.log('[DEBUG] ' + msg); }, info(msg) { console.log('[INFO] ' + msg); }, warn(msg) { console.log('[WARN] ' + msg); }, From 0cbcb2ba40dd95fc28636c9f63d5f4d7087c7af6 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Tue, 7 Oct 2025 12:57:44 -0300 Subject: [PATCH 10/10] Update changelog entry --- CHANGES.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.txt b/CHANGES.txt index b5364214..711d3ca3 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,4 +1,4 @@ -2.7.0 (October 6, 2025) +2.7.0 (October 7, 2025) - Added support for custom loggers: added `logger` configuration option and `LoggerAPI.setLogger` method to allow the SDK to use a custom logger. 2.6.0 (September 18, 2025)