Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
7566a17
feat: add custom logger support via logger callback option
EmilianoSanchez Jul 9, 2025
149c8d1
Merge branch 'development' into custom-logger
EmilianoSanchez Aug 26, 2025
77a56de
Implement custom logger interface based on console methods
EmilianoSanchez Sep 10, 2025
fd8b306
Merge branch 'main' into custom-logger
EmilianoSanchez Sep 24, 2025
e9767a4
Tests and polishing
EmilianoSanchez Sep 29, 2025
f3dc0ef
Add setLogger method
EmilianoSanchez Sep 30, 2025
5488458
Add analytics data name to storages for reusability in logs
EmilianoSanchez Sep 30, 2025
18ce047
Add logs for flushing data due to page hidden events
EmilianoSanchez Sep 30, 2025
57e1608
Merge pull request #431 from splitio/logs-for-page-hidden
EmilianoSanchez Sep 30, 2025
bf932da
Merge branch 'development' into custom-logger
EmilianoSanchez Sep 30, 2025
c9109c2
rc
EmilianoSanchez Oct 1, 2025
26d12ef
Tests
EmilianoSanchez Oct 1, 2025
f1a4155
Don't show redundant level when using custom logger
EmilianoSanchez Oct 3, 2025
a6f2bd3
rc
EmilianoSanchez Oct 3, 2025
a7470dd
stable version
EmilianoSanchez Oct 6, 2025
0cbcb2b
Update changelog entry
EmilianoSanchez Oct 7, 2025
fef1e80
Merge pull request #425 from splitio/custom-logger
EmilianoSanchez Oct 7, 2025
cebb211
chore: remove unused google analytics type dependency
EmilianoSanchez Oct 7, 2025
7f5cca6
Merge branch 'development' into remove-unused-devDependency
EmilianoSanchez Oct 7, 2025
dc05461
Merge pull request #435 from splitio/remove-unused-devDependency
EmilianoSanchez Oct 7, 2025
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
17 changes: 2 additions & 15 deletions package-lock.json

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

3 changes: 1 addition & 2 deletions 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 Expand Up @@ -57,7 +57,6 @@
}
},
"devDependencies": {
"@types/google.analytics": "0.0.40",
"@types/ioredis": "^4.28.0",
"@types/jest": "^27.0.0",
"@types/lodash": "^4.14.162",
Expand Down
3 changes: 2 additions & 1 deletion src/listeners/browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { ISettings } from '../types';
import SplitIO from '../../types/splitio';
import { ImpressionsPayload } from '../sync/submitters/types';
import { objectAssign } from '../utils/lang/objectAssign';
import { CLEANUP_REGISTERING, CLEANUP_DEREGISTERING } from '../logger/constants';
import { CLEANUP_REGISTERING, CLEANUP_DEREGISTERING, SUBMITTERS_PUSH_PAGE_HIDDEN } from '../logger/constants';
import { ISyncManager } from '../sync/types';
import { isConsentGranted } from '../consent';

Expand Down Expand Up @@ -104,6 +104,7 @@ export class BrowserSignalListener implements ISignalListener {
if (!this._sendBeacon(url, dataPayload, extraMetadata)) {
postService(JSON.stringify(dataPayload)).catch(() => { }); // no-op to handle possible promise rejection
}
this.settings.log.debug(SUBMITTERS_PUSH_PAGE_HIDDEN, [cache.name]);
}
}

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();
});
1 change: 1 addition & 0 deletions src/logger/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ export const IMPRESSIONS_TRACKER_SUCCESS = 121;
export const USER_CONSENT_UPDATED = 122;
export const USER_CONSENT_NOT_UPDATED = 123;
export const USER_CONSENT_INITIAL = 124;
export const SUBMITTERS_PUSH_PAGE_HIDDEN = 125;

export const ENGINE_VALUE_INVALID = 200;
export const ENGINE_VALUE_NO_ATTRIBUTES = 201;
Expand Down
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
1 change: 1 addition & 0 deletions src/logger/messages/info.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export const codesInfo: [number, string][] = codesWarn.concat([
[c.SYNC_SPLITS_FETCH_RETRY, c.LOG_PREFIX_SYNC_SPLITS + 'Retrying download of feature flags #%s. Reason: %s'],
[c.SUBMITTERS_PUSH_FULL_QUEUE, c.LOG_PREFIX_SYNC_SUBMITTERS + 'Flushing full %s queue and resetting timer.'],
[c.SUBMITTERS_PUSH, c.LOG_PREFIX_SYNC_SUBMITTERS + 'Pushing %s.'],
[c.SUBMITTERS_PUSH_PAGE_HIDDEN, c.LOG_PREFIX_SYNC_SUBMITTERS + 'Flushing %s because page became hidden.'],
[c.STREAMING_REFRESH_TOKEN, c.LOG_PREFIX_SYNC_STREAMING + 'Refreshing streaming token in %s seconds, and connecting streaming in %s seconds.'],
[c.STREAMING_RECONNECT, c.LOG_PREFIX_SYNC_STREAMING + 'Attempting to reconnect streaming in %s seconds.'],
[c.STREAMING_CONNECTING, c.LOG_PREFIX_SYNC_STREAMING + 'Connecting streaming.'],
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
Loading