diff --git a/packages/telemetry/browser-telemetry/__tests__/BrowserTelemetryImpl.test.ts b/packages/telemetry/browser-telemetry/__tests__/BrowserTelemetryImpl.test.ts index bb27da82c..223be5bb6 100644 --- a/packages/telemetry/browser-telemetry/__tests__/BrowserTelemetryImpl.test.ts +++ b/packages/telemetry/browser-telemetry/__tests__/BrowserTelemetryImpl.test.ts @@ -1,3 +1,4 @@ +import { LDClientLogging } from '../src/api'; import { LDClientTracking } from '../src/api/client/LDClientTracking'; import BrowserTelemetryImpl from '../src/BrowserTelemetryImpl'; import { ParsedOptions } from '../src/options'; @@ -210,6 +211,30 @@ it('unregisters collectors on close', () => { expect(mockCollector.unregister).toHaveBeenCalled(); }); +it('logs event dropped message when maxPendingEvents is reached', () => { + const mockLogger = { + warn: jest.fn(), + }; + const telemetry = new BrowserTelemetryImpl({ + ...defaultOptions, + maxPendingEvents: 2, + logger: mockLogger, + }); + telemetry.captureError(new Error('Test error')); + expect(mockLogger.warn).not.toHaveBeenCalled(); + telemetry.captureError(new Error('Test error 2')); + expect(mockLogger.warn).not.toHaveBeenCalled(); + + telemetry.captureError(new Error('Test error 3')); + expect(mockLogger.warn).toHaveBeenCalledWith( + 'LaunchDarkly - Browser Telemetry: Maximum pending events reached. Old events will be dropped until the SDK' + + ' client is registered.', + ); + + telemetry.captureError(new Error('Test error 4')); + expect(mockLogger.warn).toHaveBeenCalledTimes(1); +}); + it('filters breadcrumbs using provided filters', () => { const options: ParsedOptions = { ...defaultOptions, @@ -359,3 +384,141 @@ it('omits breadcrumbs when a filter is not a function', () => { }), ); }); + +it('warns when a breadcrumb filter is not a function', () => { + const mockLogger = { + warn: jest.fn(), + }; + const options: ParsedOptions = { + ...defaultOptions, + // @ts-ignore + breadcrumbs: { ...defaultOptions.breadcrumbs, filters: ['potato'] }, + logger: mockLogger, + }; + + const telemetry = new BrowserTelemetryImpl(options); + telemetry.addBreadcrumb({ + type: 'custom', + data: { id: 1 }, + timestamp: Date.now(), + class: 'custom', + level: 'info', + }); + + expect(mockLogger.warn).toHaveBeenCalledWith( + 'LaunchDarkly - Browser Telemetry: Error applying breadcrumb filters: TypeError: filter is not a function', + ); +}); + +it('warns when a breadcrumb filter throws an exception', () => { + const mockLogger = { + warn: jest.fn(), + }; + const options: ParsedOptions = { + ...defaultOptions, + breadcrumbs: { + ...defaultOptions.breadcrumbs, + filters: [ + () => { + throw new Error('Filter error'); + }, + ], + }, + logger: mockLogger, + }; + + const telemetry = new BrowserTelemetryImpl(options); + telemetry.addBreadcrumb({ + type: 'custom', + data: { id: 1 }, + timestamp: Date.now(), + class: 'custom', + level: 'info', + }); + + expect(mockLogger.warn).toHaveBeenCalledWith( + 'LaunchDarkly - Browser Telemetry: Error applying breadcrumb filters: Error: Filter error', + ); +}); + +it('only logs breadcrumb filter error once', () => { + const mockLogger = { + warn: jest.fn(), + }; + const options: ParsedOptions = { + ...defaultOptions, + breadcrumbs: { + ...defaultOptions.breadcrumbs, + filters: [ + () => { + throw new Error('Filter error'); + }, + ], + }, + logger: mockLogger, + }; + + const telemetry = new BrowserTelemetryImpl(options); + + // Add multiple breadcrumbs that will trigger the filter error + telemetry.addBreadcrumb({ + type: 'custom', + data: { id: 1 }, + timestamp: Date.now(), + class: 'custom', + level: 'info', + }); + + telemetry.addBreadcrumb({ + type: 'custom', + data: { id: 2 }, + timestamp: Date.now(), + class: 'custom', + level: 'info', + }); + + // Verify warning was only logged once + expect(mockLogger.warn).toHaveBeenCalledTimes(1); + expect(mockLogger.warn).toHaveBeenCalledWith( + 'LaunchDarkly - Browser Telemetry: Error applying breadcrumb filters: Error: Filter error', + ); +}); + +it('uses the client logger when no logger is provided', () => { + const options: ParsedOptions = { + ...defaultOptions, + breadcrumbs: { + ...defaultOptions.breadcrumbs, + filters: [ + () => { + throw new Error('Filter error'); + }, + ], + }, + }; + + const telemetry = new BrowserTelemetryImpl(options); + + const mockClientWithLogging: jest.Mocked = { + logger: { + warn: jest.fn(), + }, + track: jest.fn(), + }; + + telemetry.register(mockClientWithLogging); + + // Add multiple breadcrumbs that will trigger the filter error + telemetry.addBreadcrumb({ + type: 'custom', + data: { id: 1 }, + timestamp: Date.now(), + class: 'custom', + level: 'info', + }); + + expect(mockClientWithLogging.logger.warn).toHaveBeenCalledTimes(1); + expect(mockClientWithLogging.logger.warn).toHaveBeenCalledWith( + 'LaunchDarkly - Browser Telemetry: Error applying breadcrumb filters: Error: Filter error', + ); +}); diff --git a/packages/telemetry/browser-telemetry/__tests__/logging.test.ts b/packages/telemetry/browser-telemetry/__tests__/logging.test.ts new file mode 100644 index 000000000..452c821f9 --- /dev/null +++ b/packages/telemetry/browser-telemetry/__tests__/logging.test.ts @@ -0,0 +1,52 @@ +import { MinLogger } from '../src/api'; +import { fallbackLogger, prefixLog, safeMinLogger } from '../src/logging'; + +afterEach(() => { + jest.resetAllMocks(); +}); + +it('prefixes the message with the telemetry prefix', () => { + const message = 'test message'; + const prefixed = prefixLog(message); + expect(prefixed).toBe('LaunchDarkly - Browser Telemetry: test message'); +}); + +it('uses fallback logger when no logger provided', () => { + const spy = jest.spyOn(fallbackLogger, 'warn'); + const logger = safeMinLogger(undefined); + + logger.warn('test message'); + + expect(spy).toHaveBeenCalledWith('test message'); + spy.mockRestore(); +}); + +it('uses provided logger when it works correctly', () => { + const mockWarn = jest.fn(); + const testLogger: MinLogger = { + warn: mockWarn, + }; + + const logger = safeMinLogger(testLogger); + logger.warn('test message'); + + expect(mockWarn).toHaveBeenCalledWith('test message'); +}); + +it('falls back to fallback logger when provided logger throws', () => { + const spy = jest.spyOn(fallbackLogger, 'warn'); + const testLogger: MinLogger = { + warn: () => { + throw new Error('logger error'); + }, + }; + + const logger = safeMinLogger(testLogger); + logger.warn('test message'); + + expect(spy).toHaveBeenCalledWith('test message'); + expect(spy).toHaveBeenCalledWith( + 'LaunchDarkly - Browser Telemetry: The provided logger threw an exception, using fallback logger.', + ); + spy.mockRestore(); +}); diff --git a/packages/telemetry/browser-telemetry/__tests__/options.test.ts b/packages/telemetry/browser-telemetry/__tests__/options.test.ts index c3c51f731..c4d543ba4 100644 --- a/packages/telemetry/browser-telemetry/__tests__/options.test.ts +++ b/packages/telemetry/browser-telemetry/__tests__/options.test.ts @@ -63,7 +63,7 @@ it('warns when maxPendingEvents is not a number', () => { expect(outOptions.maxPendingEvents).toEqual(defaultOptions().maxPendingEvents); expect(mockLogger.warn).toHaveBeenCalledWith( - 'Config option "maxPendingEvents" should be of type number, got string, using default value', + 'LaunchDarkly - Browser Telemetry: Config option "maxPendingEvents" should be of type number, got string, using default value', ); }); @@ -90,7 +90,7 @@ it('warns when breadcrumbs config is not an object', () => { expect(outOptions.breadcrumbs).toEqual(defaultOptions().breadcrumbs); expect(mockLogger.warn).toHaveBeenCalledWith( - 'Config option "breadcrumbs" should be of type object, got string, using default value', + 'LaunchDarkly - Browser Telemetry: Config option "breadcrumbs" should be of type object, got string, using default value', ); }); @@ -105,7 +105,7 @@ it('warns when collectors is not an array', () => { expect(outOptions.collectors).toEqual(defaultOptions().collectors); expect(mockLogger.warn).toHaveBeenCalledWith( - 'Config option "collectors" should be of type Collector[], got string, using default value', + 'LaunchDarkly - Browser Telemetry: Config option "collectors" should be of type Collector[], got string, using default value', ); }); @@ -133,7 +133,7 @@ it('warns when stack config is not an object', () => { expect(outOptions.stack).toEqual(defaultOptions().stack); expect(mockLogger.warn).toHaveBeenCalledWith( - 'Config option "stack" should be of type object, got string, using default value', + 'LaunchDarkly - Browser Telemetry: Config option "stack" should be of type object, got string, using default value', ); }); @@ -152,7 +152,7 @@ it('warns when breadcrumbs.maxBreadcrumbs is not a number', () => { defaultOptions().breadcrumbs.maxBreadcrumbs, ); expect(mockLogger.warn).toHaveBeenCalledWith( - 'Config option "breadcrumbs.maxBreadcrumbs" should be of type number, got string, using default value', + 'LaunchDarkly - Browser Telemetry: Config option "breadcrumbs.maxBreadcrumbs" should be of type number, got string, using default value', ); }); @@ -183,7 +183,7 @@ it('warns when breadcrumbs.click is not boolean', () => { expect(outOptions.breadcrumbs.click).toEqual(defaultOptions().breadcrumbs.click); expect(mockLogger.warn).toHaveBeenCalledWith( - 'Config option "breadcrumbs.click" should be of type boolean, got string, using default value', + 'LaunchDarkly - Browser Telemetry: Config option "breadcrumbs.click" should be of type boolean, got string, using default value', ); }); @@ -200,7 +200,7 @@ it('warns when breadcrumbs.evaluations is not boolean', () => { expect(outOptions.breadcrumbs.evaluations).toEqual(defaultOptions().breadcrumbs.evaluations); expect(mockLogger.warn).toHaveBeenCalledWith( - 'Config option "breadcrumbs.evaluations" should be of type boolean, got string, using default value', + 'LaunchDarkly - Browser Telemetry: Config option "breadcrumbs.evaluations" should be of type boolean, got string, using default value', ); }); @@ -217,7 +217,7 @@ it('warns when breadcrumbs.flagChange is not boolean', () => { expect(outOptions.breadcrumbs.flagChange).toEqual(defaultOptions().breadcrumbs.flagChange); expect(mockLogger.warn).toHaveBeenCalledWith( - 'Config option "breadcrumbs.flagChange" should be of type boolean, got string, using default value', + 'LaunchDarkly - Browser Telemetry: Config option "breadcrumbs.flagChange" should be of type boolean, got string, using default value', ); }); @@ -234,7 +234,7 @@ it('warns when breadcrumbs.keyboardInput is not boolean', () => { expect(outOptions.breadcrumbs.keyboardInput).toEqual(defaultOptions().breadcrumbs.keyboardInput); expect(mockLogger.warn).toHaveBeenCalledWith( - 'Config option "breadcrumbs.keyboardInput" should be of type boolean, got string, using default value', + 'LaunchDarkly - Browser Telemetry: Config option "breadcrumbs.keyboardInput" should be of type boolean, got string, using default value', ); }); @@ -307,7 +307,7 @@ it('warns when breadcrumbs.http is not an object', () => { expect(outOptions.breadcrumbs.http).toEqual(defaultOptions().breadcrumbs.http); expect(mockLogger.warn).toHaveBeenCalledWith( - 'Config option "breadcrumbs.http" should be of type HttpBreadCrumbOptions | false, got string, using default value', + 'LaunchDarkly - Browser Telemetry: Config option "breadcrumbs.http" should be of type HttpBreadCrumbOptions | false, got string, using default value', ); }); @@ -328,7 +328,7 @@ it('warns when breadcrumbs.http.instrumentFetch is not boolean', () => { defaultOptions().breadcrumbs.http.instrumentFetch, ); expect(mockLogger.warn).toHaveBeenCalledWith( - 'Config option "breadcrumbs.http.instrumentFetch" should be of type boolean, got string, using default value', + 'LaunchDarkly - Browser Telemetry: Config option "breadcrumbs.http.instrumentFetch" should be of type boolean, got string, using default value', ); }); @@ -349,7 +349,7 @@ it('warns when breadcrumbs.http.instrumentXhr is not boolean', () => { defaultOptions().breadcrumbs.http.instrumentXhr, ); expect(mockLogger.warn).toHaveBeenCalledWith( - 'Config option "breadcrumbs.http.instrumentXhr" should be of type boolean, got string, using default value', + 'LaunchDarkly - Browser Telemetry: Config option "breadcrumbs.http.instrumentXhr" should be of type boolean, got string, using default value', ); }); @@ -419,7 +419,7 @@ it('warns when breadcrumbs.http.customUrlFilter is not a function', () => { expect(outOptions.breadcrumbs.http.customUrlFilter).toBeUndefined(); expect(mockLogger.warn).toHaveBeenCalledWith( - 'The "breadcrumbs.http.customUrlFilter" must be a function. Received string', + 'LaunchDarkly - Browser Telemetry: The "breadcrumbs.http.customUrlFilter" must be a function. Received string', ); }); @@ -435,6 +435,6 @@ it('warns when filters is not an array', () => { ); expect(outOptions.breadcrumbs.filters).toEqual([]); expect(mockLogger.warn).toHaveBeenCalledWith( - 'Config option "breadcrumbs.filters" should be of type array, got string, using default value', + 'LaunchDarkly - Browser Telemetry: Config option "breadcrumbs.filters" should be of type array, got string, using default value', ); }); diff --git a/packages/telemetry/browser-telemetry/src/BrowserTelemetryImpl.ts b/packages/telemetry/browser-telemetry/src/BrowserTelemetryImpl.ts index 2e16a4f24..977e93f39 100644 --- a/packages/telemetry/browser-telemetry/src/BrowserTelemetryImpl.ts +++ b/packages/telemetry/browser-telemetry/src/BrowserTelemetryImpl.ts @@ -5,7 +5,7 @@ */ import type { LDContext, LDEvaluationDetail, LDInspection } from '@launchdarkly/js-client-sdk'; -import { BreadcrumbFilter, LDClientTracking } from './api'; +import { BreadcrumbFilter, LDClientLogging, LDClientTracking, MinLogger } from './api'; import { Breadcrumb, FeatureManagementBreadcrumb } from './api/Breadcrumb'; import { BrowserTelemetry } from './api/BrowserTelemetry'; import { Collector } from './api/Collector'; @@ -18,6 +18,7 @@ import FetchCollector from './collectors/http/fetch'; import XhrCollector from './collectors/http/xhr'; import defaultUrlFilter from './filters/defaultUrlFilter'; import makeInspectors from './inspectors'; +import { fallbackLogger, prefixLog } from './logging'; import { ParsedOptions, ParsedStackOptions } from './options'; import randomUuidV4 from './randomUuidV4'; import parse from './stack/StackParser'; @@ -59,21 +60,6 @@ function applyBreadcrumbFilter( return breadcrumb === undefined ? undefined : filter(breadcrumb); } -function applyBreadcrumbFilters( - breadcrumb: Breadcrumb, - filters: BreadcrumbFilter[], -): Breadcrumb | undefined { - try { - return filters.reduce( - (breadcrumbToFilter: Breadcrumb | undefined, filter: BreadcrumbFilter) => - applyBreadcrumbFilter(breadcrumbToFilter, filter), - breadcrumb, - ); - } catch (e) { - return undefined; - } -} - function configureTraceKit(options: ParsedStackOptions) { const TraceKit = getTraceKit(); // Include before + after + source line. @@ -89,6 +75,16 @@ function configureTraceKit(options: ParsedStackOptions) { anyObj.linesOfContext = beforeAfterMax * 2 + 1; } +/** + * Check if the client supports LDClientLogging. + * + * @param client The client to check. + * @returns True if the client is an instance of LDClientLogging. + */ +function isLDClientLogging(client: unknown): client is LDClientLogging { + return (client as any).logger !== undefined; +} + export default class BrowserTelemetryImpl implements BrowserTelemetry { private _maxPendingEvents: number; private _maxBreadcrumbs: number; @@ -102,6 +98,13 @@ export default class BrowserTelemetryImpl implements BrowserTelemetry { private _collectors: Collector[] = []; private _sessionId: string = randomUuidV4(); + private _logger: MinLogger; + + // Used to ensure we only log the event dropped message once. + private _eventsDropped: boolean = false; + // Used to ensure we only log the breadcrumb filter error once. + private _breadcrumbFilterError: boolean = false; + constructor(private _options: ParsedOptions) { configureTraceKit(_options.stack); @@ -149,16 +152,35 @@ export default class BrowserTelemetryImpl implements BrowserTelemetry { const inspectors: LDInspection[] = []; makeInspectors(_options, inspectors, impl); this._inspectorInstances.push(...inspectors); + + // Set the initial logger, it may be replaced when the client is registered. + // For typescript purposes, we need the logger to be directly set in the constructor. + this._logger = this._options.logger ?? fallbackLogger; } register(client: LDClientTracking): void { this._client = client; + // When the client is registered, we need to set the logger again, because we may be able to use the client's + // logger. + this._setLogger(); this._pendingEvents.forEach((event) => { this._client?.track(event.type, event.data); }); this._pendingEvents = []; } + private _setLogger() { + // If the user has provided a logger, then we want to prioritize that over the client's logger. + // If the client supports LDClientLogging, then we to prioritize that over the fallback logger. + if (this._options.logger) { + this._logger = this._options.logger; + } else if (isLDClientLogging(this._client)) { + this._logger = this._client.logger; + } else { + this._logger = fallbackLogger; + } + } + inspectors(): LDInspection[] { return this._inspectorInstances; } @@ -175,7 +197,14 @@ export default class BrowserTelemetryImpl implements BrowserTelemetry { if (this._client === undefined) { this._pendingEvents.push({ type, data: event }); if (this._pendingEvents.length > this._maxPendingEvents) { - // TODO: Log when pending events must be dropped. (SDK-915) + if (!this._eventsDropped) { + this._eventsDropped = true; + this._logger.warn( + prefixLog( + `Maximum pending events reached. Old events will be dropped until the SDK client is registered.`, + ), + ); + } this._pendingEvents.shift(); } } @@ -212,8 +241,27 @@ export default class BrowserTelemetryImpl implements BrowserTelemetry { this._capture(SESSION_CAPTURE_KEY, { ...sessionEvent, breadcrumbs: [...this._breadcrumbs] }); } + private _applyBreadcrumbFilters( + breadcrumb: Breadcrumb, + filters: BreadcrumbFilter[], + ): Breadcrumb | undefined { + try { + return filters.reduce( + (breadcrumbToFilter: Breadcrumb | undefined, filter: BreadcrumbFilter) => + applyBreadcrumbFilter(breadcrumbToFilter, filter), + breadcrumb, + ); + } catch (e) { + if (!this._breadcrumbFilterError) { + this._breadcrumbFilterError = true; + this._logger.warn(prefixLog(`Error applying breadcrumb filters: ${e}`)); + } + return undefined; + } + } + addBreadcrumb(breadcrumb: Breadcrumb): void { - const filtered = applyBreadcrumbFilters(breadcrumb, this._options.breadcrumbs.filters); + const filtered = this._applyBreadcrumbFilters(breadcrumb, this._options.breadcrumbs.filters); if (filtered !== undefined) { this._breadcrumbs.push(filtered); if (this._breadcrumbs.length > this._maxBreadcrumbs) { diff --git a/packages/telemetry/browser-telemetry/src/MinLogger.ts b/packages/telemetry/browser-telemetry/src/api/MinLogger.ts similarity index 72% rename from packages/telemetry/browser-telemetry/src/MinLogger.ts rename to packages/telemetry/browser-telemetry/src/api/MinLogger.ts index dced72e95..6c76e558d 100644 --- a/packages/telemetry/browser-telemetry/src/MinLogger.ts +++ b/packages/telemetry/browser-telemetry/src/api/MinLogger.ts @@ -5,5 +5,11 @@ * This allows usage with multiple SDK versions. */ export interface MinLogger { + /** + * The warning logger. + * + * @param args + * A sequence of any JavaScript values. + */ warn(...args: any[]): void; } diff --git a/packages/telemetry/browser-telemetry/src/api/Options.ts b/packages/telemetry/browser-telemetry/src/api/Options.ts index ea8de25b0..32137ea71 100644 --- a/packages/telemetry/browser-telemetry/src/api/Options.ts +++ b/packages/telemetry/browser-telemetry/src/api/Options.ts @@ -1,5 +1,6 @@ import { Breadcrumb } from './Breadcrumb'; import { Collector } from './Collector'; +import { MinLogger } from './MinLogger'; /** * Interface for URL filters. @@ -185,4 +186,15 @@ export interface Options { * Configuration that controls the capture of the stack trace. */ stack?: StackOptions; + + /** + * Logger to use for warnings. + * + * This option is compatible with the `LDLogger` interface used by the LaunchDarkly SDK. + * + * If this option is not provided, the logs will be written to console.log unless the LaunchDarkly SDK is registered, + * and the registered SDK instance exposes its logger. In which case, the logs will be written to the registered SDK's + * logger. The 3.x SDKs do not expose their logger. + */ + logger?: MinLogger; } diff --git a/packages/telemetry/browser-telemetry/src/api/client/LDClientLogging.ts b/packages/telemetry/browser-telemetry/src/api/client/LDClientLogging.ts new file mode 100644 index 000000000..7e500a0b3 --- /dev/null +++ b/packages/telemetry/browser-telemetry/src/api/client/LDClientLogging.ts @@ -0,0 +1,8 @@ +import { MinLogger } from '../MinLogger'; + +/** + * Minimal client interface which allows for loggng. Works with 4.x and higher versions of the javascript client. + */ +export interface LDClientLogging { + readonly logger: MinLogger; +} diff --git a/packages/telemetry/browser-telemetry/src/api/client/index.ts b/packages/telemetry/browser-telemetry/src/api/client/index.ts index d363ce8c7..4b9285e3e 100644 --- a/packages/telemetry/browser-telemetry/src/api/client/index.ts +++ b/packages/telemetry/browser-telemetry/src/api/client/index.ts @@ -1 +1,2 @@ export * from './LDClientTracking'; +export * from './LDClientLogging'; diff --git a/packages/telemetry/browser-telemetry/src/api/index.ts b/packages/telemetry/browser-telemetry/src/api/index.ts index b71214eb4..b918c0bb9 100644 --- a/packages/telemetry/browser-telemetry/src/api/index.ts +++ b/packages/telemetry/browser-telemetry/src/api/index.ts @@ -5,3 +5,4 @@ export * from './Options'; export * from './Recorder'; export * from './stack'; export * from './client'; +export * from './MinLogger'; diff --git a/packages/telemetry/browser-telemetry/src/index.ts b/packages/telemetry/browser-telemetry/src/index.ts index c2c50a935..e52ba601c 100644 --- a/packages/telemetry/browser-telemetry/src/index.ts +++ b/packages/telemetry/browser-telemetry/src/index.ts @@ -1,11 +1,12 @@ import { BrowserTelemetry } from './api/BrowserTelemetry'; import { Options } from './api/Options'; import BrowserTelemetryImpl from './BrowserTelemetryImpl'; +import { safeMinLogger } from './logging'; import parse from './options'; export * from './api'; export function initializeTelemetry(options?: Options): BrowserTelemetry { - const parsedOptions = parse(options || {}); + const parsedOptions = parse(options || {}, safeMinLogger(options?.logger)); return new BrowserTelemetryImpl(parsedOptions); } diff --git a/packages/telemetry/browser-telemetry/src/logging.ts b/packages/telemetry/browser-telemetry/src/logging.ts new file mode 100644 index 000000000..8bd6e7946 --- /dev/null +++ b/packages/telemetry/browser-telemetry/src/logging.ts @@ -0,0 +1,33 @@ +import { MinLogger } from './api'; + +export const fallbackLogger: MinLogger = { + // Intentionally using console.warn as a fallback logger. + // eslint-disable-next-line no-console + warn: console.warn, +}; + +const loggingPrefix = 'LaunchDarkly - Browser Telemetry:'; + +export function prefixLog(message: string) { + return `${loggingPrefix} ${message}`; +} + +export function safeMinLogger(logger: MinLogger | undefined): MinLogger { + return { + warn: (...args: any[]) => { + if (!logger) { + fallbackLogger.warn(...args); + return; + } + + try { + logger.warn(...args); + } catch { + fallbackLogger.warn(...args); + fallbackLogger.warn( + prefixLog('The provided logger threw an exception, using fallback logger.'), + ); + } + }, + }; +} diff --git a/packages/telemetry/browser-telemetry/src/options.ts b/packages/telemetry/browser-telemetry/src/options.ts index 8621fcc7b..af5620026 100644 --- a/packages/telemetry/browser-telemetry/src/options.ts +++ b/packages/telemetry/browser-telemetry/src/options.ts @@ -1,4 +1,5 @@ import { Collector } from './api/Collector'; +import { MinLogger } from './api/MinLogger'; import { BreadcrumbFilter, HttpBreadcrumbOptions, @@ -6,7 +7,7 @@ import { StackOptions, UrlFilter, } from './api/Options'; -import { MinLogger } from './MinLogger'; +import { fallbackLogger, prefixLog, safeMinLogger } from './logging'; export function defaultOptions(): ParsedOptions { return { @@ -35,7 +36,9 @@ export function defaultOptions(): ParsedOptions { } function wrongOptionType(name: string, expectedType: string, actualType: string): string { - return `Config option "${name}" should be of type ${expectedType}, got ${actualType}, using default value`; + return prefixLog( + `Config option "${name}" should be of type ${expectedType}, got ${actualType}, using default value`, + ); } function checkBasic(type: string, name: string, logger?: MinLogger): (item: T) => boolean { @@ -84,7 +87,9 @@ function parseHttp( if (options?.customUrlFilter) { if (typeof options.customUrlFilter !== 'function') { logger?.warn( - `The "breadcrumbs.http.customUrlFilter" must be a function. Received ${typeof options.customUrlFilter}`, + prefixLog( + `The "breadcrumbs.http.customUrlFilter" must be a function. Received ${typeof options.customUrlFilter}`, + ), ); } } @@ -108,6 +113,18 @@ function parseHttp( }; } +function parseLogger(options: Options): MinLogger | undefined { + if (options.logger) { + const { logger } = options; + if (typeof logger === 'object' && logger !== null && 'warn' in logger) { + return safeMinLogger(logger); + } + // Using console.warn here because the logger is not suitable to log with. + fallbackLogger.warn(wrongOptionType('logger', 'MinLogger or LDLogger', typeof logger)); + } + return undefined; +} + function parseStack( options: StackOptions | undefined, defaults: ParsedStackOptions, @@ -187,10 +204,11 @@ export default function parse(options: Options, logger?: MinLogger): ParsedOptio if (Array.isArray(item)) { return true; } - logger?.warn(logger?.warn(wrongOptionType('collectors', 'Collector[]', typeof item))); + logger?.warn(wrongOptionType('collectors', 'Collector[]', typeof item)); return false; }), ], + logger: parseLogger(options), }; } @@ -299,4 +317,9 @@ export interface ParsedOptions { * Additional, or custom, collectors. */ collectors: Collector[]; + + /** + * Logger to use for warnings. + */ + logger?: MinLogger; }