Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGES.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
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)
- 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`.

Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@splitsoftware/splitio-commons",
"version": "2.6.0",
"version": "2.7.0",
"description": "Split JavaScript SDK common components",
"main": "cjs/index.js",
"module": "esm/index.js",
Expand Down
40 changes: 21 additions & 19 deletions src/logger/__tests__/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand All @@ -40,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;
Expand All @@ -51,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];
Expand All @@ -85,36 +92,31 @@ 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.
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', () => {
Expand Down
6 changes: 6 additions & 0 deletions src/logger/__tests__/sdkLogger.mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,22 @@ export const loggerMock = {
debug: jest.fn(),
info: jest.fn(),
setLogLevel: jest.fn(),
setLogger: jest.fn(),

mockClear() {
this.warn.mockClear();
this.error.mockClear();
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;
}
14 changes: 13 additions & 1 deletion src/logger/__tests__/sdkLogger.spec.ts
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -26,4 +26,16 @@ 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);

// unset custom logger
API.setLogger(undefined);
expect(getCustomLogger(logger)).toBeUndefined();

// invalid custom logger
// @ts-expect-error
API.setLogger({});
expect(getCustomLogger(logger)).toBeUndefined();
});
58 changes: 36 additions & 22 deletions src/logger/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -19,6 +20,13 @@ const LogLevelIndexes = {
NONE: 5
};

export 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);
}
Expand All @@ -40,14 +48,14 @@ export function _sprintf(format: string = '', args: any[] = []): string {
const defaultOptions = {
prefix: 'splitio',
logLevel: LogLevels.NONE,
showLevel: true,
};

export class Logger implements ILogger {

private options: Required<ILoggerOptions>;
private codes: Map<number, string>;
private logLevel: number;
private logger?: SplitIO.Logger;

constructor(options?: ILoggerOptions, codes?: Map<number, string>) {
this.options = objectAssign({}, defaultOptions, options);
Expand All @@ -60,48 +68,54 @@ export class Logger implements ILogger {
this.logLevel = LogLevelIndexes[logLevel];
}

setLogger(logger?: SplitIO.Logger) {
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);
return;
} else {
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() : ''}`;
} else {
if (args) msg = _sprintf(msg, args);
}

const formattedText = this._generateLogMessage(level, msg);

console.log(formattedText);
}
if (this.options.prefix) msg = this.options.prefix + ' => ' + msg;

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;
if (this.logger) {
try {
this.logger[method](msg);
return;
} catch (e) { /* empty */ }
}

return result += text;
DEFAULT_LOGGER[method](msg);
}

private _shouldLog(level: number) {
Expand Down
7 changes: 7 additions & 0 deletions src/logger/sdkLogger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand Down
7 changes: 4 additions & 3 deletions src/logger/types.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
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;
}

export interface ILogger extends SplitIO.ILogger {
setLogger(logger?: SplitIO.Logger): void;

debug(msg: any): void;
debug(msg: string | number, args?: any[]): void;

Expand Down
Loading