Skip to content

Commit 57d1e67

Browse files
[FSSDK-12298] logger adjustment (#325)
1 parent 455eaab commit 57d1e67

File tree

10 files changed

+340
-11
lines changed

10 files changed

+340
-11
lines changed

src/client/createInstance.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616

1717
import { createInstance as jsCreateInstance } from '@optimizely/optimizely-sdk';
1818
import type { Config, Client } from '@optimizely/optimizely-sdk';
19+
import { REACT_LOGGER } from '../logger/createLogger';
20+
import type { ReactLogger } from '../logger/ReactLogger';
1921

2022
export const CLIENT_ENGINE = 'react-sdk';
2123
export const CLIENT_VERSION = '4.0.0';
@@ -25,6 +27,7 @@ export const REACT_CLIENT_META = Symbol('react-client-meta');
2527
export interface ReactClientMeta {
2628
hasOdpManager: boolean;
2729
hasVuidManager: boolean;
30+
logger?: ReactLogger;
2831
}
2932

3033
/**
@@ -37,6 +40,13 @@ export interface ReactClientMeta {
3740
* @returns An OptimizelyClient instance with React SDK metadata
3841
*/
3942
export function createInstance(config: Config): Client {
43+
let reactLogger: ReactLogger | undefined;
44+
45+
if (config.logger) {
46+
reactLogger = (config.logger as Record<symbol, unknown>)[REACT_LOGGER] as ReactLogger | undefined;
47+
delete (config.logger as Record<symbol, unknown>)[REACT_LOGGER];
48+
}
49+
4050
const jsClient = jsCreateInstance({
4151
...config,
4252
clientEngine: CLIENT_ENGINE,
@@ -48,6 +58,7 @@ export function createInstance(config: Config): Client {
4858
reactClient[REACT_CLIENT_META] = {
4959
hasOdpManager: !!config.odpManager,
5060
hasVuidManager: !!config.vuidManager,
61+
logger: reactLogger,
5162
} satisfies ReactClientMeta;
5263

5364
return reactClient;

src/client/index.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,5 +27,4 @@ export {
2727
createOdpManager,
2828
createVuidManager,
2929
createErrorNotifier,
30-
createLogger, // This will be removed later with logger implementation changes
3130
} from '@optimizely/optimizely-sdk';

src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,8 @@ export {
2323
createOdpManager,
2424
createVuidManager,
2525
createErrorNotifier,
26-
createLogger,
2726
} from './client/index';
27+
export { createLogger, DEBUG, ERROR, WARN, INFO } from './logger/index';
2828

2929
export type * from '@optimizely/optimizely-sdk';
3030

src/logger/ReactLogger.ts

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
/**
2+
* Copyright 2026, Optimizely
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import { LogLevel } from '@optimizely/optimizely-sdk';
18+
import type { LogHandler } from '@optimizely/optimizely-sdk';
19+
20+
export interface ReactLogger {
21+
debug(message: string): void;
22+
info(message: string): void;
23+
warn(message: string): void;
24+
error(message: string): void;
25+
}
26+
27+
export interface ReactLoggerConfig {
28+
logLevel: LogLevel;
29+
logHandler?: LogHandler;
30+
}
31+
32+
const LOG_PREFIX = '[OPTIMIZELY - REACT]';
33+
const defaultLogHandler: LogHandler = {
34+
log(level: LogLevel, message: string): void {
35+
switch (level) {
36+
case LogLevel.Debug:
37+
console.debug(message);
38+
break;
39+
case LogLevel.Info:
40+
console.info(message);
41+
break;
42+
case LogLevel.Warn:
43+
console.warn(message);
44+
break;
45+
case LogLevel.Error:
46+
console.error(message);
47+
break;
48+
}
49+
},
50+
};
51+
52+
export function createReactLogger(config: ReactLoggerConfig): ReactLogger {
53+
const handler = config.logHandler ?? defaultLogHandler;
54+
const level = config.logLevel;
55+
56+
return {
57+
debug: (msg) => {
58+
if (level <= LogLevel.Debug) handler.log(LogLevel.Debug, `${LOG_PREFIX} - DEBUG ${msg}`);
59+
},
60+
info: (msg) => {
61+
if (level <= LogLevel.Info) handler.log(LogLevel.Info, `${LOG_PREFIX} - INFO ${msg}`);
62+
},
63+
warn: (msg) => {
64+
if (level <= LogLevel.Warn) handler.log(LogLevel.Warn, `${LOG_PREFIX} - WARN ${msg}`);
65+
},
66+
error: (msg) => {
67+
if (level <= LogLevel.Error) handler.log(LogLevel.Error, `${LOG_PREFIX} - ERROR ${msg}`);
68+
},
69+
};
70+
}

src/logger/createLogger.spec.ts

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
/**
2+
* Copyright 2026, Optimizely
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import { vi, describe, it, expect, beforeEach } from 'vitest';
18+
import { DEBUG, INFO, WARN, ERROR, LogLevel } from '@optimizely/optimizely-sdk';
19+
import type { LogHandler } from '@optimizely/optimizely-sdk';
20+
import { createReactLogger } from './ReactLogger';
21+
import type { ReactLogger } from './ReactLogger';
22+
23+
const mockOpaqueLogger = vi.hoisted(() => ({ __opaque: true }));
24+
25+
vi.mock('@optimizely/optimizely-sdk', async (importOriginal) => {
26+
const original = await importOriginal<typeof import('@optimizely/optimizely-sdk')>();
27+
return {
28+
...original,
29+
createLogger: vi.fn().mockReturnValue(mockOpaqueLogger),
30+
};
31+
});
32+
33+
import { createLogger, REACT_LOGGER } from './createLogger';
34+
35+
describe('createLogger', () => {
36+
beforeEach(() => {
37+
vi.clearAllMocks();
38+
});
39+
40+
it('should return the opaque logger from the JS SDK', () => {
41+
const result = createLogger({ level: INFO });
42+
expect(result).toBe(mockOpaqueLogger);
43+
});
44+
45+
it('should attach a ReactLogger via the REACT_LOGGER symbol', () => {
46+
const mockHandler: LogHandler = { log: vi.fn() };
47+
const result = createLogger({ level: INFO, logHandler: mockHandler });
48+
49+
const reactLogger = (result as Record<symbol, unknown>)[REACT_LOGGER] as ReactLogger;
50+
expect(reactLogger).toBeDefined();
51+
expect(reactLogger.debug).toBeTypeOf('function');
52+
expect(reactLogger.info).toBeTypeOf('function');
53+
expect(reactLogger.warn).toBeTypeOf('function');
54+
expect(reactLogger.error).toBeTypeOf('function');
55+
});
56+
57+
it('should create a ReactLogger that uses the provided logHandler', () => {
58+
const mockHandler: LogHandler = { log: vi.fn() };
59+
const result = createLogger({ level: INFO, logHandler: mockHandler });
60+
61+
const reactLogger = (result as Record<symbol, unknown>)[REACT_LOGGER] as ReactLogger;
62+
reactLogger.info('hello');
63+
64+
expect(mockHandler.log).toHaveBeenCalledWith(LogLevel.Info, '[OPTIMIZELY - REACT] - INFO hello');
65+
});
66+
67+
describe('log level resolution', () => {
68+
it.each([
69+
{ preset: DEBUG, expectedCalls: 4, name: 'DEBUG' },
70+
{ preset: INFO, expectedCalls: 3, name: 'INFO' },
71+
{ preset: WARN, expectedCalls: 2, name: 'WARN' },
72+
{ preset: ERROR, expectedCalls: 1, name: 'ERROR' },
73+
])('should resolve $name preset correctly', ({ preset, expectedCalls }) => {
74+
const mockHandler: LogHandler = { log: vi.fn() };
75+
const result = createLogger({ level: preset, logHandler: mockHandler });
76+
77+
const reactLogger = (result as Record<symbol, unknown>)[REACT_LOGGER] as ReactLogger;
78+
reactLogger.debug('d');
79+
reactLogger.info('i');
80+
reactLogger.warn('w');
81+
reactLogger.error('e');
82+
expect(mockHandler.log).toHaveBeenCalledTimes(expectedCalls);
83+
});
84+
});
85+
});
86+
87+
describe('createReactLogger', () => {
88+
describe('log level filtering', () => {
89+
it('should filter messages below the configured level', () => {
90+
const mockHandler: LogHandler = { log: vi.fn() };
91+
const logger = createReactLogger({ logLevel: LogLevel.Warn, logHandler: mockHandler });
92+
93+
logger.debug('should not appear');
94+
logger.info('should not appear');
95+
logger.warn('should appear');
96+
logger.error('should appear');
97+
98+
expect(mockHandler.log).toHaveBeenCalledTimes(2);
99+
expect(mockHandler.log).toHaveBeenCalledWith(LogLevel.Warn, '[OPTIMIZELY - REACT] - WARN should appear');
100+
expect(mockHandler.log).toHaveBeenCalledWith(LogLevel.Error, '[OPTIMIZELY - REACT] - ERROR should appear');
101+
});
102+
103+
it('should allow all messages when level is Debug', () => {
104+
const mockHandler: LogHandler = { log: vi.fn() };
105+
const logger = createReactLogger({ logLevel: LogLevel.Debug, logHandler: mockHandler });
106+
107+
logger.debug('d');
108+
logger.info('i');
109+
logger.warn('w');
110+
logger.error('e');
111+
112+
expect(mockHandler.log).toHaveBeenCalledTimes(4);
113+
});
114+
115+
it('should only allow error messages when level is Error', () => {
116+
const mockHandler: LogHandler = { log: vi.fn() };
117+
const logger = createReactLogger({ logLevel: LogLevel.Error, logHandler: mockHandler });
118+
119+
logger.debug('d');
120+
logger.info('i');
121+
logger.warn('w');
122+
logger.error('e');
123+
124+
expect(mockHandler.log).toHaveBeenCalledTimes(1);
125+
expect(mockHandler.log).toHaveBeenCalledWith(LogLevel.Error, '[OPTIMIZELY - REACT] - ERROR e');
126+
});
127+
});
128+
129+
describe('log handler', () => {
130+
it('should use the provided logHandler', () => {
131+
const mockHandler: LogHandler = { log: vi.fn() };
132+
const logger = createReactLogger({ logLevel: LogLevel.Info, logHandler: mockHandler });
133+
134+
logger.info('hello');
135+
136+
expect(mockHandler.log).toHaveBeenCalledWith(LogLevel.Info, '[OPTIMIZELY - REACT] - INFO hello');
137+
});
138+
139+
it('should use default console handler when logHandler is not provided', () => {
140+
const consoleSpy = vi.spyOn(console, 'info').mockImplementation(() => {});
141+
const logger = createReactLogger({ logLevel: LogLevel.Info });
142+
143+
logger.info('hello');
144+
145+
expect(consoleSpy).toHaveBeenCalledWith('[OPTIMIZELY - REACT] - INFO hello');
146+
consoleSpy.mockRestore();
147+
});
148+
});
149+
150+
describe('message prefix', () => {
151+
it('should prepend [OPTIMIZELY - REACT] to all messages', () => {
152+
const mockHandler: LogHandler = { log: vi.fn() };
153+
const logger = createReactLogger({ logLevel: LogLevel.Debug, logHandler: mockHandler });
154+
155+
logger.debug('test');
156+
logger.info('test');
157+
logger.warn('test');
158+
logger.error('test');
159+
160+
for (const call of (mockHandler.log as ReturnType<typeof vi.fn>).mock.calls) {
161+
expect(call[1]).toMatch(/^\[OPTIMIZELY - REACT\] - (DEBUG|INFO|WARN|ERROR) /);
162+
}
163+
});
164+
});
165+
});

src/logger/createLogger.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/**
2+
* Copyright 2026, Optimizely
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import { createLogger as jsCreateLogger, LogLevel, DEBUG, INFO, WARN, ERROR } from '@optimizely/optimizely-sdk';
18+
import type { LoggerConfig, OpaqueLevelPreset } from '@optimizely/optimizely-sdk';
19+
import { createReactLogger } from './ReactLogger';
20+
21+
export const REACT_LOGGER = Symbol('react-logger');
22+
23+
function resolveLogLevel(preset: OpaqueLevelPreset): LogLevel {
24+
if (preset === DEBUG) return LogLevel.Debug;
25+
if (preset === INFO) return LogLevel.Info;
26+
if (preset === WARN) return LogLevel.Warn;
27+
if (preset === ERROR) return LogLevel.Error;
28+
return LogLevel.Error;
29+
}
30+
31+
export function createLogger(config: LoggerConfig) {
32+
const opaqueLogger = jsCreateLogger(config);
33+
const reactLogger = createReactLogger({
34+
logLevel: resolveLogLevel(config.level),
35+
logHandler: config.logHandler,
36+
});
37+
(opaqueLogger as Record<symbol, unknown>)[REACT_LOGGER] = reactLogger;
38+
return opaqueLogger;
39+
}

src/logger/getReactLogger.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/**
2+
* Copyright 2026, Optimizely
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import type { Client } from '@optimizely/optimizely-sdk';
18+
import { REACT_CLIENT_META } from '../client/createInstance';
19+
import type { ReactClientMeta } from '../client/createInstance';
20+
import type { ReactLogger } from './ReactLogger';
21+
22+
/**
23+
* Returns the ReactLogger instance for the given client, or undefined
24+
* if the client has no logger (e.g., logger was not created via the
25+
* React SDK's createLogger wrapper).
26+
*/
27+
export function getReactLogger(client: Client): ReactLogger | undefined {
28+
const meta = (client as unknown as Record<symbol, ReactClientMeta>)[REACT_CLIENT_META];
29+
return meta.logger;
30+
}

src/logger/index.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/**
2+
* Copyright 2026, Optimizely
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
export { createLogger } from './createLogger';
18+
export { getReactLogger } from './getReactLogger';
19+
export { createReactLogger } from './ReactLogger';
20+
export type { ReactLogger, ReactLoggerConfig } from './ReactLogger';
21+
export { ERROR, DEBUG, WARN, INFO } from '@optimizely/optimizely-sdk';

0 commit comments

Comments
 (0)