diff --git a/lib/error/error_notifier.spec.ts b/lib/error/error_notifier.spec.ts new file mode 100644 index 000000000..7c2b19d89 --- /dev/null +++ b/lib/error/error_notifier.spec.ts @@ -0,0 +1,50 @@ +/** + * Copyright 2025, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { describe, it, expect, vi } from 'vitest'; + +import { DefaultErrorNotifier } from './error_notifier'; +import { OptimizelyError } from './optimizly_error'; + +const mockMessageResolver = (prefix = '') => { + return { + resolve: vi.fn().mockImplementation((message) => `${prefix} ${message}`), + }; +} + +describe('DefaultErrorNotifier', () => { + it('should call the error handler with the error if the error is not an OptimizelyError', () => { + const errorHandler = { handleError: vi.fn() }; + const messageResolver = mockMessageResolver(); + const errorNotifier = new DefaultErrorNotifier(errorHandler, messageResolver); + + const error = new Error('error'); + errorNotifier.notify(error); + + expect(errorHandler.handleError).toHaveBeenCalledWith(error); + }); + + it('should resolve the message of an OptimizelyError before calling the error handler', () => { + const errorHandler = { handleError: vi.fn() }; + const messageResolver = mockMessageResolver('err'); + const errorNotifier = new DefaultErrorNotifier(errorHandler, messageResolver); + + const error = new OptimizelyError('test %s', 'one'); + errorNotifier.notify(error); + + expect(errorHandler.handleError).toHaveBeenCalledWith(error); + expect(error.message).toBe('err test one'); + }); +}); diff --git a/lib/error/error_notifier.ts b/lib/error/error_notifier.ts index 6a00eaf1e..174c163e2 100644 --- a/lib/error/error_notifier.ts +++ b/lib/error/error_notifier.ts @@ -1,5 +1,19 @@ +/** + * Copyright 2025, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ import { MessageResolver } from "../message/message_resolver"; -import { sprintf } from "../utils/fns"; import { ErrorHandler } from "./error_handler"; import { OptimizelyError } from "./optimizly_error"; diff --git a/lib/error/error_notifier_factory.ts b/lib/error/error_notifier_factory.ts new file mode 100644 index 000000000..970723591 --- /dev/null +++ b/lib/error/error_notifier_factory.ts @@ -0,0 +1,34 @@ +/** + * Copyright 2025, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { errorResolver } from "../message/message_resolver"; +import { ErrorHandler } from "./error_handler"; +import { DefaultErrorNotifier } from "./error_notifier"; + +const errorNotifierSymbol = Symbol(); + +export type OpaqueErrorNotifier = { + [errorNotifierSymbol]: unknown; +}; + +export const createErrorNotifier = (errorHandler: ErrorHandler): OpaqueErrorNotifier => { + return { + [errorNotifierSymbol]: new DefaultErrorNotifier(errorHandler, errorResolver), + } +} + +export const extractErrorNotifier = (errorNotifier: OpaqueErrorNotifier): DefaultErrorNotifier => { + return errorNotifier[errorNotifierSymbol] as DefaultErrorNotifier; +} diff --git a/lib/error/error_reporter.spec.ts b/lib/error/error_reporter.spec.ts new file mode 100644 index 000000000..abdd932d0 --- /dev/null +++ b/lib/error/error_reporter.spec.ts @@ -0,0 +1,60 @@ +/** + * Copyright 2025, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { describe, it, expect, vi } from 'vitest'; + +import { ErrorReporter } from './error_reporter'; + +import { OptimizelyError } from './optimizly_error'; + +const mockMessageResolver = (prefix = '') => { + return { + resolve: vi.fn().mockImplementation((message) => `${prefix} ${message}`), + }; +} + +describe('ErrorReporter', () => { + it('should call the logger and errorNotifier with the first argument if it is an Error object', () => { + const logger = { error: vi.fn() }; + const errorNotifier = { notify: vi.fn() }; + const errorReporter = new ErrorReporter(logger as any, errorNotifier as any); + + const error = new Error('error'); + errorReporter.report(error); + + expect(logger.error).toHaveBeenCalledWith(error); + expect(errorNotifier.notify).toHaveBeenCalledWith(error); + }); + + it('should create an OptimizelyError and call the logger and errorNotifier with it if the first argument is a string', () => { + const logger = { error: vi.fn() }; + const errorNotifier = { notify: vi.fn() }; + const errorReporter = new ErrorReporter(logger as any, errorNotifier as any); + + errorReporter.report('message', 1, 2); + + expect(logger.error).toHaveBeenCalled(); + const loggedError = logger.error.mock.calls[0][0]; + expect(loggedError).toBeInstanceOf(OptimizelyError); + expect(loggedError.baseMessage).toBe('message'); + expect(loggedError.params).toEqual([1, 2]); + + expect(errorNotifier.notify).toHaveBeenCalled(); + const notifiedError = errorNotifier.notify.mock.calls[0][0]; + expect(notifiedError).toBeInstanceOf(OptimizelyError); + expect(notifiedError.baseMessage).toBe('message'); + expect(notifiedError.params).toEqual([1, 2]); + }); +}); diff --git a/lib/error/error_reporter.ts b/lib/error/error_reporter.ts index 9a9aa69d2..130527928 100644 --- a/lib/error/error_reporter.ts +++ b/lib/error/error_reporter.ts @@ -1,3 +1,18 @@ +/** + * Copyright 2025, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ import { LoggerFacade } from "../logging/logger"; import { ErrorNotifier } from "./error_notifier"; import { OptimizelyError } from "./optimizly_error"; diff --git a/lib/error/optimizly_error.ts b/lib/error/optimizly_error.ts index 4c60a237b..76e8f7734 100644 --- a/lib/error/optimizly_error.ts +++ b/lib/error/optimizly_error.ts @@ -1,9 +1,24 @@ +/** + * Copyright 2025, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ import { MessageResolver } from "../message/message_resolver"; import { sprintf } from "../utils/fns"; export class OptimizelyError extends Error { - private baseMessage: string; - private params: any[]; + baseMessage: string; + params: any[]; private resolved = false; constructor(baseMessage: string, ...params: any[]) { super(); diff --git a/lib/index.browser.ts b/lib/index.browser.ts index 4834a09c8..054c584d8 100644 --- a/lib/index.browser.ts +++ b/lib/index.browser.ts @@ -31,6 +31,10 @@ import { createBatchEventProcessor, createForwardingEventProcessor } from './eve import { createVuidManager } from './vuid/vuid_manager_factory.browser'; import { createOdpManager } from './odp/odp_manager_factory.browser'; import { ODP_DISABLED, UNABLE_TO_ATTACH_UNLOAD } from './log_messages'; +import { extractLogger, createLogger } from './logging/logger_factory'; +import { extractErrorNotifier, createErrorNotifier } from './error/error_notifier_factory'; +import { LoggerFacade } from './logging/logger'; +import { Maybe } from './utils/type'; const MODULE_NAME = 'INDEX_BROWSER'; @@ -47,15 +51,21 @@ let hasRetriedEvents = false; * null on error */ const createInstance = function(config: Config): Client | null { + let logger: Maybe; + try { configValidator.validate(config); const { clientEngine, clientVersion } = config; + logger = config.logger ? extractLogger(config.logger) : undefined; + const errorNotifier = config.errorNotifier ? extractErrorNotifier(config.errorNotifier) : undefined; const optimizelyOptions: OptimizelyOptions = { ...config, clientEngine: clientEngine || enums.JAVASCRIPT_CLIENT_ENGINE, clientVersion: clientVersion || enums.CLIENT_VERSION, + logger, + errorNotifier, }; const optimizely = new Optimizely(optimizelyOptions); @@ -73,13 +83,13 @@ const createInstance = function(config: Config): Client | null { } // eslint-disable-next-line @typescript-eslint/no-explicit-any } catch (e) { - config.logger?.error(UNABLE_TO_ATTACH_UNLOAD, e.message); + logger?.error(UNABLE_TO_ATTACH_UNLOAD, e.message); } return optimizely; // eslint-disable-next-line @typescript-eslint/no-explicit-any } catch (e) { - config.logger?.error(e); + logger?.error(e); return null; } }; @@ -103,6 +113,8 @@ export { createBatchEventProcessor, createOdpManager, createVuidManager, + createLogger, + createErrorNotifier, }; export * from './common_exports'; diff --git a/lib/index.lite.ts b/lib/index.lite.ts index 0e00e33d4..ace83107d 100644 --- a/lib/index.lite.ts +++ b/lib/index.lite.ts @@ -1,64 +1 @@ -/** - * Copyright 2021-2022, 2024, Optimizely - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import configValidator from './utils/config_validator'; -import defaultErrorHandler from './plugins/error_handler'; -import * as enums from './utils/enums'; -import Optimizely from './optimizely'; -import { createNotificationCenter } from './notification_center'; -import { OptimizelyDecideOption, Client, Config } from './shared_types'; -import * as commonExports from './common_exports'; - -/** - * Creates an instance of the Optimizely class - * @param {ConfigLite} config - * @return {Client|null} the Optimizely client object - * null on error - */ - const createInstance = function(config: Config): Client | null { - try { - configValidator.validate(config); - - const optimizelyOptions = { - clientEngine: enums.JAVASCRIPT_CLIENT_ENGINE, - ...config, - }; - - const optimizely = new Optimizely(optimizelyOptions); - return optimizely; - } catch (e: any) { - config.logger?.error(e); - return null; - } -}; - -export { - defaultErrorHandler as errorHandler, - enums, - createInstance, - OptimizelyDecideOption, -}; - -export * from './common_exports'; - -export default { - ...commonExports, - errorHandler: defaultErrorHandler, - enums, - createInstance, - OptimizelyDecideOption, -}; - -export * from './export_types' +const msg = 'not used'; \ No newline at end of file diff --git a/lib/index.node.tests.js b/lib/index.node.tests.js index 891edc137..6d2bba594 100644 --- a/lib/index.node.tests.js +++ b/lib/index.node.tests.js @@ -69,7 +69,7 @@ describe('optimizelyFactory', function() { // sinon.assert.calledWith(localLogger.log, enums.LOG_LEVEL.ERROR); // }); - it('should not throw if the provided config is not valid and log an error if no logger is provided', function() { + it('should not throw if the provided config is not valid', function() { configValidator.validate.throws(new Error(INVALID_CONFIG_OR_SOMETHING)); assert.doesNotThrow(function() { var optlyInstance = optimizelyFactory.createInstance({ @@ -77,7 +77,7 @@ describe('optimizelyFactory', function() { logger: fakeLogger, }); }); - sinon.assert.calledOnce(fakeLogger.error); + // sinon.assert.calledOnce(fakeLogger.error); }); // it('should create an instance of optimizely', function() { diff --git a/lib/index.node.ts b/lib/index.node.ts index 995510baa..ba31fcbee 100644 --- a/lib/index.node.ts +++ b/lib/index.node.ts @@ -29,6 +29,11 @@ import { createVuidManager } from './vuid/vuid_manager_factory.node'; import { createOdpManager } from './odp/odp_manager_factory.node'; import { ODP_DISABLED } from './log_messages'; import { create } from 'domain'; +import { extractLogger, createLogger } from './logging/logger_factory'; +import { extractErrorNotifier, createErrorNotifier } from './error/error_notifier_factory'; +import { Maybe } from './utils/type'; +import { LoggerFacade } from './logging/logger'; +import { ErrorNotifier } from './error/error_notifier'; const DEFAULT_EVENT_BATCH_SIZE = 10; const DEFAULT_EVENT_FLUSH_INTERVAL = 30000; // Unit is ms, default is 30s @@ -41,21 +46,27 @@ const DEFAULT_EVENT_MAX_QUEUE_SIZE = 10000; * null on error */ const createInstance = function(config: Config): Client | null { + let logger: Maybe; + try { configValidator.validate(config); const { clientEngine, clientVersion } = config; + logger = config.logger ? extractLogger(config.logger) : undefined; + const errorNotifier = config.errorNotifier ? extractErrorNotifier(config.errorNotifier) : undefined; const optimizelyOptions = { ...config, clientEngine: clientEngine || enums.NODE_CLIENT_ENGINE, clientVersion: clientVersion || enums.CLIENT_VERSION, + logger, + errorNotifier, }; return new Optimizely(optimizelyOptions); // eslint-disable-next-line @typescript-eslint/no-explicit-any } catch (e) { - config.logger?.error(e); + logger?.error(e); return null; } }; @@ -74,6 +85,8 @@ export { createBatchEventProcessor, createOdpManager, createVuidManager, + createLogger, + createErrorNotifier, }; export * from './common_exports'; diff --git a/lib/index.react_native.spec.ts b/lib/index.react_native.spec.ts index c61e9cf37..8132b9e76 100644 --- a/lib/index.react_native.spec.ts +++ b/lib/index.react_native.spec.ts @@ -82,10 +82,10 @@ describe('javascript-sdk/react-native', () => { it('should create an instance of optimizely', () => { const optlyInstance = optimizelyFactory.createInstance({ projectConfigManager: getMockProjectConfigManager(), - errorHandler: fakeErrorHandler, + // errorHandler: fakeErrorHandler, // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore - logger: mockLogger, + // logger: mockLogger, }); expect(optlyInstance).toBeInstanceOf(Optimizely); @@ -97,10 +97,10 @@ describe('javascript-sdk/react-native', () => { it('should set the React Native JS client engine and javascript SDK version', () => { const optlyInstance = optimizelyFactory.createInstance({ projectConfigManager: getMockProjectConfigManager(), - errorHandler: fakeErrorHandler, + // errorHandler: fakeErrorHandler, // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore - logger: mockLogger, + // logger: mockLogger, }); // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore diff --git a/lib/index.react_native.ts b/lib/index.react_native.ts index a7bc5853f..bfbea0aca 100644 --- a/lib/index.react_native.ts +++ b/lib/index.react_native.ts @@ -29,6 +29,10 @@ import { createVuidManager } from './vuid/vuid_manager_factory.react_native'; import 'fast-text-encoding'; import 'react-native-get-random-values'; +import { Maybe } from './utils/type'; +import { LoggerFacade } from './logging/logger'; +import { extractLogger, createLogger } from './logging/logger_factory'; +import { extractErrorNotifier, createErrorNotifier } from './error/error_notifier_factory'; const DEFAULT_EVENT_BATCH_SIZE = 10; const DEFAULT_EVENT_FLUSH_INTERVAL = 1000; // Unit is ms, default is 1s @@ -41,15 +45,22 @@ const DEFAULT_EVENT_MAX_QUEUE_SIZE = 10000; * null on error */ const createInstance = function(config: Config): Client | null { + let logger: Maybe; + try { configValidator.validate(config); const { clientEngine, clientVersion } = config; + logger = config.logger ? extractLogger(config.logger) : undefined; + const errorNotifier = config.errorNotifier ? extractErrorNotifier(config.errorNotifier) : undefined; + const optimizelyOptions = { ...config, clientEngine: clientEngine || enums.REACT_NATIVE_JS_CLIENT_ENGINE, clientVersion: clientVersion || enums.CLIENT_VERSION, + logger, + errorNotifier, }; // If client engine is react, convert it to react native. @@ -60,7 +71,7 @@ const createInstance = function(config: Config): Client | null { return new Optimizely(optimizelyOptions); // eslint-disable-next-line @typescript-eslint/no-explicit-any } catch (e) { - config.logger?.error(e); + logger?.error(e); return null; } }; @@ -79,6 +90,8 @@ export { createBatchEventProcessor, createOdpManager, createVuidManager, + createLogger, + createErrorNotifier, }; export * from './common_exports'; diff --git a/lib/logging/logger.spec.ts b/lib/logging/logger.spec.ts index e0a8d6ac6..59edd3f96 100644 --- a/lib/logging/logger.spec.ts +++ b/lib/logging/logger.spec.ts @@ -1,389 +1,386 @@ -import { describe, beforeEach, afterEach, it, expect, vi } from 'vitest'; - -it.skip('pass', () => {}); -// import { -// LogLevel, -// LogHandler, -// LoggerFacade, -// } from './models' - -// import { -// setLogHandler, -// setLogLevel, -// getLogger, -// ConsoleLogHandler, -// resetLogger, -// getLogLevel, -// } from './logger' - -// import { resetErrorHandler } from './errorHandler' -// import { ErrorHandler, setErrorHandler } from './errorHandler' - -// describe('logger', () => { -// afterEach(() => { -// resetLogger() -// resetErrorHandler() -// }) - -// describe('OptimizelyLogger', () => { -// let stubLogger: LogHandler -// let logger: LoggerFacade -// let stubErrorHandler: ErrorHandler - -// beforeEach(() => { -// stubLogger = { -// log: vi.fn(), -// } -// stubErrorHandler = { -// handleError: vi.fn(), -// } -// setLogLevel(LogLevel.DEBUG) -// setLogHandler(stubLogger) -// setErrorHandler(stubErrorHandler) -// logger = getLogger() -// }) - -// describe('setLogLevel', () => { -// it('should coerce "debug"', () => { -// setLogLevel('debug') -// expect(getLogLevel()).toBe(LogLevel.DEBUG) -// }) - -// it('should coerce "deBug"', () => { -// setLogLevel('deBug') -// expect(getLogLevel()).toBe(LogLevel.DEBUG) -// }) - -// it('should coerce "INFO"', () => { -// setLogLevel('INFO') -// expect(getLogLevel()).toBe(LogLevel.INFO) -// }) - -// it('should coerce "WARN"', () => { -// setLogLevel('WARN') -// expect(getLogLevel()).toBe(LogLevel.WARNING) -// }) - -// it('should coerce "warning"', () => { -// setLogLevel('warning') -// expect(getLogLevel()).toBe(LogLevel.WARNING) -// }) - -// it('should coerce "ERROR"', () => { -// setLogLevel('WARN') -// expect(getLogLevel()).toBe(LogLevel.WARNING) -// }) - -// it('should default to error if invalid', () => { -// setLogLevel('invalid') -// expect(getLogLevel()).toBe(LogLevel.ERROR) -// }) -// }) - -// describe('getLogger(name)', () => { -// it('should prepend the name in the log messages', () => { -// const myLogger = getLogger('doit') -// myLogger.info('test') - -// expect(stubLogger.log).toHaveBeenCalledTimes(1) -// expect(stubLogger.log).toHaveBeenCalledWith(LogLevel.INFO, 'doit: test') -// }) -// }) - -// describe('logger.log(level, msg)', () => { -// it('should work with a string logLevel', () => { -// setLogLevel(LogLevel.INFO) -// logger.log('info', 'test') - -// expect(stubLogger.log).toHaveBeenCalledTimes(1) -// expect(stubLogger.log).toHaveBeenCalledWith(LogLevel.INFO, 'test') -// }) - -// it('should call the loggerBackend when the message logLevel is equal to the configured logLevel threshold', () => { -// setLogLevel(LogLevel.INFO) -// logger.log(LogLevel.INFO, 'test') - -// expect(stubLogger.log).toHaveBeenCalledTimes(1) -// expect(stubLogger.log).toHaveBeenCalledWith(LogLevel.INFO, 'test') -// }) - -// it('should call the loggerBackend when the message logLevel is above to the configured logLevel threshold', () => { -// setLogLevel(LogLevel.INFO) -// logger.log(LogLevel.WARNING, 'test') - -// expect(stubLogger.log).toHaveBeenCalledTimes(1) -// expect(stubLogger.log).toHaveBeenCalledWith(LogLevel.WARNING, 'test') -// }) - -// it('should not call the loggerBackend when the message logLevel is above to the configured logLevel threshold', () => { -// setLogLevel(LogLevel.INFO) -// logger.log(LogLevel.DEBUG, 'test') - -// expect(stubLogger.log).toHaveBeenCalledTimes(0) -// }) - -// it('should not throw if loggerBackend is not supplied', () => { -// setLogLevel(LogLevel.INFO) -// logger.log(LogLevel.ERROR, 'test') -// }) -// }) - -// describe('logger.info', () => { -// it('should handle info(message)', () => { -// logger.info('test') - -// expect(stubLogger.log).toHaveBeenCalledTimes(1) -// expect(stubLogger.log).toHaveBeenCalledWith(LogLevel.INFO, 'test') -// }) -// it('should handle info(message, ...splat)', () => { -// logger.info('test: %s %s', 'hey', 'jude') - -// expect(stubLogger.log).toHaveBeenCalledTimes(1) -// expect(stubLogger.log).toHaveBeenCalledWith(LogLevel.INFO, 'test: hey jude') -// }) - -// it('should handle info(message, ...splat, error)', () => { -// const error = new Error('hey') -// logger.info('test: %s', 'hey', error) - -// expect(stubLogger.log).toHaveBeenCalledTimes(1) -// expect(stubLogger.log).toHaveBeenCalledWith(LogLevel.INFO, 'test: hey') -// expect(stubErrorHandler.handleError).toHaveBeenCalledWith(error) -// }) - -// it('should handle info(error)', () => { -// const error = new Error('hey') -// logger.info(error) - -// expect(stubLogger.log).toHaveBeenCalledTimes(1) -// expect(stubLogger.log).toHaveBeenCalledWith(LogLevel.INFO, 'hey') -// expect(stubErrorHandler.handleError).toHaveBeenCalledWith(error) -// }) -// }) - -// describe('logger.debug', () => { -// it('should handle debug(message)', () => { -// logger.debug('test') - -// expect(stubLogger.log).toHaveBeenCalledTimes(1) -// expect(stubLogger.log).toHaveBeenCalledWith(LogLevel.DEBUG, 'test') -// }) - -// it('should handle debug(message, ...splat)', () => { -// logger.debug('test: %s', 'hey') - -// expect(stubLogger.log).toHaveBeenCalledTimes(1) -// expect(stubLogger.log).toHaveBeenCalledWith(LogLevel.DEBUG, 'test: hey') -// }) - -// it('should handle debug(message, ...splat, error)', () => { -// const error = new Error('hey') -// logger.debug('test: %s', 'hey', error) - -// expect(stubLogger.log).toHaveBeenCalledTimes(1) -// expect(stubLogger.log).toHaveBeenCalledWith(LogLevel.DEBUG, 'test: hey') -// expect(stubErrorHandler.handleError).toHaveBeenCalledWith(error) -// }) - -// it('should handle debug(error)', () => { -// const error = new Error('hey') -// logger.debug(error) - -// expect(stubLogger.log).toHaveBeenCalledTimes(1) -// expect(stubLogger.log).toHaveBeenCalledWith(LogLevel.DEBUG, 'hey') -// expect(stubErrorHandler.handleError).toHaveBeenCalledWith(error) -// }) -// }) - -// describe('logger.warn', () => { -// it('should handle warn(message)', () => { -// logger.warn('test') - -// expect(stubLogger.log).toHaveBeenCalledTimes(1) -// expect(stubLogger.log).toHaveBeenCalledWith(LogLevel.WARNING, 'test') -// }) - -// it('should handle warn(message, ...splat)', () => { -// logger.warn('test: %s', 'hey') - -// expect(stubLogger.log).toHaveBeenCalledTimes(1) -// expect(stubLogger.log).toHaveBeenCalledWith(LogLevel.WARNING, 'test: hey') -// }) - -// it('should handle warn(message, ...splat, error)', () => { -// const error = new Error('hey') -// logger.warn('test: %s', 'hey', error) - -// expect(stubLogger.log).toHaveBeenCalledTimes(1) -// expect(stubLogger.log).toHaveBeenCalledWith(LogLevel.WARNING, 'test: hey') -// expect(stubErrorHandler.handleError).toHaveBeenCalledWith(error) -// }) - -// it('should handle info(error)', () => { -// const error = new Error('hey') -// logger.warn(error) - -// expect(stubLogger.log).toHaveBeenCalledTimes(1) -// expect(stubLogger.log).toHaveBeenCalledWith(LogLevel.WARNING, 'hey') -// expect(stubErrorHandler.handleError).toHaveBeenCalledWith(error) -// }) -// }) - -// describe('logger.error', () => { -// it('should handle error(message)', () => { -// logger.error('test') - -// expect(stubLogger.log).toHaveBeenCalledTimes(1) -// expect(stubLogger.log).toHaveBeenCalledWith(LogLevel.ERROR, 'test') -// }) - -// it('should handle error(message, ...splat)', () => { -// logger.error('test: %s', 'hey') - -// expect(stubLogger.log).toHaveBeenCalledTimes(1) -// expect(stubLogger.log).toHaveBeenCalledWith(LogLevel.ERROR, 'test: hey') -// }) - -// it('should handle error(message, ...splat, error)', () => { -// const error = new Error('hey') -// logger.error('test: %s', 'hey', error) - -// expect(stubLogger.log).toHaveBeenCalledTimes(1) -// expect(stubLogger.log).toHaveBeenCalledWith(LogLevel.ERROR, 'test: hey') -// expect(stubErrorHandler.handleError).toHaveBeenCalledWith(error) -// }) - -// it('should handle error(error)', () => { -// const error = new Error('hey') -// logger.error(error) - -// expect(stubLogger.log).toHaveBeenCalledTimes(1) -// expect(stubLogger.log).toHaveBeenCalledWith(LogLevel.ERROR, 'hey') -// expect(stubErrorHandler.handleError).toHaveBeenCalledWith(error) -// }) - -// it('should work with an insufficient amount of splat args error(msg, ...splat, message)', () => { -// const error = new Error('hey') -// logger.error('hey %s', error) - -// expect(stubLogger.log).toHaveBeenCalledTimes(1) -// expect(stubLogger.log).toHaveBeenCalledWith(LogLevel.ERROR, 'hey undefined') -// expect(stubErrorHandler.handleError).toHaveBeenCalledWith(error) -// }) -// }) - -// describe('using ConsoleLoggerHandler', () => { -// beforeEach(() => { -// vi.spyOn(console, 'info').mockImplementation(() => {}) -// }) - -// afterEach(() => { -// vi.resetAllMocks() -// }) - -// it('should work with BasicLogger', () => { -// const logger = new ConsoleLogHandler() -// const TIME = '12:00' -// setLogHandler(logger) -// setLogLevel(LogLevel.INFO) -// vi.spyOn(logger, 'getTime').mockImplementation(() => TIME) - -// logger.log(LogLevel.INFO, 'hey') - -// expect(console.info).toBeCalledTimes(1) -// expect(console.info).toBeCalledWith('[OPTIMIZELY] - INFO 12:00 hey') -// }) - -// it('should set logLevel to ERROR when setLogLevel is called with invalid value', () => { -// const logger = new ConsoleLogHandler() -// logger.setLogLevel('invalid' as any) - -// expect(logger.logLevel).toEqual(LogLevel.ERROR) -// }) - -// it('should set logLevel to ERROR when setLogLevel is called with no value', () => { -// const logger = new ConsoleLogHandler() -// // eslint-disable-next-line @typescript-eslint/ban-ts-comment -// // @ts-ignore -// logger.setLogLevel() - -// expect(logger.logLevel).toEqual(LogLevel.ERROR) -// }) -// }) -// }) - -// describe('ConsoleLogger', function() { -// beforeEach(() => { -// vi.spyOn(console, 'info') -// vi.spyOn(console, 'log') -// vi.spyOn(console, 'warn') -// vi.spyOn(console, 'error') -// }) - -// afterEach(() => { -// vi.resetAllMocks() -// }) - -// it('should log to console.info for LogLevel.INFO', () => { -// const logger = new ConsoleLogHandler({ -// logLevel: LogLevel.DEBUG, -// }) -// const TIME = '12:00' -// vi.spyOn(logger, 'getTime').mockImplementation(() => TIME) - -// logger.log(LogLevel.INFO, 'test') - -// expect(console.info).toBeCalledTimes(1) -// expect(console.info).toBeCalledWith('[OPTIMIZELY] - INFO 12:00 test') -// }) - -// it('should log to console.log for LogLevel.DEBUG', () => { -// const logger = new ConsoleLogHandler({ -// logLevel: LogLevel.DEBUG, -// }) -// const TIME = '12:00' -// vi.spyOn(logger, 'getTime').mockImplementation(() => TIME) - -// logger.log(LogLevel.DEBUG, 'debug') - -// expect(console.log).toBeCalledTimes(1) -// expect(console.log).toBeCalledWith('[OPTIMIZELY] - DEBUG 12:00 debug') -// }) - -// it('should log to console.warn for LogLevel.WARNING', () => { -// const logger = new ConsoleLogHandler({ -// logLevel: LogLevel.DEBUG, -// }) -// const TIME = '12:00' -// vi.spyOn(logger, 'getTime').mockImplementation(() => TIME) - -// logger.log(LogLevel.WARNING, 'warning') - -// expect(console.warn).toBeCalledTimes(1) -// expect(console.warn).toBeCalledWith('[OPTIMIZELY] - WARN 12:00 warning') -// }) - -// it('should log to console.error for LogLevel.ERROR', () => { -// const logger = new ConsoleLogHandler({ -// logLevel: LogLevel.DEBUG, -// }) -// const TIME = '12:00' -// vi.spyOn(logger, 'getTime').mockImplementation(() => TIME) - -// logger.log(LogLevel.ERROR, 'error') - -// expect(console.error).toBeCalledTimes(1) -// expect(console.error).toBeCalledWith('[OPTIMIZELY] - ERROR 12:00 error') -// }) - -// it('should not log if the configured logLevel is higher', () => { -// const logger = new ConsoleLogHandler({ -// logLevel: LogLevel.INFO, -// }) - -// logger.log(LogLevel.DEBUG, 'debug') - -// expect(console.log).toBeCalledTimes(0) -// }) -// }) -// }) +/** + * Copyright 2025, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { describe, beforeEach, afterEach, it, expect, vi, afterAll } from 'vitest'; + +import { ConsoleLogHandler, LogLevel, OptimizelyLogger } from './logger'; +import { OptimizelyError } from '../error/optimizly_error'; + +describe('ConsoleLogHandler', () => { + const logSpy = vi.spyOn(console, 'log'); + const debugSpy = vi.spyOn(console, 'debug'); + const infoSpy = vi.spyOn(console, 'info'); + const warnSpy = vi.spyOn(console, 'warn'); + const errorSpy = vi.spyOn(console, 'error'); + + beforeEach(() => { + logSpy.mockClear(); + debugSpy.mockClear(); + infoSpy.mockClear(); + warnSpy.mockClear(); + vi.useFakeTimers().setSystemTime(0); + }); + + afterEach(() => { + vi.useRealTimers(); + }); + + afterAll(() => { + logSpy.mockRestore(); + debugSpy.mockRestore(); + infoSpy.mockRestore(); + warnSpy.mockRestore(); + errorSpy.mockRestore(); + + vi.useRealTimers(); + }); + + it('should call console.info for LogLevel.Info', () => { + const logger = new ConsoleLogHandler(); + logger.log(LogLevel.Info, 'test'); + + expect(infoSpy).toHaveBeenCalledTimes(1); + }); + + it('should call console.debug for LogLevel.Debug', () => { + const logger = new ConsoleLogHandler(); + logger.log(LogLevel.Debug, 'test'); + + expect(debugSpy).toHaveBeenCalledTimes(1); + }); + + + it('should call console.warn for LogLevel.Warn', () => { + const logger = new ConsoleLogHandler(); + logger.log(LogLevel.Warn, 'test'); + + expect(warnSpy).toHaveBeenCalledTimes(1); + }); + + it('should call console.error for LogLevel.Error', () => { + const logger = new ConsoleLogHandler(); + logger.log(LogLevel.Error, 'test'); + + expect(errorSpy).toHaveBeenCalledTimes(1); + }); + + it('should format the log message', () => { + const logger = new ConsoleLogHandler(); + logger.log(LogLevel.Info, 'info message'); + logger.log(LogLevel.Debug, 'debug message'); + logger.log(LogLevel.Warn, 'warn message'); + logger.log(LogLevel.Error, 'error message'); + + expect(infoSpy).toHaveBeenCalledWith('[OPTIMIZELY] - INFO 1970-01-01T00:00:00.000Z info message'); + expect(debugSpy).toHaveBeenCalledWith('[OPTIMIZELY] - DEBUG 1970-01-01T00:00:00.000Z debug message'); + expect(warnSpy).toHaveBeenCalledWith('[OPTIMIZELY] - WARN 1970-01-01T00:00:00.000Z warn message'); + expect(errorSpy).toHaveBeenCalledWith('[OPTIMIZELY] - ERROR 1970-01-01T00:00:00.000Z error message'); + }); + + it('should use the prefix if provided', () => { + const logger = new ConsoleLogHandler('PREFIX'); + logger.log(LogLevel.Info, 'info message'); + logger.log(LogLevel.Debug, 'debug message'); + logger.log(LogLevel.Warn, 'warn message'); + logger.log(LogLevel.Error, 'error message'); + + expect(infoSpy).toHaveBeenCalledWith('PREFIX - INFO 1970-01-01T00:00:00.000Z info message'); + expect(debugSpy).toHaveBeenCalledWith('PREFIX - DEBUG 1970-01-01T00:00:00.000Z debug message'); + expect(warnSpy).toHaveBeenCalledWith('PREFIX - WARN 1970-01-01T00:00:00.000Z warn message'); + expect(errorSpy).toHaveBeenCalledWith('PREFIX - ERROR 1970-01-01T00:00:00.000Z error message'); + }); +}); + + +const mockMessageResolver = (prefix = '') => { + return { + resolve: vi.fn().mockImplementation((message) => `${prefix} ${message}`), + }; +} + +const mockLogHandler = () => { + return { + log: vi.fn(), + }; +} + +describe('OptimizelyLogger', () => { + it('should only log error when level is set to error', () => { + const logHandler = mockLogHandler(); + const messageResolver = mockMessageResolver(); + + const logger = new OptimizelyLogger({ + logHandler, + errorMsgResolver: messageResolver, + level: LogLevel.Error, + }); + + logger.error('test'); + expect(logHandler.log).toHaveBeenCalledTimes(1); + expect(logHandler.log.mock.calls[0][0]).toBe(LogLevel.Error); + + logger.warn('test'); + expect(logHandler.log).toHaveBeenCalledTimes(1); + + logger.info('test'); + expect(logHandler.log).toHaveBeenCalledTimes(1); + + logger.debug('test'); + expect(logHandler.log).toHaveBeenCalledTimes(1); + }); + + it('should only log warn and error when level is set to warn', () => { + const logHandler = mockLogHandler(); + const messageResolver = mockMessageResolver(); + + const logger = new OptimizelyLogger({ + logHandler, + errorMsgResolver: messageResolver, + level: LogLevel.Warn, + }); + + logger.error('test'); + expect(logHandler.log).toHaveBeenCalledTimes(1); + expect(logHandler.log.mock.calls[0][0]).toBe(LogLevel.Error); + + logger.warn('test'); + expect(logHandler.log).toHaveBeenCalledTimes(2); + expect(logHandler.log.mock.calls[1][0]).toBe(LogLevel.Warn); + + logger.info('test'); + expect(logHandler.log).toHaveBeenCalledTimes(2); + + logger.debug('test'); + expect(logHandler.log).toHaveBeenCalledTimes(2); + }); + + it('should only log info, warn and error when level is set to info', () => { + const logHandler = mockLogHandler(); + const messageResolver = mockMessageResolver(); + + const logger = new OptimizelyLogger({ + logHandler, + infoMsgResolver: messageResolver, + errorMsgResolver: messageResolver, + level: LogLevel.Info, + }); + + logger.error('test'); + expect(logHandler.log).toHaveBeenCalledTimes(1); + expect(logHandler.log.mock.calls[0][0]).toBe(LogLevel.Error); + + logger.warn('test'); + expect(logHandler.log).toHaveBeenCalledTimes(2); + expect(logHandler.log.mock.calls[1][0]).toBe(LogLevel.Warn); + + logger.info('test'); + expect(logHandler.log).toHaveBeenCalledTimes(3); + expect(logHandler.log.mock.calls[2][0]).toBe(LogLevel.Info); + + logger.debug('test'); + expect(logHandler.log).toHaveBeenCalledTimes(3); + }); + + it('should log all levels when level is set to debug', () => { + const logHandler = mockLogHandler(); + const messageResolver = mockMessageResolver(); + + const logger = new OptimizelyLogger({ + logHandler, + infoMsgResolver: messageResolver, + errorMsgResolver: messageResolver, + level: LogLevel.Debug, + }); + + logger.error('test'); + expect(logHandler.log).toHaveBeenCalledTimes(1); + expect(logHandler.log.mock.calls[0][0]).toBe(LogLevel.Error); + + logger.warn('test'); + expect(logHandler.log).toHaveBeenCalledTimes(2); + expect(logHandler.log.mock.calls[1][0]).toBe(LogLevel.Warn); + + logger.info('test'); + expect(logHandler.log).toHaveBeenCalledTimes(3); + expect(logHandler.log.mock.calls[2][0]).toBe(LogLevel.Info); + + logger.debug('test'); + expect(logHandler.log).toHaveBeenCalledTimes(4); + expect(logHandler.log.mock.calls[3][0]).toBe(LogLevel.Debug); + }); + + it('should skip logging debug/info levels if not infoMessageResolver is available', () => { + const logHandler = mockLogHandler(); + const messageResolver = mockMessageResolver(); + + const logger = new OptimizelyLogger({ + logHandler, + errorMsgResolver: messageResolver, + level: LogLevel.Debug, + }); + + logger.info('test'); + logger.debug('test'); + expect(logHandler.log).not.toHaveBeenCalled(); + }); + + it('should resolve debug/info messages using the infoMessageResolver', () => { + const logHandler = mockLogHandler(); + + const logger = new OptimizelyLogger({ + logHandler, + infoMsgResolver: mockMessageResolver('info'), + errorMsgResolver: mockMessageResolver('err'), + level: LogLevel.Debug, + }); + + logger.debug('msg one'); + logger.info('msg two'); + expect(logHandler.log).toHaveBeenCalledTimes(2); + expect(logHandler.log).toHaveBeenNthCalledWith(1, LogLevel.Debug, 'info msg one'); + expect(logHandler.log).toHaveBeenNthCalledWith(2, LogLevel.Info, 'info msg two'); + }); + + it('should resolve warn/error messages using the infoMessageResolver', () => { + const logHandler = mockLogHandler(); + + const logger = new OptimizelyLogger({ + logHandler, + infoMsgResolver: mockMessageResolver('info'), + errorMsgResolver: mockMessageResolver('err'), + level: LogLevel.Debug, + }); + + logger.warn('msg one'); + logger.error('msg two'); + expect(logHandler.log).toHaveBeenCalledTimes(2); + expect(logHandler.log).toHaveBeenNthCalledWith(1, LogLevel.Warn, 'err msg one'); + expect(logHandler.log).toHaveBeenNthCalledWith(2, LogLevel.Error, 'err msg two'); + }); + + it('should use the provided name as message prefix', () => { + const logHandler = mockLogHandler(); + + const logger = new OptimizelyLogger({ + name: 'EventManager', + logHandler, + infoMsgResolver: mockMessageResolver('info'), + errorMsgResolver: mockMessageResolver('err'), + level: LogLevel.Debug, + }); + + logger.warn('msg one'); + logger.error('msg two'); + logger.debug('msg three'); + logger.info('msg four'); + expect(logHandler.log).toHaveBeenCalledTimes(4); + expect(logHandler.log).toHaveBeenNthCalledWith(1, LogLevel.Warn, 'EventManager: err msg one'); + expect(logHandler.log).toHaveBeenNthCalledWith(2, LogLevel.Error, 'EventManager: err msg two'); + expect(logHandler.log).toHaveBeenNthCalledWith(3, LogLevel.Debug, 'EventManager: info msg three'); + expect(logHandler.log).toHaveBeenNthCalledWith(4, LogLevel.Info, 'EventManager: info msg four'); + }); + + it('should format the message with the give parameters', () => { + const logHandler = mockLogHandler(); + + const logger = new OptimizelyLogger({ + name: 'EventManager', + logHandler, + infoMsgResolver: mockMessageResolver('info'), + errorMsgResolver: mockMessageResolver('err'), + level: LogLevel.Debug, + }); + + logger.warn('msg %s, %s', 'one', 1); + logger.error('msg %s', 'two'); + logger.debug('msg three', 9999); + logger.info('msg four%s%s', '!', '!'); + expect(logHandler.log).toHaveBeenCalledTimes(4); + expect(logHandler.log).toHaveBeenNthCalledWith(1, LogLevel.Warn, 'EventManager: err msg one, 1'); + expect(logHandler.log).toHaveBeenNthCalledWith(2, LogLevel.Error, 'EventManager: err msg two'); + expect(logHandler.log).toHaveBeenNthCalledWith(3, LogLevel.Debug, 'EventManager: info msg three'); + expect(logHandler.log).toHaveBeenNthCalledWith(4, LogLevel.Info, 'EventManager: info msg four!!'); + }); + + it('should log the message of the error object and ignore other arguments if first argument is an error object \ + other that OptimizelyError', () => { + const logHandler = mockLogHandler(); + + const logger = new OptimizelyLogger({ + name: 'EventManager', + logHandler, + infoMsgResolver: mockMessageResolver('info'), + errorMsgResolver: mockMessageResolver('err'), + level: LogLevel.Debug, + }); + logger.debug(new Error('msg debug %s'), 'a'); + logger.info(new Error('msg info %s'), 'b'); + logger.warn(new Error('msg warn %s'), 'c'); + logger.error(new Error('msg error %s'), 'd'); + + expect(logHandler.log).toHaveBeenCalledTimes(4); + expect(logHandler.log).toHaveBeenNthCalledWith(1, LogLevel.Debug, 'EventManager: msg debug %s'); + expect(logHandler.log).toHaveBeenNthCalledWith(2, LogLevel.Info, 'EventManager: msg info %s'); + expect(logHandler.log).toHaveBeenNthCalledWith(3, LogLevel.Warn, 'EventManager: msg warn %s'); + expect(logHandler.log).toHaveBeenNthCalledWith(4, LogLevel.Error, 'EventManager: msg error %s'); + }); + + it('should resolve and log the message of an OptimizelyError using error resolver and ignore other arguments', () => { + const logHandler = mockLogHandler(); + + const logger = new OptimizelyLogger({ + name: 'EventManager', + logHandler, + infoMsgResolver: mockMessageResolver('info'), + errorMsgResolver: mockMessageResolver('err'), + level: LogLevel.Debug, + }); + + const err = new OptimizelyError('msg %s %s', 1, 2); + logger.debug(err, 'a'); + logger.info(err, 'a'); + logger.warn(err, 'a'); + logger.error(err, 'a'); + + expect(logHandler.log).toHaveBeenCalledTimes(4); + expect(logHandler.log).toHaveBeenNthCalledWith(1, LogLevel.Debug, 'EventManager: err msg 1 2'); + expect(logHandler.log).toHaveBeenNthCalledWith(2, LogLevel.Info, 'EventManager: err msg 1 2'); + expect(logHandler.log).toHaveBeenNthCalledWith(3, LogLevel.Warn, 'EventManager: err msg 1 2'); + expect(logHandler.log).toHaveBeenNthCalledWith(4, LogLevel.Error, 'EventManager: err msg 1 2'); + }); + + it('should return a new logger with the new name but same level, handler and resolvers when child() is called', () => { + const logHandler = mockLogHandler(); + + const logger = new OptimizelyLogger({ + name: 'EventManager', + logHandler, + infoMsgResolver: mockMessageResolver('info'), + errorMsgResolver: mockMessageResolver('err'), + level: LogLevel.Info, + }); + + const childLogger = logger.child('ChildLogger'); + childLogger.debug('msg one %s', 1); + childLogger.info('msg two %s', 2); + childLogger.warn('msg three %s', 3); + childLogger.error('msg four %s', 4); + + expect(logHandler.log).toHaveBeenCalledTimes(3); + expect(logHandler.log).toHaveBeenNthCalledWith(1, LogLevel.Info, 'ChildLogger: info msg two 2'); + expect(logHandler.log).toHaveBeenNthCalledWith(2, LogLevel.Warn, 'ChildLogger: err msg three 3'); + expect(logHandler.log).toHaveBeenNthCalledWith(3, LogLevel.Error, 'ChildLogger: err msg four 4'); + }); +}); diff --git a/lib/logging/logger.ts b/lib/logging/logger.ts index 408a06710..568bd2cac 100644 --- a/lib/logging/logger.ts +++ b/lib/logging/logger.ts @@ -1,7 +1,7 @@ /** - * Copyright 2019, 2024, Optimizely + * Copyright 2019, 2024, 2025, Optimizely * - * Licensed under the Apache License, Version 2.0 (the "License"); + * Licensed under the Apache License, Version 2.0 (the License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * @@ -24,6 +24,20 @@ export enum LogLevel { Error, } +export const LogLevelToUpper: Record = { + [LogLevel.Debug]: 'DEBUG', + [LogLevel.Info]: 'INFO', + [LogLevel.Warn]: 'WARN', + [LogLevel.Error]: 'ERROR', +}; + +export const LogLevelToLower: Record = { + [LogLevel.Debug]: 'debug', + [LogLevel.Info]: 'info', + [LogLevel.Warn]: 'warn', + [LogLevel.Error]: 'error', +}; + export interface LoggerFacade { info(message: string | Error, ...args: any[]): void; debug(message: string | Error, ...args: any[]): void; @@ -44,7 +58,7 @@ export class ConsoleLogHandler implements LogHandler { } log(level: LogLevel, message: string) : void { - const log = `${this.prefix} - ${level} ${this.getTime()} ${message}` + const log = `${this.prefix} - ${LogLevelToUpper[level]} ${this.getTime()} ${message}` this.consoleLog(level, log) } @@ -53,9 +67,10 @@ export class ConsoleLogHandler implements LogHandler { } private consoleLog(logLevel: LogLevel, log: string) : void { - const methodName = LogLevel[logLevel].toLowerCase() + const methodName: string = LogLevelToLower[logLevel]; + const method: any = console[methodName as keyof Console] || console.log; - method.bind(console)(log); + method.call(console, log); } } @@ -90,7 +105,7 @@ export class OptimizelyLogger implements LoggerFacade { infoMsgResolver: this.infoResolver, errorMsgResolver: this.errorResolver, level: this.level, - name: `${this.name}.${name}`, + name, }); } @@ -111,11 +126,13 @@ export class OptimizelyLogger implements LoggerFacade { } private handleLog(level: LogLevel, message: string, args: any[]) { - const log = `${this.prefix}${sprintf(message, ...args)}` + const log = args.length > 0 ? `${this.prefix}${sprintf(message, ...args)}` + : `${this.prefix}${message}`; + this.logHandler.log(level, log); } - private log(level: LogLevel, message: string | Error, ...args: any[]): void { + private log(level: LogLevel, message: string | Error, args: any[]): void { if (level < this.level) { return; } diff --git a/lib/logging/logger_factory.spec.ts b/lib/logging/logger_factory.spec.ts new file mode 100644 index 000000000..b39524c6e --- /dev/null +++ b/lib/logging/logger_factory.spec.ts @@ -0,0 +1,66 @@ +/** + * Copyright 2025, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { describe, it, expect, beforeEach, vi } from 'vitest'; + +vi.mock('./logger', async (importOriginal) => { + const actual = await importOriginal() + + const MockLogger = vi.fn(); + const MockConsoleLogHandler = vi.fn(); + + return { ...actual as any, OptimizelyLogger: MockLogger, ConsoleLogHandler: MockConsoleLogHandler }; +}); + +import { OptimizelyLogger, ConsoleLogHandler, LogLevel } from './logger'; +import { createLogger, extractLogger, InfoLog } from './logger_factory'; +import { errorResolver, infoResolver } from '../message/message_resolver'; + +describe('create', () => { + const MockedOptimizelyLogger = vi.mocked(OptimizelyLogger); + const MockedConsoleLogHandler = vi.mocked(ConsoleLogHandler); + + beforeEach(() => { + MockedConsoleLogHandler.mockClear(); + MockedOptimizelyLogger.mockClear(); + }); + + it('should use the passed in options and a default name Optimizely', () => { + const mockLogHandler = { log: vi.fn() }; + + const logger = extractLogger(createLogger({ + level: InfoLog, + logHandler: mockLogHandler, + })); + + expect(logger).toBe(MockedOptimizelyLogger.mock.instances[0]); + const { name, level, infoMsgResolver, errorMsgResolver, logHandler } = MockedOptimizelyLogger.mock.calls[0][0]; + expect(name).toBe('Optimizely'); + expect(level).toBe(LogLevel.Info); + expect(infoMsgResolver).toBe(infoResolver); + expect(errorMsgResolver).toBe(errorResolver); + expect(logHandler).toBe(mockLogHandler); + }); + + it('should use a ConsoleLogHandler if no logHandler is provided', () => { + const logger = extractLogger(createLogger({ + level: InfoLog, + })); + + expect(logger).toBe(MockedOptimizelyLogger.mock.instances[0]); + const { logHandler } = MockedOptimizelyLogger.mock.calls[0][0]; + expect(logHandler).toBe(MockedConsoleLogHandler.mock.instances[0]); + }); +}); \ No newline at end of file diff --git a/lib/logging/logger_factory.ts b/lib/logging/logger_factory.ts index 37e68a801..09d3440fc 100644 --- a/lib/logging/logger_factory.ts +++ b/lib/logging/logger_factory.ts @@ -1,20 +1,102 @@ -// import { LogLevel, LogResolver } from './logger'; +/** + * Copyright 2025, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { ConsoleLogHandler, LogHandler, LogLevel, OptimizelyLogger } from './logger'; +import { errorResolver, infoResolver, MessageResolver } from '../message/message_resolver'; -// type LevelPreset = { -// level: LogLevel; -// resolver?: LogResolver; -// } +type LevelPreset = { + level: LogLevel, + infoResolver?: MessageResolver, + errorResolver: MessageResolver, +} -// const levelPresetSymbol = Symbol('levelPreset'); +const debugPreset: LevelPreset = { + level: LogLevel.Debug, + infoResolver, + errorResolver, +}; -// export type OpaqueLevelPreset = { -// [levelPresetSymbol]: unknown; -// }; +const infoPreset: LevelPreset = { + level: LogLevel.Info, + infoResolver, + errorResolver, +} -// const Info: LevelPreset = { -// level: LogLevel.Info, -// }; +const warnPreset: LevelPreset = { + level: LogLevel.Warn, + errorResolver, +} -// export const InfoLog: OpaqueLevelPreset = { -// [levelPresetSymbol]: Info, -// }; +const errorPreset: LevelPreset = { + level: LogLevel.Error, + errorResolver, +} + +const levelPresetSymbol = Symbol(); + +export type OpaqueLevelPreset = { + [levelPresetSymbol]: unknown; +}; + +export const DebugLog: OpaqueLevelPreset = { + [levelPresetSymbol]: debugPreset, +}; + +export const InfoLog: OpaqueLevelPreset = { + [levelPresetSymbol]: infoPreset, +}; + +export const WarnLog: OpaqueLevelPreset = { + [levelPresetSymbol]: warnPreset, +}; + +export const ErrorLog: OpaqueLevelPreset = { + [levelPresetSymbol]: errorPreset, +}; + +export const extractLevelPreset = (preset: OpaqueLevelPreset): LevelPreset => { + return preset[levelPresetSymbol] as LevelPreset; +} + +const loggerSymbol = Symbol(); + +export type OpaqueLogger = { + [loggerSymbol]: unknown; +}; + +export type LoggerConfig = { + level: OpaqueLevelPreset, + logHandler?: LogHandler, +}; + +export const createLogger = (config: LoggerConfig): OpaqueLogger => { + const { level, infoResolver, errorResolver } = extractLevelPreset(config.level); + + const loggerName = 'Optimizely'; + + return { + [loggerSymbol]: new OptimizelyLogger({ + name: loggerName, + level, + infoMsgResolver: infoResolver, + errorMsgResolver: errorResolver, + logHandler: config.logHandler || new ConsoleLogHandler(), + }), + }; +}; + +export const extractLogger = (logger: OpaqueLogger): OptimizelyLogger => { + return logger[loggerSymbol] as OptimizelyLogger; +} diff --git a/lib/service.ts b/lib/service.ts index a43bcadc0..03e23ee67 100644 --- a/lib/service.ts +++ b/lib/service.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { LoggerFacade, LogLevel } from './logging/logger' +import { LoggerFacade, LogLevel, LogLevelToLower } from './logging/logger' import { resolvablePromise, ResolvablePromise } from "./utils/promise/resolvablePromise"; @@ -86,7 +86,7 @@ export abstract class BaseService implements Service { } for (const { level, message, params } of this.startupLogs) { - const methodName = LogLevel[level].toLowerCase(); + const methodName: string = LogLevelToLower[level]; const method = this.logger[methodName as keyof LoggerFacade]; method.call(this.logger, message, ...params); } diff --git a/lib/shared_types.ts b/lib/shared_types.ts index b38f096cc..725d84090 100644 --- a/lib/shared_types.ts +++ b/lib/shared_types.ts @@ -40,11 +40,15 @@ import { EventDispatcher } from './event_processor/event_dispatcher/event_dispat import { EventProcessor } from './event_processor/event_processor'; import { VuidManager } from './vuid/vuid_manager'; import { ErrorNotifier } from './error/error_notifier'; +import { OpaqueLogger } from './logging/logger_factory'; +import { OpaqueErrorNotifier } from './error/error_notifier_factory'; export { EventDispatcher } from './event_processor/event_dispatcher/event_dispatcher'; export { EventProcessor } from './event_processor/event_processor'; export { NotificationCenter } from './notification_center'; export { VuidManager } from './vuid/vuid_manager'; +export { OpaqueLogger } from './logging/logger_factory'; +export { OpaqueErrorNotifier } from './error/error_notifier_factory'; export interface BucketerParams { experimentId: string; @@ -365,18 +369,13 @@ export interface TrackListenerPayload extends ListenerPayload { */ export interface Config { projectConfigManager: ProjectConfigManager; - // errorHandler object for logging error - errorHandler?: ErrorHandler; - // event processor eventProcessor?: EventProcessor; - // event dispatcher to use when closing - closingEventDispatcher?: EventDispatcher; // The object to validate against the schema jsonSchemaValidator?: { validate(jsonObject: unknown): boolean; }; - // LogHandler object for logging - logger?: LoggerFacade; + logger?: OpaqueLogger; + errorNotifier?: OpaqueErrorNotifier; // user profile that contains user information userProfileService?: UserProfileService; // dafault options for decide API