Skip to content

Commit cca6db3

Browse files
committed
Merge branch 'main' into rlamb/update-compat-exports
2 parents 9b7e53f + 21670c4 commit cca6db3

File tree

7 files changed

+215
-49
lines changed

7 files changed

+215
-49
lines changed

packages/sdk/browser/src/BrowserClient.ts

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import {
22
AutoEnvAttributes,
33
base64UrlEncode,
4+
BasicLogger,
45
LDClient as CommonClient,
56
Configuration,
6-
createSafeLogger,
77
Encoding,
88
FlagManager,
99
internal,
@@ -98,15 +98,18 @@ export class BrowserClient extends LDClientImpl implements LDClient {
9898
// Overrides the default logger from the common implementation.
9999
const logger =
100100
customLogger ??
101-
createSafeLogger({
102-
// eslint-disable-next-line no-console
103-
debug: debug ? console.debug : () => {},
104-
// eslint-disable-next-line no-console
105-
info: console.info,
106-
// eslint-disable-next-line no-console
107-
warn: console.warn,
108-
// eslint-disable-next-line no-console
109-
error: console.error,
101+
new BasicLogger({
102+
destination: {
103+
// eslint-disable-next-line no-console
104+
debug: console.debug,
105+
// eslint-disable-next-line no-console
106+
info: console.info,
107+
// eslint-disable-next-line no-console
108+
warn: console.warn,
109+
// eslint-disable-next-line no-console
110+
error: console.error,
111+
},
112+
level: debug ? 'debug' : 'info',
110113
});
111114

112115
// TODO: Use the already-configured baseUri from the SDK config. SDK-560

packages/sdk/browser/src/index.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
*/
1313
import {
1414
AutoEnvAttributes,
15+
BasicLogger,
16+
BasicLoggerOptions,
1517
EvaluationSeriesContext,
1618
EvaluationSeriesData,
1719
Hook,
@@ -84,3 +86,54 @@ export function initialize(clientSideId: string, options?: LDOptions): LDClient
8486
// AutoEnvAttributes are not supported yet in the browser SDK.
8587
return new BrowserClient(clientSideId, AutoEnvAttributes.Disabled, options);
8688
}
89+
90+
/**
91+
* Provides a simple {@link LDLogger} implementation.
92+
*
93+
* This logging implementation uses a simple format that includes only the log level
94+
* and the message text. By default the output is written to `console.error`.
95+
*
96+
* To use the logger created by this function, put it into {@link LDOptions.logger}. If
97+
* you do not set {@link LDOptions.logger} to anything, the SDK uses a default logger
98+
* that will log "info" level and higher priorty messages and it will log messages to
99+
* console.info, console.warn, and console.error.
100+
*
101+
* @param options Configuration for the logger. If no options are specified, the
102+
* logger uses `{ level: 'info' }`.
103+
*
104+
* @example
105+
* This example shows how to use `basicLogger` in your SDK options to enable console
106+
* logging only at `warn` and `error` levels.
107+
* ```javascript
108+
* const ldOptions = {
109+
* logger: basicLogger({ level: 'warn' }),
110+
* };
111+
* ```
112+
*
113+
* @example
114+
* This example shows how to use `basicLogger` in your SDK options to cause all
115+
* log output to go to `console.log`
116+
* ```javascript
117+
* const ldOptions = {
118+
* logger: basicLogger({ destination: console.log }),
119+
* };
120+
* ```
121+
*
122+
* * @example
123+
* The configuration also allows you to control the destination for each log level.
124+
* ```javascript
125+
* const ldOptions = {
126+
* logger: basicLogger({
127+
* destination: {
128+
* debug: console.debug,
129+
* info: console.info,
130+
* warn: console.warn,
131+
* error:console.error
132+
* }
133+
* }),
134+
* };
135+
* ```
136+
*/
137+
export function basicLogger(options: BasicLoggerOptions): LDLogger {
138+
return new BasicLogger(options);
139+
}

packages/shared/common/__tests__/logging/BasicLogger.test.ts

Lines changed: 103 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@ import { BasicLogger, LDLogLevel } from '../../src';
22

33
const spy = jest.spyOn(console, 'error').mockImplementation(() => {});
44

5+
beforeEach(() => {
6+
jest.clearAllMocks();
7+
});
8+
59
describe.each<[LDLogLevel, string[]]>([
610
[
711
'debug',
@@ -64,10 +68,6 @@ describe('given a logger with a custom name', () => {
6468
describe('given a default logger', () => {
6569
const logger = new BasicLogger({});
6670

67-
beforeEach(() => {
68-
jest.clearAllMocks();
69-
});
70-
7171
it('logs to the console', () => {
7272
logger.warn('potato', 'bacon');
7373
expect(spy).toHaveBeenCalledWith('potato', 'bacon');
@@ -81,10 +81,6 @@ describe('given a logger with a destination that throws', () => {
8181
},
8282
});
8383

84-
beforeEach(() => {
85-
jest.clearAllMocks();
86-
});
87-
8884
it('logs to the console instead of throwing', () => {
8985
logger.error('a');
9086
expect(spy).toHaveBeenCalledWith('error: [LaunchDarkly] a');
@@ -94,10 +90,6 @@ describe('given a logger with a destination that throws', () => {
9490
describe('given a logger with a formatter that throws', () => {
9591
const strings: string[] = [];
9692

97-
beforeEach(() => {
98-
jest.clearAllMocks();
99-
});
100-
10193
const logger = new BasicLogger({
10294
destination: (...args: any) => {
10395
strings.push(args.join(' '));
@@ -112,3 +104,102 @@ describe('given a logger with a formatter that throws', () => {
112104
expect(spy).toHaveBeenCalledTimes(0);
113105
});
114106
});
107+
108+
it('dispatches logs correctly with multiple destinations', () => {
109+
const debug = jest.fn();
110+
const info = jest.fn();
111+
const warn = jest.fn();
112+
const error = jest.fn();
113+
114+
const logger = new BasicLogger({
115+
destination: {
116+
debug,
117+
info,
118+
warn,
119+
error,
120+
},
121+
level: 'debug',
122+
});
123+
124+
logger.debug('toDebug');
125+
logger.info('toInfo');
126+
logger.warn('toWarn');
127+
logger.error('toError');
128+
129+
expect(debug).toHaveBeenCalledTimes(1);
130+
expect(debug).toHaveBeenCalledWith('debug: [LaunchDarkly] toDebug');
131+
132+
expect(info).toHaveBeenCalledTimes(1);
133+
expect(info).toHaveBeenCalledWith('info: [LaunchDarkly] toInfo');
134+
135+
expect(warn).toHaveBeenCalledTimes(1);
136+
expect(warn).toHaveBeenCalledWith('warn: [LaunchDarkly] toWarn');
137+
138+
expect(error).toHaveBeenCalledTimes(1);
139+
expect(error).toHaveBeenCalledWith('error: [LaunchDarkly] toError');
140+
});
141+
142+
it('handles destinations which throw', () => {
143+
const debug = jest.fn(() => {
144+
throw new Error('bad');
145+
});
146+
const info = jest.fn(() => {
147+
throw new Error('bad');
148+
});
149+
const warn = jest.fn(() => {
150+
throw new Error('bad');
151+
});
152+
const error = jest.fn(() => {
153+
throw new Error('bad');
154+
});
155+
156+
const logger = new BasicLogger({
157+
destination: {
158+
debug,
159+
info,
160+
warn,
161+
error,
162+
},
163+
level: 'debug',
164+
});
165+
166+
logger.debug('toDebug');
167+
logger.info('toInfo');
168+
logger.warn('toWarn');
169+
logger.error('toError');
170+
171+
expect(spy).toHaveBeenCalledTimes(4);
172+
expect(spy).toHaveBeenCalledWith('debug: [LaunchDarkly] toDebug');
173+
expect(spy).toHaveBeenCalledWith('info: [LaunchDarkly] toInfo');
174+
expect(spy).toHaveBeenCalledWith('warn: [LaunchDarkly] toWarn');
175+
expect(spy).toHaveBeenCalledWith('error: [LaunchDarkly] toError');
176+
});
177+
178+
it('handles destinations which are not defined', () => {
179+
const debug = jest.fn();
180+
const info = jest.fn();
181+
const logger = new BasicLogger({
182+
// @ts-ignore
183+
destination: {
184+
debug,
185+
info,
186+
},
187+
level: 'debug',
188+
});
189+
190+
logger.debug('toDebug');
191+
logger.info('toInfo');
192+
logger.warn('toWarn');
193+
logger.error('toError');
194+
195+
expect(debug).toHaveBeenCalledTimes(1);
196+
expect(debug).toHaveBeenCalledWith('debug: [LaunchDarkly] toDebug');
197+
198+
expect(info).toHaveBeenCalledTimes(1);
199+
expect(info).toHaveBeenCalledWith('info: [LaunchDarkly] toInfo');
200+
201+
expect(spy).toHaveBeenCalledTimes(2);
202+
203+
expect(spy).toHaveBeenCalledWith('toWarn');
204+
expect(spy).toHaveBeenCalledWith('toError');
205+
});

packages/shared/common/src/api/logging/BasicLoggerOptions.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,18 +21,23 @@ export interface BasicLoggerOptions {
2121
name?: string;
2222

2323
/**
24-
* An optional function to use to print each log line.
24+
* An optional function, or map of levels to functions, to use to print each log line.
2525
*
26-
* If this is specified, `basicLogger` calls it to write each line of output. The
26+
* If not specified, the default is `console.error`.
27+
*
28+
* If a function is specified, `basicLogger` calls it to write each line of output. The
2729
* argument is a fully formatted log line, not including a linefeed. The function
2830
* is only called for log levels that are enabled.
2931
*
30-
* If not specified, the default is `console.error`.
32+
* If a map is specified, then each entry will be used as the destination for the corresponding
33+
* log level. Any level that is not specified will use the default of `console.error`.
3134
*
3235
* Setting this property to anything other than a function will cause SDK
3336
* initialization to fail.
3437
*/
35-
destination?: (line: string) => void;
38+
destination?:
39+
| ((line: string) => void)
40+
| Record<'debug' | 'info' | 'warn' | 'error', (line: string) => void>;
3641

3742
/**
3843
* An optional formatter to use. The formatter should be compatible

packages/shared/common/src/logging/BasicLogger.ts

Lines changed: 32 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
1-
import { BasicLoggerOptions, LDLogger } from '../api';
1+
import { BasicLoggerOptions, LDLogger, LDLogLevel } from '../api';
22
import format from './format';
33

4-
const LogPriority = {
5-
debug: 0,
6-
info: 1,
7-
warn: 2,
8-
error: 3,
9-
none: 4,
10-
};
4+
enum LogPriority {
5+
debug = 0,
6+
info = 1,
7+
warn = 2,
8+
error = 3,
9+
none = 4,
10+
}
1111

12-
const LevelNames = ['debug', 'info', 'warn', 'error', 'none'];
12+
const LEVEL_NAMES: LDLogLevel[] = ['debug', 'info', 'warn', 'error', 'none'];
1313

1414
/**
1515
* A basic logger which handles filtering by level.
@@ -27,7 +27,7 @@ export default class BasicLogger implements LDLogger {
2727

2828
private _name: string;
2929

30-
private _destination?: (line: string) => void;
30+
private _destinations?: Record<number, (line: string) => void>;
3131

3232
private _formatter?: (...args: any[]) => string;
3333

@@ -43,9 +43,23 @@ export default class BasicLogger implements LDLogger {
4343
constructor(options: BasicLoggerOptions) {
4444
this._logLevel = LogPriority[options.level ?? 'info'] ?? LogPriority.info;
4545
this._name = options.name ?? 'LaunchDarkly';
46-
// eslint-disable-next-line no-console
47-
this._destination = options.destination;
4846
this._formatter = options.formatter;
47+
if (typeof options.destination === 'object') {
48+
this._destinations = {
49+
[LogPriority.debug]: options.destination.debug,
50+
[LogPriority.info]: options.destination.info,
51+
[LogPriority.warn]: options.destination.warn,
52+
[LogPriority.error]: options.destination.error,
53+
};
54+
} else if (typeof options.destination === 'function') {
55+
const { destination } = options;
56+
this._destinations = {
57+
[LogPriority.debug]: destination,
58+
[LogPriority.info]: destination,
59+
[LogPriority.warn]: destination,
60+
[LogPriority.error]: destination,
61+
};
62+
}
4963
}
5064

5165
private _tryFormat(...args: any[]): string {
@@ -60,9 +74,9 @@ export default class BasicLogger implements LDLogger {
6074
}
6175
}
6276

63-
private _tryWrite(msg: string) {
77+
private _tryWrite(destination: (msg: string) => void, msg: string) {
6478
try {
65-
this._destination!(msg);
79+
destination(msg);
6680
} catch {
6781
// eslint-disable-next-line no-console
6882
console.error(msg);
@@ -71,10 +85,11 @@ export default class BasicLogger implements LDLogger {
7185

7286
private _log(level: number, args: any[]) {
7387
if (level >= this._logLevel) {
74-
const prefix = `${LevelNames[level]}: [${this._name}]`;
88+
const prefix = `${LEVEL_NAMES[level]}: [${this._name}]`;
7589
try {
76-
if (this._destination) {
77-
this._tryWrite(`${prefix} ${this._tryFormat(...args)}`);
90+
const destination = this._destinations?.[level];
91+
if (destination) {
92+
this._tryWrite(destination, `${prefix} ${this._tryFormat(...args)}`);
7893
} else {
7994
// `console.error` has its own formatter.
8095
// So we don't need to do anything.

packages/shared/sdk-client/__tests__/configuration/Configuration.test.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,7 @@ describe('Configuration', () => {
2222
withReasons: false,
2323
eventsUri: 'https://events.launchdarkly.com',
2424
flushInterval: 30,
25-
logger: {
26-
_destination: console.error,
27-
_logLevel: 1,
28-
_name: 'LaunchDarkly',
29-
},
25+
logger: expect.anything(),
3026
maxCachedContexts: 5,
3127
privateAttributes: [],
3228
sendEvents: true,

packages/shared/sdk-client/src/api/LDOptions.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,10 @@ export interface LDOptions {
126126
* @remarks
127127
* Set a custom {@link LDLogger} if you want full control of logging behavior.
128128
*
129-
* @defaultValue A {@link BasicLogger} which outputs to the console at `info` level.
129+
* @defaultValue The default logging implementation will varybased on platform. For the browser
130+
* the default logger will log "info" level and higher priorty messages and it will log messages to
131+
* console.info, console.warn, and console.error. Other platforms may use a `BasicLogger` instance
132+
* also defaulted to the "info" level.
130133
*/
131134
logger?: LDLogger;
132135

0 commit comments

Comments
 (0)