Skip to content

Commit 2ef1486

Browse files
authored
feat: Add basic logging support for browser-telemetry. (#736)
Adds the ability to specify a logger and to also use the base SDK logger when possible. The base SDK of a 3.x version does not expose the logger, but 4.x will. There is a minimal replication of the warning level of the logging interface to allow compatibility with both the 3.x SDK and the 4.x SDK. Prefixing is done at message time instead of as part of the logger. Potentially it could be ideal to do it in the logger, but this approach makes it clear that the interpolation supported by most browser loggers will not come into play. (Where if the logger does this prefixing it is either complex or you lose the sprintf style formatting support.) Some earlier log messages has been added, but a logger instance was not yet available.
1 parent 5c327a1 commit 2ef1486

File tree

12 files changed

+385
-37
lines changed

12 files changed

+385
-37
lines changed

packages/telemetry/browser-telemetry/__tests__/BrowserTelemetryImpl.test.ts

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { LDClientLogging } from '../src/api';
12
import { LDClientTracking } from '../src/api/client/LDClientTracking';
23
import BrowserTelemetryImpl from '../src/BrowserTelemetryImpl';
34
import { ParsedOptions } from '../src/options';
@@ -210,6 +211,30 @@ it('unregisters collectors on close', () => {
210211
expect(mockCollector.unregister).toHaveBeenCalled();
211212
});
212213

214+
it('logs event dropped message when maxPendingEvents is reached', () => {
215+
const mockLogger = {
216+
warn: jest.fn(),
217+
};
218+
const telemetry = new BrowserTelemetryImpl({
219+
...defaultOptions,
220+
maxPendingEvents: 2,
221+
logger: mockLogger,
222+
});
223+
telemetry.captureError(new Error('Test error'));
224+
expect(mockLogger.warn).not.toHaveBeenCalled();
225+
telemetry.captureError(new Error('Test error 2'));
226+
expect(mockLogger.warn).not.toHaveBeenCalled();
227+
228+
telemetry.captureError(new Error('Test error 3'));
229+
expect(mockLogger.warn).toHaveBeenCalledWith(
230+
'LaunchDarkly - Browser Telemetry: Maximum pending events reached. Old events will be dropped until the SDK' +
231+
' client is registered.',
232+
);
233+
234+
telemetry.captureError(new Error('Test error 4'));
235+
expect(mockLogger.warn).toHaveBeenCalledTimes(1);
236+
});
237+
213238
it('filters breadcrumbs using provided filters', () => {
214239
const options: ParsedOptions = {
215240
...defaultOptions,
@@ -359,3 +384,141 @@ it('omits breadcrumbs when a filter is not a function', () => {
359384
}),
360385
);
361386
});
387+
388+
it('warns when a breadcrumb filter is not a function', () => {
389+
const mockLogger = {
390+
warn: jest.fn(),
391+
};
392+
const options: ParsedOptions = {
393+
...defaultOptions,
394+
// @ts-ignore
395+
breadcrumbs: { ...defaultOptions.breadcrumbs, filters: ['potato'] },
396+
logger: mockLogger,
397+
};
398+
399+
const telemetry = new BrowserTelemetryImpl(options);
400+
telemetry.addBreadcrumb({
401+
type: 'custom',
402+
data: { id: 1 },
403+
timestamp: Date.now(),
404+
class: 'custom',
405+
level: 'info',
406+
});
407+
408+
expect(mockLogger.warn).toHaveBeenCalledWith(
409+
'LaunchDarkly - Browser Telemetry: Error applying breadcrumb filters: TypeError: filter is not a function',
410+
);
411+
});
412+
413+
it('warns when a breadcrumb filter throws an exception', () => {
414+
const mockLogger = {
415+
warn: jest.fn(),
416+
};
417+
const options: ParsedOptions = {
418+
...defaultOptions,
419+
breadcrumbs: {
420+
...defaultOptions.breadcrumbs,
421+
filters: [
422+
() => {
423+
throw new Error('Filter error');
424+
},
425+
],
426+
},
427+
logger: mockLogger,
428+
};
429+
430+
const telemetry = new BrowserTelemetryImpl(options);
431+
telemetry.addBreadcrumb({
432+
type: 'custom',
433+
data: { id: 1 },
434+
timestamp: Date.now(),
435+
class: 'custom',
436+
level: 'info',
437+
});
438+
439+
expect(mockLogger.warn).toHaveBeenCalledWith(
440+
'LaunchDarkly - Browser Telemetry: Error applying breadcrumb filters: Error: Filter error',
441+
);
442+
});
443+
444+
it('only logs breadcrumb filter error once', () => {
445+
const mockLogger = {
446+
warn: jest.fn(),
447+
};
448+
const options: ParsedOptions = {
449+
...defaultOptions,
450+
breadcrumbs: {
451+
...defaultOptions.breadcrumbs,
452+
filters: [
453+
() => {
454+
throw new Error('Filter error');
455+
},
456+
],
457+
},
458+
logger: mockLogger,
459+
};
460+
461+
const telemetry = new BrowserTelemetryImpl(options);
462+
463+
// Add multiple breadcrumbs that will trigger the filter error
464+
telemetry.addBreadcrumb({
465+
type: 'custom',
466+
data: { id: 1 },
467+
timestamp: Date.now(),
468+
class: 'custom',
469+
level: 'info',
470+
});
471+
472+
telemetry.addBreadcrumb({
473+
type: 'custom',
474+
data: { id: 2 },
475+
timestamp: Date.now(),
476+
class: 'custom',
477+
level: 'info',
478+
});
479+
480+
// Verify warning was only logged once
481+
expect(mockLogger.warn).toHaveBeenCalledTimes(1);
482+
expect(mockLogger.warn).toHaveBeenCalledWith(
483+
'LaunchDarkly - Browser Telemetry: Error applying breadcrumb filters: Error: Filter error',
484+
);
485+
});
486+
487+
it('uses the client logger when no logger is provided', () => {
488+
const options: ParsedOptions = {
489+
...defaultOptions,
490+
breadcrumbs: {
491+
...defaultOptions.breadcrumbs,
492+
filters: [
493+
() => {
494+
throw new Error('Filter error');
495+
},
496+
],
497+
},
498+
};
499+
500+
const telemetry = new BrowserTelemetryImpl(options);
501+
502+
const mockClientWithLogging: jest.Mocked<LDClientLogging & LDClientTracking> = {
503+
logger: {
504+
warn: jest.fn(),
505+
},
506+
track: jest.fn(),
507+
};
508+
509+
telemetry.register(mockClientWithLogging);
510+
511+
// Add multiple breadcrumbs that will trigger the filter error
512+
telemetry.addBreadcrumb({
513+
type: 'custom',
514+
data: { id: 1 },
515+
timestamp: Date.now(),
516+
class: 'custom',
517+
level: 'info',
518+
});
519+
520+
expect(mockClientWithLogging.logger.warn).toHaveBeenCalledTimes(1);
521+
expect(mockClientWithLogging.logger.warn).toHaveBeenCalledWith(
522+
'LaunchDarkly - Browser Telemetry: Error applying breadcrumb filters: Error: Filter error',
523+
);
524+
});
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { MinLogger } from '../src/api';
2+
import { fallbackLogger, prefixLog, safeMinLogger } from '../src/logging';
3+
4+
afterEach(() => {
5+
jest.resetAllMocks();
6+
});
7+
8+
it('prefixes the message with the telemetry prefix', () => {
9+
const message = 'test message';
10+
const prefixed = prefixLog(message);
11+
expect(prefixed).toBe('LaunchDarkly - Browser Telemetry: test message');
12+
});
13+
14+
it('uses fallback logger when no logger provided', () => {
15+
const spy = jest.spyOn(fallbackLogger, 'warn');
16+
const logger = safeMinLogger(undefined);
17+
18+
logger.warn('test message');
19+
20+
expect(spy).toHaveBeenCalledWith('test message');
21+
spy.mockRestore();
22+
});
23+
24+
it('uses provided logger when it works correctly', () => {
25+
const mockWarn = jest.fn();
26+
const testLogger: MinLogger = {
27+
warn: mockWarn,
28+
};
29+
30+
const logger = safeMinLogger(testLogger);
31+
logger.warn('test message');
32+
33+
expect(mockWarn).toHaveBeenCalledWith('test message');
34+
});
35+
36+
it('falls back to fallback logger when provided logger throws', () => {
37+
const spy = jest.spyOn(fallbackLogger, 'warn');
38+
const testLogger: MinLogger = {
39+
warn: () => {
40+
throw new Error('logger error');
41+
},
42+
};
43+
44+
const logger = safeMinLogger(testLogger);
45+
logger.warn('test message');
46+
47+
expect(spy).toHaveBeenCalledWith('test message');
48+
expect(spy).toHaveBeenCalledWith(
49+
'LaunchDarkly - Browser Telemetry: The provided logger threw an exception, using fallback logger.',
50+
);
51+
spy.mockRestore();
52+
});

packages/telemetry/browser-telemetry/__tests__/options.test.ts

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ it('warns when maxPendingEvents is not a number', () => {
6363

6464
expect(outOptions.maxPendingEvents).toEqual(defaultOptions().maxPendingEvents);
6565
expect(mockLogger.warn).toHaveBeenCalledWith(
66-
'Config option "maxPendingEvents" should be of type number, got string, using default value',
66+
'LaunchDarkly - Browser Telemetry: Config option "maxPendingEvents" should be of type number, got string, using default value',
6767
);
6868
});
6969

@@ -90,7 +90,7 @@ it('warns when breadcrumbs config is not an object', () => {
9090

9191
expect(outOptions.breadcrumbs).toEqual(defaultOptions().breadcrumbs);
9292
expect(mockLogger.warn).toHaveBeenCalledWith(
93-
'Config option "breadcrumbs" should be of type object, got string, using default value',
93+
'LaunchDarkly - Browser Telemetry: Config option "breadcrumbs" should be of type object, got string, using default value',
9494
);
9595
});
9696

@@ -105,7 +105,7 @@ it('warns when collectors is not an array', () => {
105105

106106
expect(outOptions.collectors).toEqual(defaultOptions().collectors);
107107
expect(mockLogger.warn).toHaveBeenCalledWith(
108-
'Config option "collectors" should be of type Collector[], got string, using default value',
108+
'LaunchDarkly - Browser Telemetry: Config option "collectors" should be of type Collector[], got string, using default value',
109109
);
110110
});
111111

@@ -133,7 +133,7 @@ it('warns when stack config is not an object', () => {
133133

134134
expect(outOptions.stack).toEqual(defaultOptions().stack);
135135
expect(mockLogger.warn).toHaveBeenCalledWith(
136-
'Config option "stack" should be of type object, got string, using default value',
136+
'LaunchDarkly - Browser Telemetry: Config option "stack" should be of type object, got string, using default value',
137137
);
138138
});
139139

@@ -152,7 +152,7 @@ it('warns when breadcrumbs.maxBreadcrumbs is not a number', () => {
152152
defaultOptions().breadcrumbs.maxBreadcrumbs,
153153
);
154154
expect(mockLogger.warn).toHaveBeenCalledWith(
155-
'Config option "breadcrumbs.maxBreadcrumbs" should be of type number, got string, using default value',
155+
'LaunchDarkly - Browser Telemetry: Config option "breadcrumbs.maxBreadcrumbs" should be of type number, got string, using default value',
156156
);
157157
});
158158

@@ -183,7 +183,7 @@ it('warns when breadcrumbs.click is not boolean', () => {
183183

184184
expect(outOptions.breadcrumbs.click).toEqual(defaultOptions().breadcrumbs.click);
185185
expect(mockLogger.warn).toHaveBeenCalledWith(
186-
'Config option "breadcrumbs.click" should be of type boolean, got string, using default value',
186+
'LaunchDarkly - Browser Telemetry: Config option "breadcrumbs.click" should be of type boolean, got string, using default value',
187187
);
188188
});
189189

@@ -200,7 +200,7 @@ it('warns when breadcrumbs.evaluations is not boolean', () => {
200200

201201
expect(outOptions.breadcrumbs.evaluations).toEqual(defaultOptions().breadcrumbs.evaluations);
202202
expect(mockLogger.warn).toHaveBeenCalledWith(
203-
'Config option "breadcrumbs.evaluations" should be of type boolean, got string, using default value',
203+
'LaunchDarkly - Browser Telemetry: Config option "breadcrumbs.evaluations" should be of type boolean, got string, using default value',
204204
);
205205
});
206206

@@ -217,7 +217,7 @@ it('warns when breadcrumbs.flagChange is not boolean', () => {
217217

218218
expect(outOptions.breadcrumbs.flagChange).toEqual(defaultOptions().breadcrumbs.flagChange);
219219
expect(mockLogger.warn).toHaveBeenCalledWith(
220-
'Config option "breadcrumbs.flagChange" should be of type boolean, got string, using default value',
220+
'LaunchDarkly - Browser Telemetry: Config option "breadcrumbs.flagChange" should be of type boolean, got string, using default value',
221221
);
222222
});
223223

@@ -234,7 +234,7 @@ it('warns when breadcrumbs.keyboardInput is not boolean', () => {
234234

235235
expect(outOptions.breadcrumbs.keyboardInput).toEqual(defaultOptions().breadcrumbs.keyboardInput);
236236
expect(mockLogger.warn).toHaveBeenCalledWith(
237-
'Config option "breadcrumbs.keyboardInput" should be of type boolean, got string, using default value',
237+
'LaunchDarkly - Browser Telemetry: Config option "breadcrumbs.keyboardInput" should be of type boolean, got string, using default value',
238238
);
239239
});
240240

@@ -307,7 +307,7 @@ it('warns when breadcrumbs.http is not an object', () => {
307307

308308
expect(outOptions.breadcrumbs.http).toEqual(defaultOptions().breadcrumbs.http);
309309
expect(mockLogger.warn).toHaveBeenCalledWith(
310-
'Config option "breadcrumbs.http" should be of type HttpBreadCrumbOptions | false, got string, using default value',
310+
'LaunchDarkly - Browser Telemetry: Config option "breadcrumbs.http" should be of type HttpBreadCrumbOptions | false, got string, using default value',
311311
);
312312
});
313313

@@ -328,7 +328,7 @@ it('warns when breadcrumbs.http.instrumentFetch is not boolean', () => {
328328
defaultOptions().breadcrumbs.http.instrumentFetch,
329329
);
330330
expect(mockLogger.warn).toHaveBeenCalledWith(
331-
'Config option "breadcrumbs.http.instrumentFetch" should be of type boolean, got string, using default value',
331+
'LaunchDarkly - Browser Telemetry: Config option "breadcrumbs.http.instrumentFetch" should be of type boolean, got string, using default value',
332332
);
333333
});
334334

@@ -349,7 +349,7 @@ it('warns when breadcrumbs.http.instrumentXhr is not boolean', () => {
349349
defaultOptions().breadcrumbs.http.instrumentXhr,
350350
);
351351
expect(mockLogger.warn).toHaveBeenCalledWith(
352-
'Config option "breadcrumbs.http.instrumentXhr" should be of type boolean, got string, using default value',
352+
'LaunchDarkly - Browser Telemetry: Config option "breadcrumbs.http.instrumentXhr" should be of type boolean, got string, using default value',
353353
);
354354
});
355355

@@ -419,7 +419,7 @@ it('warns when breadcrumbs.http.customUrlFilter is not a function', () => {
419419

420420
expect(outOptions.breadcrumbs.http.customUrlFilter).toBeUndefined();
421421
expect(mockLogger.warn).toHaveBeenCalledWith(
422-
'The "breadcrumbs.http.customUrlFilter" must be a function. Received string',
422+
'LaunchDarkly - Browser Telemetry: The "breadcrumbs.http.customUrlFilter" must be a function. Received string',
423423
);
424424
});
425425

@@ -435,6 +435,6 @@ it('warns when filters is not an array', () => {
435435
);
436436
expect(outOptions.breadcrumbs.filters).toEqual([]);
437437
expect(mockLogger.warn).toHaveBeenCalledWith(
438-
'Config option "breadcrumbs.filters" should be of type array, got string, using default value',
438+
'LaunchDarkly - Browser Telemetry: Config option "breadcrumbs.filters" should be of type array, got string, using default value',
439439
);
440440
});

0 commit comments

Comments
 (0)