From 212238955dc801f6846ed20cc123e913388b1546 Mon Sep 17 00:00:00 2001 From: Md Junaed Hossain <169046794+junaed-optimizely@users.noreply.github.com> Date: Mon, 6 Apr 2026 20:03:19 +0600 Subject: [PATCH 1/6] [FSSDK-12298] simple logger impl. --- src/logger/ReactLogger.ts | 71 +++++++++++++++++++++++++++++ src/logger/createLogger.ts | 45 ++++++++++++++++++ src/logger/index.ts | 19 ++++++++ src/provider/OptimizelyProvider.tsx | 10 +--- 4 files changed, 137 insertions(+), 8 deletions(-) create mode 100644 src/logger/ReactLogger.ts create mode 100644 src/logger/createLogger.ts create mode 100644 src/logger/index.ts diff --git a/src/logger/ReactLogger.ts b/src/logger/ReactLogger.ts new file mode 100644 index 0000000..39e60d7 --- /dev/null +++ b/src/logger/ReactLogger.ts @@ -0,0 +1,71 @@ +/** + * Copyright 2026, 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 { LogLevel } from '@optimizely/optimizely-sdk'; +import type { LogHandler } from '@optimizely/optimizely-sdk'; + +export interface ReactLogger { + debug(message: string): void; + info(message: string): void; + warn(message: string): void; + error(message: string): void; +} + +export interface ReactLoggerConfig { + logLevel: LogLevel; + logHandler?: LogHandler; +} + +const LOG_PREFIX = '[ReactSDK]'; + +const defaultLogHandler: LogHandler = { + log(level: LogLevel, message: string): void { + switch (level) { + case LogLevel.Debug: + console.debug(message); + break; + case LogLevel.Info: + console.info(message); + break; + case LogLevel.Warn: + console.warn(message); + break; + case LogLevel.Error: + console.error(message); + break; + } + }, +}; + +export function createReactLogger(config: ReactLoggerConfig): ReactLogger { + const handler = config.logHandler ?? defaultLogHandler; + const level = config.logLevel; + + return { + debug: (msg) => { + if (level <= LogLevel.Debug) handler.log(LogLevel.Debug, `${LOG_PREFIX} ${msg}`); + }, + info: (msg) => { + if (level <= LogLevel.Info) handler.log(LogLevel.Info, `${LOG_PREFIX} ${msg}`); + }, + warn: (msg) => { + if (level <= LogLevel.Warn) handler.log(LogLevel.Warn, `${LOG_PREFIX} ${msg}`); + }, + error: (msg) => { + if (level <= LogLevel.Error) handler.log(LogLevel.Error, `${LOG_PREFIX} ${msg}`); + }, + }; +} diff --git a/src/logger/createLogger.ts b/src/logger/createLogger.ts new file mode 100644 index 0000000..542c2bc --- /dev/null +++ b/src/logger/createLogger.ts @@ -0,0 +1,45 @@ +/** + * Copyright 2026, 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 { createLogger as jsCreateLogger, LogLevel, DEBUG, INFO, WARN, ERROR } from '@optimizely/optimizely-sdk'; +import type { LoggerConfig, OpaqueLevelPreset } from '@optimizely/optimizely-sdk'; +import { createReactLogger } from './ReactLogger'; +import type { ReactLogger } from './ReactLogger'; + +let reactLogger: ReactLogger | undefined; + +function resolveLogLevel(preset: OpaqueLevelPreset): LogLevel { + if (preset === DEBUG) return LogLevel.Debug; + if (preset === INFO) return LogLevel.Info; + if (preset === WARN) return LogLevel.Warn; + if (preset === ERROR) return LogLevel.Error; + return LogLevel.Error; +} + +export function createLogger(config: LoggerConfig) { + const opaqueLogger = jsCreateLogger(config); + + reactLogger = createReactLogger({ + logLevel: resolveLogLevel(config.level), + logHandler: config.logHandler, + }); + + return opaqueLogger; +} + +export function getReactLogger(): ReactLogger | undefined { + return reactLogger; +} diff --git a/src/logger/index.ts b/src/logger/index.ts new file mode 100644 index 0000000..3e9cfce --- /dev/null +++ b/src/logger/index.ts @@ -0,0 +1,19 @@ +/** + * Copyright 2026, 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. + */ + +export { createLogger, getReactLogger } from './createLogger'; +export { createReactLogger } from './ReactLogger'; +export type { ReactLogger, ReactLoggerConfig } from './ReactLogger'; diff --git a/src/provider/OptimizelyProvider.tsx b/src/provider/OptimizelyProvider.tsx index 1f3a6a8..07db832 100644 --- a/src/provider/OptimizelyProvider.tsx +++ b/src/provider/OptimizelyProvider.tsx @@ -19,16 +19,10 @@ import { NOTIFICATION_TYPES } from '@optimizely/optimizely-sdk'; import { ProviderStateStore } from './ProviderStateStore'; import { UserContextManager } from '../utils/UserContextManager'; +import { getReactLogger } from '../logger/index'; import type { OptimizelyProviderProps, OptimizelyContextValue } from './types'; import type { Client } from '@optimizely/optimizely-sdk'; -// TODO: Replace with proper logger when implemented -const logger = { - info: (msg: string) => console.info(`[OptimizelyProvider] ${msg}`), - warn: (msg: string) => console.warn(`[OptimizelyProvider] ${msg}`), - error: (msg: string) => console.error(`[OptimizelyProvider] ${msg}`), -}; - /** * React Context for Optimizely. */ @@ -81,7 +75,7 @@ export function OptimizelyProvider({ // Readiness is derived from userContext + getOptimizelyConfig() by hooks. useEffect(() => { if (!client) { - logger?.error('OptimizelyProvider must be passed an Optimizely client instance'); + getReactLogger()?.error('OptimizelyProvider must be passed an Optimizely client instance'); store.setError(new Error('Optimizely client is required')); return; } From 18ba1908a8da1fba474da79ee5d07a0a21e94c3e Mon Sep 17 00:00:00 2001 From: Md Junaed Hossain <169046794+junaed-optimizely@users.noreply.github.com> Date: Mon, 6 Apr 2026 20:06:57 +0600 Subject: [PATCH 2/6] [FSSDK-12298] test addition --- src/logger/createLogger.spec.ts | 122 ++++++++++++++++++++++++++++++++ vitest.config.mts | 3 +- 2 files changed, 124 insertions(+), 1 deletion(-) create mode 100644 src/logger/createLogger.spec.ts diff --git a/src/logger/createLogger.spec.ts b/src/logger/createLogger.spec.ts new file mode 100644 index 0000000..a599429 --- /dev/null +++ b/src/logger/createLogger.spec.ts @@ -0,0 +1,122 @@ +/** + * Copyright 2026, 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 { vi, describe, it, expect, beforeEach } from 'vitest'; +import { DEBUG, INFO, WARN, ERROR, LogLevel } from '@optimizely/optimizely-sdk'; +import type { LoggerConfig, LogHandler } from '@optimizely/optimizely-sdk'; + +const mockOpaqueLogger = vi.hoisted(() => ({ __opaque: true })); + +vi.mock('@optimizely/optimizely-sdk', async (importOriginal) => { + const original = await importOriginal(); + return { + ...original, + createLogger: vi.fn().mockReturnValue(mockOpaqueLogger), + }; +}); + +import { createLogger, getReactLogger } from './createLogger'; + +describe('createLogger', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('should return the opaque logger from the JS SDK', () => { + const config: LoggerConfig = { level: INFO }; + const result = createLogger(config); + expect(result).toBe(mockOpaqueLogger); + }); + + it('should make the ReactLogger available via getReactLogger', () => { + const config: LoggerConfig = { level: INFO }; + createLogger(config); + + const reactLogger = getReactLogger(); + expect(reactLogger).toBeDefined(); + expect(typeof reactLogger!.debug).toBe('function'); + expect(typeof reactLogger!.info).toBe('function'); + expect(typeof reactLogger!.warn).toBe('function'); + expect(typeof reactLogger!.error).toBe('function'); + }); + + describe('log level filtering', () => { + it('should filter messages below the configured level', () => { + const mockHandler: LogHandler = { log: vi.fn() }; + createLogger({ level: WARN, logHandler: mockHandler }); + + const logger = getReactLogger()!; + logger.debug('should not appear'); + logger.info('should not appear'); + logger.warn('should appear'); + logger.error('should appear'); + + expect(mockHandler.log).toHaveBeenCalledTimes(2); + expect(mockHandler.log).toHaveBeenCalledWith(LogLevel.Warn, '[ReactSDK] should appear'); + expect(mockHandler.log).toHaveBeenCalledWith(LogLevel.Error, '[ReactSDK] should appear'); + }); + + it('should allow all messages when level is DEBUG', () => { + const mockHandler: LogHandler = { log: vi.fn() }; + createLogger({ level: DEBUG, logHandler: mockHandler }); + + const logger = getReactLogger()!; + logger.debug('d'); + logger.info('i'); + logger.warn('w'); + logger.error('e'); + + expect(mockHandler.log).toHaveBeenCalledTimes(4); + }); + + it('should only allow error messages when level is ERROR', () => { + const mockHandler: LogHandler = { log: vi.fn() }; + createLogger({ level: ERROR, logHandler: mockHandler }); + + const logger = getReactLogger()!; + logger.debug('d'); + logger.info('i'); + logger.warn('w'); + logger.error('e'); + + expect(mockHandler.log).toHaveBeenCalledTimes(1); + expect(mockHandler.log).toHaveBeenCalledWith(LogLevel.Error, '[ReactSDK] e'); + }); + }); + + describe('custom log handler', () => { + it('should use the provided logHandler', () => { + const mockHandler: LogHandler = { log: vi.fn() }; + createLogger({ level: INFO, logHandler: mockHandler }); + + const logger = getReactLogger()!; + logger.info('hello'); + + expect(mockHandler.log).toHaveBeenCalledWith(LogLevel.Info, '[ReactSDK] hello'); + }); + + it('should use default console handler when logHandler is not provided', () => { + const consoleSpy = vi.spyOn(console, 'info').mockImplementation(() => {}); + createLogger({ level: INFO }); + + const logger = getReactLogger()!; + logger.info('hello'); + + expect(consoleSpy).toHaveBeenCalledWith('[ReactSDK] hello'); + consoleSpy.mockRestore(); + }); + }); +}); diff --git a/vitest.config.mts b/vitest.config.mts index dd653e6..2f40632 100644 --- a/vitest.config.mts +++ b/vitest.config.mts @@ -12,13 +12,14 @@ export default defineConfig({ 'src/provider/**/*.spec.{ts,tsx}', 'src/utils/**/*.spec.{ts,tsx}', 'src/hooks/**/*.spec.{ts,tsx}', + 'src/logger/**/*.spec.{ts,tsx}', // Add more paths as migration progresses ], coverage: { provider: 'v8', reporter: ['text-summary', 'lcov'], reportsDirectory: './coverage', - include: ['src/client/**', 'src/provider/**', 'src/utils/**', 'src/hooks/**'], + include: ['src/client/**', 'src/provider/**', 'src/utils/**', 'src/hooks/**', 'src/logger/**'], }, }, }); From dd51a392527ff234b61e6da3d6340c5c173ebd8f Mon Sep 17 00:00:00 2001 From: Md Junaed Hossain <169046794+junaed-optimizely@users.noreply.github.com> Date: Mon, 6 Apr 2026 20:11:55 +0600 Subject: [PATCH 3/6] [FSSDK-12298] export adjustment --- src/client/index.ts | 1 - src/index.ts | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/client/index.ts b/src/client/index.ts index ae63d95..e3c002f 100644 --- a/src/client/index.ts +++ b/src/client/index.ts @@ -27,5 +27,4 @@ export { createOdpManager, createVuidManager, createErrorNotifier, - createLogger, // This will be removed later with logger implementation changes } from '@optimizely/optimizely-sdk'; diff --git a/src/index.ts b/src/index.ts index 8cbfacb..4f6eb6b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -23,8 +23,8 @@ export { createOdpManager, createVuidManager, createErrorNotifier, - createLogger, } from './client/index'; +export { createLogger } from './logger/index'; export type * from '@optimizely/optimizely-sdk'; From 0aca0d336268c302d85db84bb6d7b59ae3c9d6ab Mon Sep 17 00:00:00 2001 From: Md Junaed Hossain <169046794+junaed-optimizely@users.noreply.github.com> Date: Mon, 6 Apr 2026 20:49:18 +0600 Subject: [PATCH 4/6] [FSSDK-12298] singleton removal --- src/client/createInstance.ts | 5 ++ src/logger/createLogger.spec.ts | 101 +++++++++++++++++++++------- src/logger/createLogger.ts | 11 +-- src/logger/getReactLogger.ts | 39 +++++++++++ src/logger/index.ts | 3 +- src/logger/loggerConfigRegistry.ts | 30 +++++++++ src/provider/OptimizelyProvider.tsx | 3 +- 7 files changed, 156 insertions(+), 36 deletions(-) create mode 100644 src/logger/getReactLogger.ts create mode 100644 src/logger/loggerConfigRegistry.ts diff --git a/src/client/createInstance.ts b/src/client/createInstance.ts index b6ee036..1e02715 100644 --- a/src/client/createInstance.ts +++ b/src/client/createInstance.ts @@ -16,6 +16,8 @@ import { createInstance as jsCreateInstance } from '@optimizely/optimizely-sdk'; import type { Config, Client } from '@optimizely/optimizely-sdk'; +import { getLoggerConfig } from '../logger/loggerConfigRegistry'; +import type { ReactLoggerConfig, ReactLogger } from '../logger/ReactLogger'; export const CLIENT_ENGINE = 'react-sdk'; export const CLIENT_VERSION = '4.0.0'; @@ -25,6 +27,8 @@ export const REACT_CLIENT_META = Symbol('react-client-meta'); export interface ReactClientMeta { hasOdpManager: boolean; hasVuidManager: boolean; + loggerConfig?: ReactLoggerConfig; + logger?: ReactLogger; } /** @@ -48,6 +52,7 @@ export function createInstance(config: Config): Client { reactClient[REACT_CLIENT_META] = { hasOdpManager: !!config.odpManager, hasVuidManager: !!config.vuidManager, + loggerConfig: config.logger ? getLoggerConfig(config.logger) : undefined, } satisfies ReactClientMeta; return reactClient; diff --git a/src/logger/createLogger.spec.ts b/src/logger/createLogger.spec.ts index a599429..1714514 100644 --- a/src/logger/createLogger.spec.ts +++ b/src/logger/createLogger.spec.ts @@ -17,6 +17,9 @@ import { vi, describe, it, expect, beforeEach } from 'vitest'; import { DEBUG, INFO, WARN, ERROR, LogLevel } from '@optimizely/optimizely-sdk'; import type { LoggerConfig, LogHandler } from '@optimizely/optimizely-sdk'; +import { storeLoggerConfig, getLoggerConfig } from './loggerConfigRegistry'; +import { createReactLogger } from './ReactLogger'; +import type { ReactLoggerConfig } from './ReactLogger'; const mockOpaqueLogger = vi.hoisted(() => ({ __opaque: true })); @@ -28,7 +31,7 @@ vi.mock('@optimizely/optimizely-sdk', async (importOriginal) => { }; }); -import { createLogger, getReactLogger } from './createLogger'; +import { createLogger } from './createLogger'; describe('createLogger', () => { beforeEach(() => { @@ -41,24 +44,62 @@ describe('createLogger', () => { expect(result).toBe(mockOpaqueLogger); }); - it('should make the ReactLogger available via getReactLogger', () => { - const config: LoggerConfig = { level: INFO }; - createLogger(config); - - const reactLogger = getReactLogger(); - expect(reactLogger).toBeDefined(); - expect(typeof reactLogger!.debug).toBe('function'); - expect(typeof reactLogger!.info).toBe('function'); - expect(typeof reactLogger!.warn).toBe('function'); - expect(typeof reactLogger!.error).toBe('function'); + it('should store the resolved config in the registry', () => { + const mockHandler: LogHandler = { log: vi.fn() }; + createLogger({ level: INFO, logHandler: mockHandler }); + + const storedConfig = getLoggerConfig(mockOpaqueLogger); + expect(storedConfig).toBeDefined(); + expect(storedConfig!.logLevel).toBe(LogLevel.Info); + expect(storedConfig!.logHandler).toBe(mockHandler); + }); + + describe('log level resolution', () => { + it.each([ + { preset: DEBUG, expected: LogLevel.Debug, name: 'DEBUG' }, + { preset: INFO, expected: LogLevel.Info, name: 'INFO' }, + { preset: WARN, expected: LogLevel.Warn, name: 'WARN' }, + { preset: ERROR, expected: LogLevel.Error, name: 'ERROR' }, + ])('should resolve $name preset to LogLevel.$name', ({ preset, expected }) => { + createLogger({ level: preset }); + const storedConfig = getLoggerConfig(mockOpaqueLogger); + expect(storedConfig!.logLevel).toBe(expected); + }); }); +}); + +describe('loggerConfigRegistry', () => { + it('should return undefined for unknown logger objects', () => { + expect(getLoggerConfig({})).toBeUndefined(); + }); + + it('should store and retrieve config for a given logger', () => { + const logger = {}; + const config: ReactLoggerConfig = { logLevel: LogLevel.Warn }; + storeLoggerConfig(logger, config); + expect(getLoggerConfig(logger)).toBe(config); + }); + + it('should support multiple loggers with different configs', () => { + const logger1 = {}; + const logger2 = {}; + const config1: ReactLoggerConfig = { logLevel: LogLevel.Debug }; + const config2: ReactLoggerConfig = { logLevel: LogLevel.Error }; + storeLoggerConfig(logger1, config1); + storeLoggerConfig(logger2, config2); + + expect(getLoggerConfig(logger1)).toBe(config1); + expect(getLoggerConfig(logger2)).toBe(config2); + }); +}); + +describe('createReactLogger', () => { describe('log level filtering', () => { it('should filter messages below the configured level', () => { const mockHandler: LogHandler = { log: vi.fn() }; - createLogger({ level: WARN, logHandler: mockHandler }); + const logger = createReactLogger({ logLevel: LogLevel.Warn, logHandler: mockHandler }); - const logger = getReactLogger()!; logger.debug('should not appear'); logger.info('should not appear'); logger.warn('should appear'); @@ -69,11 +110,10 @@ describe('createLogger', () => { expect(mockHandler.log).toHaveBeenCalledWith(LogLevel.Error, '[ReactSDK] should appear'); }); - it('should allow all messages when level is DEBUG', () => { + it('should allow all messages when level is Debug', () => { const mockHandler: LogHandler = { log: vi.fn() }; - createLogger({ level: DEBUG, logHandler: mockHandler }); + const logger = createReactLogger({ logLevel: LogLevel.Debug, logHandler: mockHandler }); - const logger = getReactLogger()!; logger.debug('d'); logger.info('i'); logger.warn('w'); @@ -82,11 +122,10 @@ describe('createLogger', () => { expect(mockHandler.log).toHaveBeenCalledTimes(4); }); - it('should only allow error messages when level is ERROR', () => { + it('should only allow error messages when level is Error', () => { const mockHandler: LogHandler = { log: vi.fn() }; - createLogger({ level: ERROR, logHandler: mockHandler }); + const logger = createReactLogger({ logLevel: LogLevel.Error, logHandler: mockHandler }); - const logger = getReactLogger()!; logger.debug('d'); logger.info('i'); logger.warn('w'); @@ -97,12 +136,11 @@ describe('createLogger', () => { }); }); - describe('custom log handler', () => { + describe('log handler', () => { it('should use the provided logHandler', () => { const mockHandler: LogHandler = { log: vi.fn() }; - createLogger({ level: INFO, logHandler: mockHandler }); + const logger = createReactLogger({ logLevel: LogLevel.Info, logHandler: mockHandler }); - const logger = getReactLogger()!; logger.info('hello'); expect(mockHandler.log).toHaveBeenCalledWith(LogLevel.Info, '[ReactSDK] hello'); @@ -110,13 +148,28 @@ describe('createLogger', () => { it('should use default console handler when logHandler is not provided', () => { const consoleSpy = vi.spyOn(console, 'info').mockImplementation(() => {}); - createLogger({ level: INFO }); + const logger = createReactLogger({ logLevel: LogLevel.Info }); - const logger = getReactLogger()!; logger.info('hello'); expect(consoleSpy).toHaveBeenCalledWith('[ReactSDK] hello'); consoleSpy.mockRestore(); }); }); + + describe('message prefix', () => { + it('should prepend [ReactSDK] to all messages', () => { + const mockHandler: LogHandler = { log: vi.fn() }; + const logger = createReactLogger({ logLevel: LogLevel.Debug, logHandler: mockHandler }); + + logger.debug('test'); + logger.info('test'); + logger.warn('test'); + logger.error('test'); + + for (const call of (mockHandler.log as ReturnType).mock.calls) { + expect(call[1]).toMatch(/^\[ReactSDK\] /); + } + }); + }); }); diff --git a/src/logger/createLogger.ts b/src/logger/createLogger.ts index 542c2bc..4faee16 100644 --- a/src/logger/createLogger.ts +++ b/src/logger/createLogger.ts @@ -16,10 +16,7 @@ import { createLogger as jsCreateLogger, LogLevel, DEBUG, INFO, WARN, ERROR } from '@optimizely/optimizely-sdk'; import type { LoggerConfig, OpaqueLevelPreset } from '@optimizely/optimizely-sdk'; -import { createReactLogger } from './ReactLogger'; -import type { ReactLogger } from './ReactLogger'; - -let reactLogger: ReactLogger | undefined; +import { storeLoggerConfig } from './loggerConfigRegistry'; function resolveLogLevel(preset: OpaqueLevelPreset): LogLevel { if (preset === DEBUG) return LogLevel.Debug; @@ -32,14 +29,10 @@ function resolveLogLevel(preset: OpaqueLevelPreset): LogLevel { export function createLogger(config: LoggerConfig) { const opaqueLogger = jsCreateLogger(config); - reactLogger = createReactLogger({ + storeLoggerConfig(opaqueLogger, { logLevel: resolveLogLevel(config.level), logHandler: config.logHandler, }); return opaqueLogger; } - -export function getReactLogger(): ReactLogger | undefined { - return reactLogger; -} diff --git a/src/logger/getReactLogger.ts b/src/logger/getReactLogger.ts new file mode 100644 index 0000000..47035c4 --- /dev/null +++ b/src/logger/getReactLogger.ts @@ -0,0 +1,39 @@ +/** + * Copyright 2026, 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 type { Client } from '@optimizely/optimizely-sdk'; +import { REACT_CLIENT_META } from '../client/createInstance'; +import type { ReactClientMeta } from '../client/createInstance'; +import { createReactLogger } from './ReactLogger'; + +/** + * Returns the cached ReactLogger instance for the given client. + * Creates it lazily on first call; subsequent calls return the same instance. + * Returns undefined if the client has no logger config (e.g., logger was + * not created via the React SDK's createLogger wrapper). + */ +export function getReactLogger(client: Client) { + const meta = (client as unknown as Record)[REACT_CLIENT_META]; + + if (meta.logger) return meta.logger; + + if (meta.loggerConfig) { + meta.logger = createReactLogger(meta.loggerConfig); + return meta.logger; + } + + return undefined; +} diff --git a/src/logger/index.ts b/src/logger/index.ts index 3e9cfce..fec9cf2 100644 --- a/src/logger/index.ts +++ b/src/logger/index.ts @@ -14,6 +14,7 @@ * limitations under the License. */ -export { createLogger, getReactLogger } from './createLogger'; +export { createLogger } from './createLogger'; +export { getReactLogger } from './getReactLogger'; export { createReactLogger } from './ReactLogger'; export type { ReactLogger, ReactLoggerConfig } from './ReactLogger'; diff --git a/src/logger/loggerConfigRegistry.ts b/src/logger/loggerConfigRegistry.ts new file mode 100644 index 0000000..ccc23de --- /dev/null +++ b/src/logger/loggerConfigRegistry.ts @@ -0,0 +1,30 @@ +/** + * Copyright 2026, 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 type { ReactLoggerConfig } from './ReactLogger'; + +// WeakMap keyed by OpaqueLogger objects. Bridges the gap between +// createLogger() and createInstance() — supports multiple clients with +// different configs and automatically releases entries when the logger is GC'd. +const registry = new WeakMap(); + +export function storeLoggerConfig(logger: object, config: ReactLoggerConfig): void { + registry.set(logger, config); +} + +export function getLoggerConfig(logger: object): ReactLoggerConfig | undefined { + return registry.get(logger); +} diff --git a/src/provider/OptimizelyProvider.tsx b/src/provider/OptimizelyProvider.tsx index 07db832..9f67b61 100644 --- a/src/provider/OptimizelyProvider.tsx +++ b/src/provider/OptimizelyProvider.tsx @@ -19,7 +19,6 @@ import { NOTIFICATION_TYPES } from '@optimizely/optimizely-sdk'; import { ProviderStateStore } from './ProviderStateStore'; import { UserContextManager } from '../utils/UserContextManager'; -import { getReactLogger } from '../logger/index'; import type { OptimizelyProviderProps, OptimizelyContextValue } from './types'; import type { Client } from '@optimizely/optimizely-sdk'; @@ -75,7 +74,7 @@ export function OptimizelyProvider({ // Readiness is derived from userContext + getOptimizelyConfig() by hooks. useEffect(() => { if (!client) { - getReactLogger()?.error('OptimizelyProvider must be passed an Optimizely client instance'); + console.error('[ReactSDK] OptimizelyProvider must be passed an Optimizely client instance'); store.setError(new Error('Optimizely client is required')); return; } From 143f4e77ca67e17cd24979f50e5a18dc23359477 Mon Sep 17 00:00:00 2001 From: Md Junaed Hossain <169046794+junaed-optimizely@users.noreply.github.com> Date: Tue, 7 Apr 2026 21:25:38 +0600 Subject: [PATCH 5/6] [FSSDK-12298] client level attachment --- src/client/createInstance.ts | 14 +++-- src/index.ts | 2 +- src/logger/ReactLogger.ts | 11 ++-- src/logger/createLogger.spec.ts | 96 +++++++++++++----------------- src/logger/createLogger.ts | 9 +-- src/logger/getReactLogger.ts | 21 ++----- src/logger/index.ts | 1 + src/logger/loggerConfigRegistry.ts | 30 ---------- 8 files changed, 71 insertions(+), 113 deletions(-) delete mode 100644 src/logger/loggerConfigRegistry.ts diff --git a/src/client/createInstance.ts b/src/client/createInstance.ts index 1e02715..85d99bd 100644 --- a/src/client/createInstance.ts +++ b/src/client/createInstance.ts @@ -16,8 +16,8 @@ import { createInstance as jsCreateInstance } from '@optimizely/optimizely-sdk'; import type { Config, Client } from '@optimizely/optimizely-sdk'; -import { getLoggerConfig } from '../logger/loggerConfigRegistry'; -import type { ReactLoggerConfig, ReactLogger } from '../logger/ReactLogger'; +import { REACT_LOGGER } from '../logger/createLogger'; +import type { ReactLogger } from '../logger/ReactLogger'; export const CLIENT_ENGINE = 'react-sdk'; export const CLIENT_VERSION = '4.0.0'; @@ -27,7 +27,6 @@ export const REACT_CLIENT_META = Symbol('react-client-meta'); export interface ReactClientMeta { hasOdpManager: boolean; hasVuidManager: boolean; - loggerConfig?: ReactLoggerConfig; logger?: ReactLogger; } @@ -41,6 +40,13 @@ export interface ReactClientMeta { * @returns An OptimizelyClient instance with React SDK metadata */ export function createInstance(config: Config): Client { + let reactLogger: ReactLogger | undefined; + + if (config.logger) { + reactLogger = (config.logger as Record)[REACT_LOGGER] as ReactLogger | undefined; + delete (config.logger as Record)[REACT_LOGGER]; + } + const jsClient = jsCreateInstance({ ...config, clientEngine: CLIENT_ENGINE, @@ -52,7 +58,7 @@ export function createInstance(config: Config): Client { reactClient[REACT_CLIENT_META] = { hasOdpManager: !!config.odpManager, hasVuidManager: !!config.vuidManager, - loggerConfig: config.logger ? getLoggerConfig(config.logger) : undefined, + logger: reactLogger, } satisfies ReactClientMeta; return reactClient; diff --git a/src/index.ts b/src/index.ts index 4f6eb6b..c45a87a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -24,7 +24,7 @@ export { createVuidManager, createErrorNotifier, } from './client/index'; -export { createLogger } from './logger/index'; +export { createLogger, DEBUG, ERROR, WARN, INFO } from './logger/index'; export type * from '@optimizely/optimizely-sdk'; diff --git a/src/logger/ReactLogger.ts b/src/logger/ReactLogger.ts index 39e60d7..43a3f65 100644 --- a/src/logger/ReactLogger.ts +++ b/src/logger/ReactLogger.ts @@ -29,8 +29,7 @@ export interface ReactLoggerConfig { logHandler?: LogHandler; } -const LOG_PREFIX = '[ReactSDK]'; - +const LOG_PREFIX = '[OPTIMIZELY - REACT]'; const defaultLogHandler: LogHandler = { log(level: LogLevel, message: string): void { switch (level) { @@ -56,16 +55,16 @@ export function createReactLogger(config: ReactLoggerConfig): ReactLogger { return { debug: (msg) => { - if (level <= LogLevel.Debug) handler.log(LogLevel.Debug, `${LOG_PREFIX} ${msg}`); + if (level <= LogLevel.Debug) handler.log(LogLevel.Debug, `${LOG_PREFIX} - DEBUG ${msg}`); }, info: (msg) => { - if (level <= LogLevel.Info) handler.log(LogLevel.Info, `${LOG_PREFIX} ${msg}`); + if (level <= LogLevel.Info) handler.log(LogLevel.Info, `${LOG_PREFIX} - INFO ${msg}`); }, warn: (msg) => { - if (level <= LogLevel.Warn) handler.log(LogLevel.Warn, `${LOG_PREFIX} ${msg}`); + if (level <= LogLevel.Warn) handler.log(LogLevel.Warn, `${LOG_PREFIX} - WARN ${msg}`); }, error: (msg) => { - if (level <= LogLevel.Error) handler.log(LogLevel.Error, `${LOG_PREFIX} ${msg}`); + if (level <= LogLevel.Error) handler.log(LogLevel.Error, `${LOG_PREFIX} - ERROR ${msg}`); }, }; } diff --git a/src/logger/createLogger.spec.ts b/src/logger/createLogger.spec.ts index 1714514..1858123 100644 --- a/src/logger/createLogger.spec.ts +++ b/src/logger/createLogger.spec.ts @@ -16,10 +16,9 @@ import { vi, describe, it, expect, beforeEach } from 'vitest'; import { DEBUG, INFO, WARN, ERROR, LogLevel } from '@optimizely/optimizely-sdk'; -import type { LoggerConfig, LogHandler } from '@optimizely/optimizely-sdk'; -import { storeLoggerConfig, getLoggerConfig } from './loggerConfigRegistry'; +import type { LogHandler } from '@optimizely/optimizely-sdk'; import { createReactLogger } from './ReactLogger'; -import type { ReactLoggerConfig } from './ReactLogger'; +import type { ReactLogger } from './ReactLogger'; const mockOpaqueLogger = vi.hoisted(() => ({ __opaque: true })); @@ -31,7 +30,7 @@ vi.mock('@optimizely/optimizely-sdk', async (importOriginal) => { }; }); -import { createLogger } from './createLogger'; +import { createLogger, REACT_LOGGER } from './createLogger'; describe('createLogger', () => { beforeEach(() => { @@ -39,58 +38,49 @@ describe('createLogger', () => { }); it('should return the opaque logger from the JS SDK', () => { - const config: LoggerConfig = { level: INFO }; - const result = createLogger(config); + const result = createLogger({ level: INFO }); expect(result).toBe(mockOpaqueLogger); }); - it('should store the resolved config in the registry', () => { + it('should attach a ReactLogger via the REACT_LOGGER symbol', () => { const mockHandler: LogHandler = { log: vi.fn() }; - createLogger({ level: INFO, logHandler: mockHandler }); - - const storedConfig = getLoggerConfig(mockOpaqueLogger); - expect(storedConfig).toBeDefined(); - expect(storedConfig!.logLevel).toBe(LogLevel.Info); - expect(storedConfig!.logHandler).toBe(mockHandler); + const result = createLogger({ level: INFO, logHandler: mockHandler }); + + const reactLogger = (result as Record)[REACT_LOGGER] as ReactLogger; + expect(reactLogger).toBeDefined(); + expect(reactLogger.debug).toBeTypeOf('function'); + expect(reactLogger.info).toBeTypeOf('function'); + expect(reactLogger.warn).toBeTypeOf('function'); + expect(reactLogger.error).toBeTypeOf('function'); }); - describe('log level resolution', () => { - it.each([ - { preset: DEBUG, expected: LogLevel.Debug, name: 'DEBUG' }, - { preset: INFO, expected: LogLevel.Info, name: 'INFO' }, - { preset: WARN, expected: LogLevel.Warn, name: 'WARN' }, - { preset: ERROR, expected: LogLevel.Error, name: 'ERROR' }, - ])('should resolve $name preset to LogLevel.$name', ({ preset, expected }) => { - createLogger({ level: preset }); - const storedConfig = getLoggerConfig(mockOpaqueLogger); - expect(storedConfig!.logLevel).toBe(expected); - }); - }); -}); + it('should create a ReactLogger that uses the provided logHandler', () => { + const mockHandler: LogHandler = { log: vi.fn() }; + const result = createLogger({ level: INFO, logHandler: mockHandler }); -describe('loggerConfigRegistry', () => { - it('should return undefined for unknown logger objects', () => { - expect(getLoggerConfig({})).toBeUndefined(); - }); + const reactLogger = (result as Record)[REACT_LOGGER] as ReactLogger; + reactLogger.info('hello'); - it('should store and retrieve config for a given logger', () => { - const logger = {}; - const config: ReactLoggerConfig = { logLevel: LogLevel.Warn }; - storeLoggerConfig(logger, config); - expect(getLoggerConfig(logger)).toBe(config); + expect(mockHandler.log).toHaveBeenCalledWith(LogLevel.Info, '[OPTIMIZELY - REACT] - INFO hello'); }); - it('should support multiple loggers with different configs', () => { - const logger1 = {}; - const logger2 = {}; - const config1: ReactLoggerConfig = { logLevel: LogLevel.Debug }; - const config2: ReactLoggerConfig = { logLevel: LogLevel.Error }; - - storeLoggerConfig(logger1, config1); - storeLoggerConfig(logger2, config2); - - expect(getLoggerConfig(logger1)).toBe(config1); - expect(getLoggerConfig(logger2)).toBe(config2); + describe('log level resolution', () => { + it.each([ + { preset: DEBUG, expectedCalls: 4, name: 'DEBUG' }, + { preset: INFO, expectedCalls: 3, name: 'INFO' }, + { preset: WARN, expectedCalls: 2, name: 'WARN' }, + { preset: ERROR, expectedCalls: 1, name: 'ERROR' }, + ])('should resolve $name preset correctly', ({ preset, expectedCalls }) => { + const mockHandler: LogHandler = { log: vi.fn() }; + const result = createLogger({ level: preset, logHandler: mockHandler }); + + const reactLogger = (result as Record)[REACT_LOGGER] as ReactLogger; + reactLogger.debug('d'); + reactLogger.info('i'); + reactLogger.warn('w'); + reactLogger.error('e'); + expect(mockHandler.log).toHaveBeenCalledTimes(expectedCalls); + }); }); }); @@ -106,8 +96,8 @@ describe('createReactLogger', () => { logger.error('should appear'); expect(mockHandler.log).toHaveBeenCalledTimes(2); - expect(mockHandler.log).toHaveBeenCalledWith(LogLevel.Warn, '[ReactSDK] should appear'); - expect(mockHandler.log).toHaveBeenCalledWith(LogLevel.Error, '[ReactSDK] should appear'); + expect(mockHandler.log).toHaveBeenCalledWith(LogLevel.Warn, '[OPTIMIZELY - REACT] - WARN should appear'); + expect(mockHandler.log).toHaveBeenCalledWith(LogLevel.Error, '[OPTIMIZELY - REACT] - ERROR should appear'); }); it('should allow all messages when level is Debug', () => { @@ -132,7 +122,7 @@ describe('createReactLogger', () => { logger.error('e'); expect(mockHandler.log).toHaveBeenCalledTimes(1); - expect(mockHandler.log).toHaveBeenCalledWith(LogLevel.Error, '[ReactSDK] e'); + expect(mockHandler.log).toHaveBeenCalledWith(LogLevel.Error, '[OPTIMIZELY - REACT] - ERROR e'); }); }); @@ -143,7 +133,7 @@ describe('createReactLogger', () => { logger.info('hello'); - expect(mockHandler.log).toHaveBeenCalledWith(LogLevel.Info, '[ReactSDK] hello'); + expect(mockHandler.log).toHaveBeenCalledWith(LogLevel.Info, '[OPTIMIZELY - REACT] - INFO hello'); }); it('should use default console handler when logHandler is not provided', () => { @@ -152,13 +142,13 @@ describe('createReactLogger', () => { logger.info('hello'); - expect(consoleSpy).toHaveBeenCalledWith('[ReactSDK] hello'); + expect(consoleSpy).toHaveBeenCalledWith('[OPTIMIZELY - REACT] - INFO hello'); consoleSpy.mockRestore(); }); }); describe('message prefix', () => { - it('should prepend [ReactSDK] to all messages', () => { + it('should prepend [OPTIMIZELY - REACT] to all messages', () => { const mockHandler: LogHandler = { log: vi.fn() }; const logger = createReactLogger({ logLevel: LogLevel.Debug, logHandler: mockHandler }); @@ -168,7 +158,7 @@ describe('createReactLogger', () => { logger.error('test'); for (const call of (mockHandler.log as ReturnType).mock.calls) { - expect(call[1]).toMatch(/^\[ReactSDK\] /); + expect(call[1]).toMatch(/^\[OPTIMIZELY - REACT\] - (DEBUG|INFO|WARN|ERROR) /); } }); }); diff --git a/src/logger/createLogger.ts b/src/logger/createLogger.ts index 4faee16..54b2837 100644 --- a/src/logger/createLogger.ts +++ b/src/logger/createLogger.ts @@ -16,7 +16,9 @@ import { createLogger as jsCreateLogger, LogLevel, DEBUG, INFO, WARN, ERROR } from '@optimizely/optimizely-sdk'; import type { LoggerConfig, OpaqueLevelPreset } from '@optimizely/optimizely-sdk'; -import { storeLoggerConfig } from './loggerConfigRegistry'; +import { createReactLogger } from './ReactLogger'; + +export const REACT_LOGGER = Symbol('react-logger'); function resolveLogLevel(preset: OpaqueLevelPreset): LogLevel { if (preset === DEBUG) return LogLevel.Debug; @@ -28,11 +30,10 @@ function resolveLogLevel(preset: OpaqueLevelPreset): LogLevel { export function createLogger(config: LoggerConfig) { const opaqueLogger = jsCreateLogger(config); - - storeLoggerConfig(opaqueLogger, { + const reactLogger = createReactLogger({ logLevel: resolveLogLevel(config.level), logHandler: config.logHandler, }); - + (opaqueLogger as Record)[REACT_LOGGER] = reactLogger; return opaqueLogger; } diff --git a/src/logger/getReactLogger.ts b/src/logger/getReactLogger.ts index 47035c4..58287f3 100644 --- a/src/logger/getReactLogger.ts +++ b/src/logger/getReactLogger.ts @@ -17,23 +17,14 @@ import type { Client } from '@optimizely/optimizely-sdk'; import { REACT_CLIENT_META } from '../client/createInstance'; import type { ReactClientMeta } from '../client/createInstance'; -import { createReactLogger } from './ReactLogger'; +import type { ReactLogger } from './ReactLogger'; /** - * Returns the cached ReactLogger instance for the given client. - * Creates it lazily on first call; subsequent calls return the same instance. - * Returns undefined if the client has no logger config (e.g., logger was - * not created via the React SDK's createLogger wrapper). + * Returns the ReactLogger instance for the given client, or undefined + * if the client has no logger (e.g., logger was not created via the + * React SDK's createLogger wrapper). */ -export function getReactLogger(client: Client) { +export function getReactLogger(client: Client): ReactLogger | undefined { const meta = (client as unknown as Record)[REACT_CLIENT_META]; - - if (meta.logger) return meta.logger; - - if (meta.loggerConfig) { - meta.logger = createReactLogger(meta.loggerConfig); - return meta.logger; - } - - return undefined; + return meta.logger; } diff --git a/src/logger/index.ts b/src/logger/index.ts index fec9cf2..7642bc3 100644 --- a/src/logger/index.ts +++ b/src/logger/index.ts @@ -18,3 +18,4 @@ export { createLogger } from './createLogger'; export { getReactLogger } from './getReactLogger'; export { createReactLogger } from './ReactLogger'; export type { ReactLogger, ReactLoggerConfig } from './ReactLogger'; +export { ERROR, DEBUG, WARN, INFO } from '@optimizely/optimizely-sdk'; diff --git a/src/logger/loggerConfigRegistry.ts b/src/logger/loggerConfigRegistry.ts deleted file mode 100644 index ccc23de..0000000 --- a/src/logger/loggerConfigRegistry.ts +++ /dev/null @@ -1,30 +0,0 @@ -/** - * Copyright 2026, 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 type { ReactLoggerConfig } from './ReactLogger'; - -// WeakMap keyed by OpaqueLogger objects. Bridges the gap between -// createLogger() and createInstance() — supports multiple clients with -// different configs and automatically releases entries when the logger is GC'd. -const registry = new WeakMap(); - -export function storeLoggerConfig(logger: object, config: ReactLoggerConfig): void { - registry.set(logger, config); -} - -export function getLoggerConfig(logger: object): ReactLoggerConfig | undefined { - return registry.get(logger); -} From df684dabf036a8a61d5ecd60fec6e2daeab28584 Mon Sep 17 00:00:00 2001 From: Md Junaed Hossain <169046794+junaed-optimizely@users.noreply.github.com> Date: Wed, 8 Apr 2026 21:58:38 +0600 Subject: [PATCH 6/6] [FSSDK-12298] message update --- src/provider/OptimizelyProvider.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/provider/OptimizelyProvider.tsx b/src/provider/OptimizelyProvider.tsx index 9f67b61..eeb5027 100644 --- a/src/provider/OptimizelyProvider.tsx +++ b/src/provider/OptimizelyProvider.tsx @@ -74,7 +74,7 @@ export function OptimizelyProvider({ // Readiness is derived from userContext + getOptimizelyConfig() by hooks. useEffect(() => { if (!client) { - console.error('[ReactSDK] OptimizelyProvider must be passed an Optimizely client instance'); + console.error('[OPTIMIZELY - REACT] OptimizelyProvider must be passed an Optimizely client instance'); store.setError(new Error('Optimizely client is required')); return; }