diff --git a/src/batchUploader.ts b/src/batchUploader.ts index 9f456dad0..b9b598d9d 100644 --- a/src/batchUploader.ts +++ b/src/batchUploader.ts @@ -259,15 +259,15 @@ export class BatchUploader { return; } - const { verbose } = this.mpInstance.Logger; + const { Logger } = this.mpInstance; this.eventsQueuedForProcessing.push(event); if (this.offlineStorageEnabled && this.eventVault) { this.eventVault.store(this.eventsQueuedForProcessing); } - verbose(`Queuing event: ${JSON.stringify(event)}`); - verbose(`Queued event count: ${this.eventsQueuedForProcessing.length}`); + Logger.verbose(`Queuing event: ${JSON.stringify(event)}`); + Logger.verbose(`Queued event count: ${this.eventsQueuedForProcessing.length}`); if (this.shouldTriggerImmediateUpload(event.EventDataType)) { this.prepareAndUpload(false, false); diff --git a/src/identityApiClient.ts b/src/identityApiClient.ts index 2b36a88de..e357a645d 100644 --- a/src/identityApiClient.ts +++ b/src/identityApiClient.ts @@ -88,12 +88,12 @@ export default function IdentityAPIClient( aliasRequest: IAliasRequest, aliasCallback: IAliasCallback ) { - const { verbose, error } = mpInstance.Logger; + const { Logger } = mpInstance; const { invokeAliasCallback } = mpInstance._Helpers; const { aliasUrl } = mpInstance._Store.SDKConfig; const { devToken: apiKey } = mpInstance._Store; - verbose(Messages.InformationMessages.SendAliasHttp); + Logger.verbose(Messages.InformationMessages.SendAliasHttp); // https://go.mparticle.com/work/SQDSDKS-6750 const uploadUrl = `https://${aliasUrl}${apiKey}/Alias`; @@ -136,7 +136,7 @@ export default function IdentityAPIClient( try { aliasResponseBody = await response.json(); } catch (e) { - verbose('The request has no response body'); + Logger.verbose('The request has no response body'); } } else { // https://go.mparticle.com/work/SQDSDKS-6568 @@ -171,11 +171,11 @@ export default function IdentityAPIClient( } - verbose(message); + Logger.verbose(message); invokeAliasCallback(aliasCallback, response.status, errorMessage); } catch (e) { const errorMessage = (e as Error).message || e.toString(); - error('Error sending alias request to mParticle servers. ' + errorMessage); + Logger.error('Error sending alias request to mParticle servers. ' + errorMessage); invokeAliasCallback( aliasCallback, HTTPCodes.noHttpCoverage, @@ -193,15 +193,14 @@ export default function IdentityAPIClient( mpid: MPID, knownIdentities: UserIdentities ) { - const { verbose, error } = mpInstance.Logger; const { invokeCallback } = mpInstance._Helpers; - - verbose(Messages.InformationMessages.SendIdentityBegin); + const { Logger } = mpInstance; + Logger.verbose(Messages.InformationMessages.SendIdentityBegin); if (!identityApiRequest) { - error(Messages.ErrorMessages.APIRequestEmpty); + Logger.error(Messages.ErrorMessages.APIRequestEmpty); return; } - verbose(Messages.InformationMessages.SendIdentityHttp); + Logger.verbose(Messages.InformationMessages.SendIdentityHttp); if (mpInstance._Store.identityCallInFlight) { invokeCallback( @@ -289,7 +288,7 @@ export default function IdentityAPIClient( mpInstance._Store.identityCallInFlight = false; - verbose(message); + Logger.verbose(message); parseIdentityResponse( identityResponse, previousMPID, @@ -304,7 +303,7 @@ export default function IdentityAPIClient( const errorMessage = (err as Error).message || err.toString(); - error('Error sending identity request to servers' + ' - ' + errorMessage); + Logger.error('Error sending identity request to servers' + ' - ' + errorMessage); invokeCallback( callback, HTTPCodes.noHttpCoverage, diff --git a/src/logger.js b/src/logger.js deleted file mode 100644 index a88d13321..000000000 --- a/src/logger.js +++ /dev/null @@ -1,61 +0,0 @@ -function Logger(config) { - var self = this; - var logLevel = config.logLevel || 'warning'; - if (config.hasOwnProperty('logger')) { - this.logger = config.logger; - } else { - this.logger = new ConsoleLogger(); - } - - this.verbose = function(msg) { - if (logLevel !== 'none') { - if (self.logger.verbose && logLevel === 'verbose') { - self.logger.verbose(msg); - } - } - }; - - this.warning = function(msg) { - if (logLevel !== 'none') { - if ( - self.logger.warning && - (logLevel === 'verbose' || logLevel === 'warning') - ) { - self.logger.warning(msg); - } - } - }; - - this.error = function(msg) { - if (logLevel !== 'none') { - if (self.logger.error) { - self.logger.error(msg); - } - } - }; - - this.setLogLevel = function(newLogLevel) { - logLevel = newLogLevel; - }; -} - -function ConsoleLogger() { - this.verbose = function(msg) { - if (console && console.info) { - console.info(msg); - } - }; - - this.error = function(msg) { - if (console && console.error) { - console.error(msg); - } - }; - this.warning = function(msg) { - if (console && console.warn) { - console.warn(msg); - } - }; -} - -export default Logger; diff --git a/src/logger.ts b/src/logger.ts new file mode 100644 index 000000000..6ec19c056 --- /dev/null +++ b/src/logger.ts @@ -0,0 +1,66 @@ +import { LogLevelType, SDKInitConfig, SDKLoggerApi } from './sdkRuntimeModels'; + +export type ILoggerConfig = Pick; +export type IConsoleLogger = Partial>; + +export class Logger { + private logLevel: LogLevelType; + private logger: IConsoleLogger; + + constructor(config: ILoggerConfig) { + this.logLevel = config.logLevel ?? LogLevelType.Warning; + this.logger = config.logger ?? new ConsoleLogger(); + } + + public verbose(msg: string): void { + if(this.logLevel === LogLevelType.None) + return; + + if (this.logger.verbose && this.logLevel === LogLevelType.Verbose) { + this.logger.verbose(msg); + } + } + + public warning(msg: string): void { + if(this.logLevel === LogLevelType.None) + return; + + if (this.logger.warning && + (this.logLevel === LogLevelType.Verbose || this.logLevel === LogLevelType.Warning)) { + this.logger.warning(msg); + } + } + + public error(msg: string): void { + if(this.logLevel === LogLevelType.None) + return; + + if (this.logger.error) { + this.logger.error(msg); + } + } + + public setLogLevel(newLogLevel: LogLevelType): void { + this.logLevel = newLogLevel; + } +} + +export class ConsoleLogger implements IConsoleLogger { + public verbose(msg: string): void { + if (console && console.info) { + console.info(msg); + } + } + + public error(msg: string): void { + if (console && console.error) { + console.error(msg); + } + } + + public warning(msg: string): void { + if (console && console.warn) { + console.warn(msg); + } + } +} diff --git a/src/mp-instance.ts b/src/mp-instance.ts index fd32ef2b8..1448e4941 100644 --- a/src/mp-instance.ts +++ b/src/mp-instance.ts @@ -25,7 +25,7 @@ import CookieSyncManager, { ICookieSyncManager } from './cookieSyncManager'; import SessionManager, { ISessionManager } from './sessionManager'; import Ecommerce from './ecommerce'; import Store, { IStore } from './store'; -import Logger from './logger'; +import { Logger } from './logger'; import Persistence from './persistence'; import Events from './events'; import Forwarders from './forwarders'; @@ -227,6 +227,7 @@ export default function mParticleInstance(this: IMParticleWebSDKInstance, instan if (instance._Store) { delete instance._Store; } + instance.Logger = new Logger(config); instance._Store = new Store(config, instance); instance._Store.isLocalStorageAvailable = instance._Persistence.determineLocalStorageAvailability( window.localStorage diff --git a/src/sdkRuntimeModels.ts b/src/sdkRuntimeModels.ts index 6e73ebc43..579c9832e 100644 --- a/src/sdkRuntimeModels.ts +++ b/src/sdkRuntimeModels.ts @@ -40,6 +40,7 @@ import { SDKECommerceAPI } from './ecommerce.interfaces'; import { IErrorLogMessage, IMParticleWebSDKInstance, IntegrationDelays } from './mp-instance'; import Constants from './constants'; import RoktManager, { IRoktLauncherOptions } from './roktManager'; +import { IConsoleLogger } from './logger'; // TODO: Resolve this with version in @mparticle/web-sdk export type SDKEventCustomFlags = Dictionary; @@ -259,7 +260,13 @@ export interface IMParticleInstanceManager extends MParticleWebSDK { export type BooleanStringLowerCase = 'false' | 'true'; export type BooleanStringTitleCase = 'False' | 'True'; -export type LogLevelType = 'none' | 'verbose' | 'warning' | 'error'; +export type LogLevelType = (typeof LogLevelType)[keyof typeof LogLevelType]; +export const LogLevelType = { + None: 'none', + Verbose: 'verbose', + Warning: 'warning', + Error: 'error', +} as const; // TODO: This should eventually be moved into wherever init logic lives // TODO: Replace/Merge this with MPConfiguration in @types/mparticle__web-sdk @@ -309,6 +316,7 @@ export interface SDKInitConfig launcherOptions?: IRoktLauncherOptions; rq?: Function[] | any[]; + logger?: IConsoleLogger; } export interface DataPlanConfig { diff --git a/test/jest/logger.spec.ts b/test/jest/logger.spec.ts new file mode 100644 index 000000000..def3a637d --- /dev/null +++ b/test/jest/logger.spec.ts @@ -0,0 +1,136 @@ +import { Logger, ConsoleLogger } from '../../src/logger'; +import { LogLevelType } from '../../src/sdkRuntimeModels'; + +describe('Logger', () => { + let mockConsole: any; + let logger: Logger; + + beforeEach(() => { + mockConsole = { + info: jest.fn(), + warn: jest.fn(), + error: jest.fn() + }; + (global as any).console = mockConsole; + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should call verbose, warning, and error methods on ConsoleLogger at correct log levels', () => { + logger = new Logger({ logLevel: LogLevelType.Verbose }); + + logger.verbose('message1'); + logger.warning('message2'); + logger.error('message3'); + + expect(mockConsole.info).toHaveBeenCalledWith('message1'); + expect(mockConsole.warn).toHaveBeenCalledWith('message2'); + expect(mockConsole.error).toHaveBeenCalledWith('message3'); + }); + + it('should only call warning and error at warning log level', () => { + logger = new Logger({ logLevel: LogLevelType.Warning }); + + logger.verbose('message1'); + logger.warning('message2'); + logger.error('message3'); + + expect(mockConsole.info).not.toHaveBeenCalled(); + expect(mockConsole.warn).toHaveBeenCalledWith('message2'); + expect(mockConsole.error).toHaveBeenCalledWith('message3'); + }); + + it('should not call any log methods at none log level', () => { + logger = new Logger({ logLevel: LogLevelType.None }); + + logger.verbose('message1'); + logger.warning('message2'); + logger.error('message3'); + + expect(mockConsole.info).not.toHaveBeenCalled(); + expect(mockConsole.warn).not.toHaveBeenCalled(); + expect(mockConsole.error).not.toHaveBeenCalled(); + }); + + it('should only call error at error log level', () => { + logger = new Logger({ logLevel: LogLevelType.Error }); + + logger.verbose('message1'); + logger.warning('message2'); + logger.error('message3'); + + expect(mockConsole.info).not.toHaveBeenCalled(); + expect(mockConsole.warn).not.toHaveBeenCalled(); + expect(mockConsole.error).toHaveBeenCalledWith('message3'); + }); + + it('should allow providing a custom logger', () => { + const customLogger = { + verbose: jest.fn(), + warning: jest.fn(), + error: jest.fn() + }; + + logger = new Logger({ logLevel: 'verbose' as any, logger: customLogger }); + + logger.verbose('test-verbose'); + logger.warning('test-warning'); + logger.error('test-error'); + + expect(customLogger.verbose).toHaveBeenCalledWith('test-verbose'); + expect(customLogger.warning).toHaveBeenCalledWith('test-warning'); + expect(customLogger.error).toHaveBeenCalledWith('test-error'); + }); + + it('should change log level with setLogLevel', () => { + logger = new Logger({ logLevel: 'none' as any }); + + logger.verbose('one'); + logger.warning('two'); + logger.error('three'); + expect(mockConsole.info).not.toHaveBeenCalled(); + expect(mockConsole.warn).not.toHaveBeenCalled(); + expect(mockConsole.error).not.toHaveBeenCalled(); + + logger.setLogLevel('verbose' as any); + + logger.verbose('a'); + logger.warning('b'); + logger.error('c'); + expect(mockConsole.info).toHaveBeenCalledWith('a'); + expect(mockConsole.warn).toHaveBeenCalledWith('b'); + expect(mockConsole.error).toHaveBeenCalledWith('c'); + }); +}); + +describe('ConsoleLogger', () => { + let mockConsole: any; + let consoleLogger: ConsoleLogger; + + beforeEach(() => { + mockConsole = { + info: jest.fn(), + warn: jest.fn(), + error: jest.fn() + }; + (global as any).console = mockConsole; + consoleLogger = new ConsoleLogger(); + }); + + it('should use console.info for verbose', () => { + consoleLogger.verbose('hi'); + expect(mockConsole.info).toHaveBeenCalledWith('hi'); + }); + + it('should use console.warn for warning', () => { + consoleLogger.warning('warn msg'); + expect(mockConsole.warn).toHaveBeenCalledWith('warn msg'); + }); + + it('should use console.error for error', () => { + consoleLogger.error('err'); + expect(mockConsole.error).toHaveBeenCalledWith('err'); + }); +}); diff --git a/test/src/tests-audience-manager.ts b/test/src/tests-audience-manager.ts index 9bee9a8e0..db6111fce 100644 --- a/test/src/tests-audience-manager.ts +++ b/test/src/tests-audience-manager.ts @@ -7,7 +7,7 @@ import { IMParticleInstanceManager, SDKLoggerApi } from '../../src/sdkRuntimeMod import AudienceManager, { IAudienceMemberships, IAudienceMembershipsServerResponse } from '../../src/audienceManager'; -import Logger from '../../src/logger.js'; +import { Logger } from '../../src/logger'; import Utils from './config/utils'; const { fetchMockSuccess } = Utils; diff --git a/test/src/tests-batchUploader.ts b/test/src/tests-batchUploader.ts index 681a0b0d6..7d1a9819c 100644 --- a/test/src/tests-batchUploader.ts +++ b/test/src/tests-batchUploader.ts @@ -10,7 +10,7 @@ import Utils from './config/utils'; import { BatchUploader } from '../../src/batchUploader'; import { expect } from 'chai'; import _BatchValidator from '../../src/mockBatchCreator'; -import Logger from '../../src/logger.js'; +import { Logger } from '../../src/logger'; import { event0, event1, event2, event3 } from '../fixtures/events'; import fetchMock from 'fetch-mock/esm/client'; const {