diff --git a/src/__tests__/IterableInApp.test.ts b/src/__tests__/IterableInApp.test.ts index b4a157413..bddb3f3f9 100644 --- a/src/__tests__/IterableInApp.test.ts +++ b/src/__tests__/IterableInApp.test.ts @@ -1,7 +1,5 @@ import { NativeEventEmitter } from 'react-native'; -import { IterableLogger } from '../core'; - import { MockRNIterableAPI } from '../__mocks__/MockRNIterableAPI'; import { @@ -21,7 +19,6 @@ import { describe('Iterable In App', () => { beforeEach(() => { jest.clearAllMocks(); - Iterable.logger = new IterableLogger(new IterableConfig()); }); test('trackInAppOpen_params_methodCalledWithParams', () => { @@ -202,9 +199,11 @@ describe('Iterable In App', () => { // WHEN the simulated local queue is set to the in-app messages MockRNIterableAPI.setMessages(messages); // THEN Iterable.inAppManager.getMessages returns the list of in-app messages - return await Iterable.inAppManager?.getMessages().then((messagesObtained) => { - expect(messagesObtained).toEqual(messages); - }); + return await Iterable.inAppManager + ?.getMessages() + .then((messagesObtained) => { + expect(messagesObtained).toEqual(messages); + }); }); test('showMessage_messageAndConsume_returnsClickedUrl', async () => { @@ -222,9 +221,11 @@ describe('Iterable In App', () => { // WHEN the simulated clicked url is set to the clicked url MockRNIterableAPI.setClickedUrl(clickedUrl); // THEN Iterable,inAppManager.showMessage returns the simulated clicked url - return await Iterable.inAppManager?.showMessage(message, consume).then((url) => { - expect(url).toEqual(clickedUrl); - }); + return await Iterable.inAppManager + ?.showMessage(message, consume) + .then((url) => { + expect(url).toEqual(clickedUrl); + }); }); test('removeMessage_params_methodCalledWithParams', () => { diff --git a/src/core/classes/Iterable.test.ts b/src/core/classes/Iterable.test.ts index 79c47792c..7774b5bba 100644 --- a/src/core/classes/Iterable.test.ts +++ b/src/core/classes/Iterable.test.ts @@ -2,7 +2,6 @@ import { NativeEventEmitter, Platform } from 'react-native'; import { MockLinking } from '../../__mocks__/MockLinking'; import { MockRNIterableAPI } from '../../__mocks__/MockRNIterableAPI'; -import { IterableLogger } from '..'; // import from the same location that consumers import from import { Iterable, @@ -10,33 +9,25 @@ import { IterableActionContext, IterableActionSource, IterableAttributionInfo, + IterableAuthResponse, IterableCommerceItem, IterableConfig, IterableDataRegion, IterableEventName, - IterableLogLevel, - IterableInAppMessage, IterableInAppCloseSource, IterableInAppDeleteSource, IterableInAppLocation, + IterableInAppMessage, + IterableInAppShowResponse, IterableInAppTrigger, IterableInAppTriggerType, - IterableAuthResponse, - IterableInAppShowResponse, + IterableLogLevel, } from '../..'; import { TestHelper } from '../../__tests__/TestHelper'; -const getDefaultConfig = () => { - const config = new IterableConfig(); - config.logReactNativeSdkCalls = false; - return config; -}; - describe('Iterable', () => { beforeEach(() => { jest.clearAllMocks(); - const config = getDefaultConfig(); - Iterable.logger = new IterableLogger(config); }); afterEach(() => { @@ -265,7 +256,7 @@ describe('Iterable', () => { expect(config.customActionHandler).toBe(undefined); expect(config.inAppHandler).toBe(undefined); expect(config.authHandler).toBe(undefined); - expect(config.logLevel).toBe(IterableLogLevel.info); + expect(config.logLevel).toBe(IterableLogLevel.debug); expect(config.logReactNativeSdkCalls).toBe(true); expect(config.expiringAuthTokenRefreshPeriod).toBe(60.0); expect(config.allowedProtocols).toEqual([]); @@ -281,7 +272,7 @@ describe('Iterable', () => { expect(configDict.customActionHandlerPresent).toBe(false); expect(configDict.inAppHandlerPresent).toBe(false); expect(configDict.authHandlerPresent).toBe(false); - expect(configDict.logLevel).toBe(IterableLogLevel.info); + expect(configDict.logLevel).toBe(IterableLogLevel.debug); expect(configDict.expiringAuthTokenRefreshPeriod).toBe(60.0); expect(configDict.allowedProtocols).toEqual([]); expect(configDict.androidSdkUseInMemoryStorageForInApps).toBe(false); diff --git a/src/core/classes/Iterable.ts b/src/core/classes/Iterable.ts index 0b28c2f8b..9ef784679 100644 --- a/src/core/classes/Iterable.ts +++ b/src/core/classes/Iterable.ts @@ -42,12 +42,6 @@ const RNEventEmitter = new NativeEventEmitter(RNIterableAPI); */ /* eslint-enable tsdoc/syntax */ export class Iterable { - /** - * Logger for the Iterable SDK - * Log level is set with {@link IterableLogLevel} - */ - static logger: IterableLogger = new IterableLogger(new IterableConfig()); - /** * Current configuration of the Iterable SDK */ @@ -121,9 +115,7 @@ export class Iterable { config: IterableConfig = new IterableConfig() ): Promise { Iterable.savedConfig = config; - Iterable.logger = new IterableLogger(Iterable.savedConfig); - - this.setupEventHandlers(); + this.setupIterable(config); const version = this.getVersionFromPackageJson(); @@ -141,10 +133,7 @@ export class Iterable { config: IterableConfig = new IterableConfig(), apiEndPoint: string ): Promise { - Iterable.savedConfig = config; - Iterable.logger = new IterableLogger(Iterable.savedConfig); - - this.setupEventHandlers(); + this.setupIterable(config); const version = this.getVersionFromPackageJson(); @@ -155,6 +144,22 @@ export class Iterable { }); } + /** + * @internal + * Does basic setup of the Iterable SDK. + * @param config - The configuration object for the Iterable SDK + */ + private static setupIterable(config: IterableConfig = new IterableConfig()) { + if (config) { + Iterable.savedConfig = config; + + IterableLogger.setLoggingEnabled(config.logReactNativeSdkCalls ?? true); + IterableLogger.setLogLevel(config.logLevel); + } + + this.setupEventHandlers(); + } + /** * Associate the current user with the passed in email parameter. * @@ -502,7 +507,7 @@ export class Iterable { items: IterableCommerceItem[], dataFields?: unknown ) { - Iterable?.logger?.log('trackPurchase'); + IterableLogger?.log('trackPurchase'); IterableApi.trackPurchase({ total, items, dataFields }); } @@ -531,7 +536,7 @@ export class Iterable { location: IterableInAppLocation ) { if (!message?.messageId) { - Iterable?.logger?.log( + IterableLogger?.log( `Skipping trackInAppOpen because message ID is required, but received ${message}.` ); return; @@ -968,9 +973,7 @@ export class Iterable { (promiseResult as IterableAuthResponse).failureCallback?.(); } } else { - Iterable?.logger?.log( - 'No callback received from native layer' - ); + IterableLogger?.log('No callback received from native layer'); } }, 1000); // Use unref() to prevent the timeout from keeping the process alive @@ -979,12 +982,12 @@ export class Iterable { //If promise only returns string Iterable.authManager.passAlongAuthToken(promiseResult as string); } else { - Iterable?.logger?.log( + IterableLogger?.log( 'Unexpected promise returned. Auth token expects promise of String or AuthResponse type.' ); } }) - .catch((e) => Iterable?.logger?.log(e)); + .catch((e) => IterableLogger?.log(e)); }); RNEventEmitter.addListener( @@ -1018,7 +1021,7 @@ export class Iterable { } }) .catch((reason) => { - Iterable?.logger?.log('could not open url: ' + reason); + IterableLogger?.log('could not open url: ' + reason); }); } } diff --git a/src/core/classes/IterableApi.ts b/src/core/classes/IterableApi.ts index 122b5e33a..fe2b446a3 100644 --- a/src/core/classes/IterableApi.ts +++ b/src/core/classes/IterableApi.ts @@ -11,6 +11,7 @@ import type { IterableInboxImpressionRowInfo } from '../../inbox/types/IterableI import { IterableAttributionInfo } from './IterableAttributionInfo'; import type { IterableCommerceItem } from './IterableCommerceItem'; import { IterableConfig } from './IterableConfig'; +import { IterableLogger } from './IterableLogger'; /** * Contains functions that directly interact with the native layer. @@ -39,7 +40,7 @@ export class IterableApi { version: string; } ): Promise { - // IterableLogger.log('initializeWithApiKey: ', apiKey); + IterableLogger.log('initializeWithApiKey: ', apiKey); return RNIterableAPI.initializeWithApiKey(apiKey, config.toDict(), version); } @@ -61,7 +62,7 @@ export class IterableApi { apiEndPoint: string; } ): Promise { - // IterableLogger.log('initialize2WithApiKey: ', apiKey); + IterableLogger.log('initialize2WithApiKey: ', apiKey); return RNIterableAPI.initialize2WithApiKey( apiKey, config.toDict(), @@ -86,7 +87,7 @@ export class IterableApi { * related action will be taken */ static setEmail(email: string | null, authToken?: string | null) { - // IterableLogger.log('setEmail: ', email); + IterableLogger.log('setEmail: ', email); return RNIterableAPI.setEmail(email, authToken); } @@ -96,7 +97,7 @@ export class IterableApi { * @returns The email associated with the current user */ static getEmail() { - // IterableLogger.log('getEmail'); + IterableLogger.log('getEmail'); return RNIterableAPI.getEmail(); } @@ -115,7 +116,7 @@ export class IterableApi { userId: string | null | undefined, authToken?: string | null ) { - // IterableLogger.log('setUserId: ', userId); + IterableLogger.log('setUserId: ', userId); return RNIterableAPI.setUserId(userId, authToken); } @@ -123,7 +124,7 @@ export class IterableApi { * Get the `userId` associated with the current user. */ static getUserId() { - // IterableLogger.log('getUserId'); + IterableLogger.log('getUserId'); return RNIterableAPI.getUserId(); } @@ -131,7 +132,7 @@ export class IterableApi { * Disable the device for the current user. */ static disableDeviceForCurrentUser() { - // IterableLogger.log('disableDeviceForCurrentUser'); + IterableLogger.log('disableDeviceForCurrentUser'); return RNIterableAPI.disableDeviceForCurrentUser(); } @@ -142,7 +143,7 @@ export class IterableApi { * @param mergeNestedObjects - Whether to merge nested objects */ static updateUser(dataFields: unknown, mergeNestedObjects: boolean) { - // IterableLogger.log('updateUser: ', dataFields, mergeNestedObjects); + IterableLogger.log('updateUser: ', dataFields, mergeNestedObjects); return RNIterableAPI.updateUser(dataFields, mergeNestedObjects); } @@ -153,7 +154,7 @@ export class IterableApi { * @param authToken - The new auth token (JWT) to set with the new email, optional - If null/undefined, no JWT-related action will be taken */ static updateEmail(email: string, authToken?: string | null) { - // IterableLogger.log('updateEmail: ', email, authToken); + IterableLogger.log('updateEmail: ', email, authToken); return RNIterableAPI.updateEmail(email, authToken); } @@ -186,14 +187,14 @@ export class IterableApi { appAlreadyRunning: boolean; dataFields?: unknown; }) { - // IterableLogger.log( - // 'trackPushOpenWithCampaignId: ', - // campaignId, - // templateId, - // messageId, - // appAlreadyRunning, - // dataFields - // ); + IterableLogger.log( + 'trackPushOpenWithCampaignId: ', + campaignId, + templateId, + messageId, + appAlreadyRunning, + dataFields + ); return RNIterableAPI.trackPushOpenWithCampaignId( campaignId, templateId, @@ -220,7 +221,7 @@ export class IterableApi { items: IterableCommerceItem[]; dataFields?: unknown; }) { - // IterableLogger.log('trackPurchase: ', total, items, dataFields); + IterableLogger.log('trackPurchase: ', total, items, dataFields); return RNIterableAPI.trackPurchase(total, items, dataFields); } @@ -239,7 +240,7 @@ export class IterableApi { message: IterableInAppMessage; location: IterableInAppLocation; }) { - // IterableLogger.log('trackInAppOpen: ', message, location); + IterableLogger.log('trackInAppOpen: ', message, location); return RNIterableAPI.trackInAppOpen(message.messageId, location); } @@ -262,7 +263,7 @@ export class IterableApi { location: IterableInAppLocation; clickedUrl: string; }) { - // IterableLogger.log('trackInAppClick: ', message, location, clickedUrl); + IterableLogger.log('trackInAppClick: ', message, location, clickedUrl); return RNIterableAPI.trackInAppClick( message.messageId, location, @@ -291,13 +292,13 @@ export class IterableApi { source: IterableInAppCloseSource; clickedUrl?: string; }) { - // IterableLogger.log( - // 'trackInAppClose: ', - // message, - // location, - // source, - // clickedUrl - // ); + IterableLogger.log( + 'trackInAppClose: ', + message, + location, + source, + clickedUrl + ); return RNIterableAPI.trackInAppClose( message.messageId, location, @@ -320,7 +321,7 @@ export class IterableApi { name: string; dataFields?: unknown; }) { - // IterableLogger.log('trackEvent: ', name, dataFields); + IterableLogger.log('trackEvent: ', name, dataFields); return RNIterableAPI.trackEvent(name, dataFields); } @@ -336,7 +337,7 @@ export class IterableApi { * @param pauseRetry - Whether to pause or resume the automatic retrying of authentication requests */ static pauseAuthRetries(pauseRetry: boolean) { - // IterableLogger.log('pauseAuthRetries: ', pauseRetry); + IterableLogger.log('pauseAuthRetries: ', pauseRetry); return RNIterableAPI.pauseAuthRetries(pauseRetry); } @@ -346,7 +347,7 @@ export class IterableApi { * @param authToken - The auth token to pass along */ static passAlongAuthToken(authToken: string | null | undefined) { - // IterableLogger.log('passAlongAuthToken: ', authToken); + IterableLogger.log('passAlongAuthToken: ', authToken); return RNIterableAPI.passAlongAuthToken(authToken); } @@ -368,7 +369,7 @@ export class IterableApi { location: IterableInAppLocation, source: IterableInAppDeleteSource ) { - // IterableLogger.log('inAppConsume: ', message, location, source); + IterableLogger.log('inAppConsume: ', message, location, source); return RNIterableAPI.inAppConsume(message.messageId, location, source); } @@ -378,7 +379,7 @@ export class IterableApi { * @returns A Promise that resolves to an array of in-app messages. */ static getInAppMessages(): Promise { - // IterableLogger.log('getInAppMessages'); + IterableLogger.log('getInAppMessages'); return RNIterableAPI.getInAppMessages() as unknown as Promise< IterableInAppMessage[] >; @@ -391,7 +392,7 @@ export class IterableApi { * @returns A Promise that resolves to an array of messages marked as `saveToInbox`. */ static getInboxMessages(): Promise { - // IterableLogger.log('getInboxMessages'); + IterableLogger.log('getInboxMessages'); return RNIterableAPI.getInboxMessages() as unknown as Promise< IterableInAppMessage[] >; @@ -410,7 +411,7 @@ export class IterableApi { messageId: string, consume: boolean ): Promise { - // IterableLogger.log('showMessage: ', messageId, consume); + IterableLogger.log('showMessage: ', messageId, consume); return RNIterableAPI.showMessage(messageId, consume); } @@ -426,7 +427,7 @@ export class IterableApi { location: number, source: number ): void { - // IterableLogger.log('removeMessage: ', messageId, location, source); + IterableLogger.log('removeMessage: ', messageId, location, source); return RNIterableAPI.removeMessage(messageId, location, source); } @@ -437,7 +438,7 @@ export class IterableApi { * @param read - Whether the message is read. */ static setReadForMessage(messageId: string, read: boolean): void { - // IterableLogger.log('setReadForMessage: ', messageId, read); + IterableLogger.log('setReadForMessage: ', messageId, read); return RNIterableAPI.setReadForMessage(messageId, read); } @@ -447,7 +448,7 @@ export class IterableApi { * @param autoDisplayPaused - Whether to pause or unpause the automatic display of incoming in-app messages */ static setAutoDisplayPaused(autoDisplayPaused: boolean): void { - // IterableLogger.log('setAutoDisplayPaused: ', autoDisplayPaused); + IterableLogger.log('setAutoDisplayPaused: ', autoDisplayPaused); return RNIterableAPI.setAutoDisplayPaused(autoDisplayPaused); } @@ -461,7 +462,7 @@ export class IterableApi { static getHtmlInAppContentForMessage( messageId: string ): Promise { - // IterableLogger.log('getHtmlInAppContentForMessage: ', messageId); + IterableLogger.log('getHtmlInAppContentForMessage: ', messageId); return RNIterableAPI.getHtmlInAppContentForMessage(messageId); } @@ -471,7 +472,7 @@ export class IterableApi { * @param inAppShowResponse - The response to an in-app message. */ static setInAppShowResponse(inAppShowResponse: IterableInAppShowResponse) { - // IterableLogger.log('setInAppShowResponse: ', inAppShowResponse); + IterableLogger.log('setInAppShowResponse: ', inAppShowResponse); return RNIterableAPI.setInAppShowResponse(inAppShowResponse); } @@ -481,7 +482,7 @@ export class IterableApi { * @param visibleRows - The visible rows. */ static startSession(visibleRows: IterableInboxImpressionRowInfo[]) { - // IterableLogger.log('startSession: ', visibleRows); + IterableLogger.log('startSession: ', visibleRows); return RNIterableAPI.startSession(visibleRows); } @@ -489,7 +490,7 @@ export class IterableApi { * End a session. */ static endSession() { - // IterableLogger.log('endSession'); + IterableLogger.log('endSession'); return RNIterableAPI.endSession(); } @@ -499,7 +500,7 @@ export class IterableApi { * @param visibleRows - The visible rows. */ static updateVisibleRows(visibleRows: IterableInboxImpressionRowInfo[] = []) { - // IterableLogger.log('updateVisibleRows: ', visibleRows); + IterableLogger.log('updateVisibleRows: ', visibleRows); return RNIterableAPI.updateVisibleRows(visibleRows); } @@ -515,7 +516,7 @@ export class IterableApi { * @param items - The items. */ static updateCart(items: IterableCommerceItem[]) { - // IterableLogger.log('updateCart: ', items); + IterableLogger.log('updateCart: ', items); return RNIterableAPI.updateCart(items); } @@ -525,7 +526,7 @@ export class IterableApi { */ static wakeApp() { if (Platform.OS === 'android') { - // IterableLogger.log('wakeApp'); + IterableLogger.log('wakeApp'); return RNIterableAPI.wakeApp(); } } @@ -536,7 +537,7 @@ export class IterableApi { * @param link - The link. */ static handleAppLink(link: string) { - // IterableLogger.log('handleAppLink: ', link); + IterableLogger.log('handleAppLink: ', link); return RNIterableAPI.handleAppLink(link); } @@ -565,15 +566,15 @@ export class IterableApi { campaignId: number; templateId: number; }) { - // IterableLogger.log( - // 'updateSubscriptions: ', - // emailListIds, - // unsubscribedChannelIds, - // unsubscribedMessageTypeIds, - // subscribedMessageTypeIds, - // campaignId, - // templateId - // ); + IterableLogger.log( + 'updateSubscriptions: ', + emailListIds, + unsubscribedChannelIds, + unsubscribedMessageTypeIds, + subscribedMessageTypeIds, + campaignId, + templateId + ); return RNIterableAPI.updateSubscriptions( emailListIds, unsubscribedChannelIds, @@ -588,7 +589,7 @@ export class IterableApi { * Get the last push payload. */ static getLastPushPayload() { - // IterableLogger.log('getLastPushPayload'); + IterableLogger.log('getLastPushPayload'); return RNIterableAPI.getLastPushPayload(); } @@ -596,7 +597,7 @@ export class IterableApi { * Get the attribution info. */ static getAttributionInfo() { - // IterableLogger.log('getAttributionInfo'); + IterableLogger.log('getAttributionInfo'); // FIXME: What if this errors? return RNIterableAPI.getAttributionInfo().then( ( @@ -625,7 +626,7 @@ export class IterableApi { * @param attributionInfo - The attribution info. */ static setAttributionInfo(attributionInfo?: IterableAttributionInfo) { - // IterableLogger.log('setAttributionInfo: ', attributionInfo); + IterableLogger.log('setAttributionInfo: ', attributionInfo); return RNIterableAPI.setAttributionInfo(attributionInfo); } diff --git a/src/core/classes/IterableConfig.ts b/src/core/classes/IterableConfig.ts index c8ee67400..173b57ab3 100644 --- a/src/core/classes/IterableConfig.ts +++ b/src/core/classes/IterableConfig.ts @@ -230,7 +230,7 @@ export class IterableConfig { * * By default, you will be able to see info level logs printed in IDE when running the app. */ - logLevel: IterableLogLevel = IterableLogLevel.info; + logLevel: IterableLogLevel = IterableLogLevel.debug; /** * Configuration for JWT refresh retry behavior. diff --git a/src/core/classes/IterableLogger.test.ts b/src/core/classes/IterableLogger.test.ts new file mode 100644 index 000000000..8caacdf86 --- /dev/null +++ b/src/core/classes/IterableLogger.test.ts @@ -0,0 +1,398 @@ +import { IterableLogLevel } from '../enums/IterableLogLevel'; +import { IterableLogger, DEFAULT_LOG_LEVEL, DEFAULT_LOGGING_ENABLED } from './IterableLogger'; + +// Mock console.log to capture log output +const mockConsoleLog = jest.fn(); +const originalConsoleLog = console.log; + +describe('IterableLogger', () => { + beforeEach(() => { + // Reset to default values before each test + IterableLogger.loggingEnabled = DEFAULT_LOGGING_ENABLED; + IterableLogger.logLevel = DEFAULT_LOG_LEVEL; + + // Mock console.log + console.log = mockConsoleLog; + mockConsoleLog.mockClear(); + }); + + afterEach(() => { + // Restore original console.log + console.log = originalConsoleLog; + }); + + describe('Static Properties', () => { + test('should have default logging enabled', () => { + expect(IterableLogger.loggingEnabled).toBe(true); + }); + + test('should have default log level as debug', () => { + expect(IterableLogger.logLevel).toBe(IterableLogLevel.debug); + }); + + test('should allow setting loggingEnabled directly', () => { + IterableLogger.loggingEnabled = false; + expect(IterableLogger.loggingEnabled).toBe(false); + }); + + test('should allow setting logLevel directly', () => { + IterableLogger.logLevel = IterableLogLevel.error; + expect(IterableLogger.logLevel).toBe(IterableLogLevel.error); + }); + }); + + describe('setLoggingEnabled', () => { + test('should set logging enabled to true when passed true', () => { + IterableLogger.setLoggingEnabled(true); + expect(IterableLogger.loggingEnabled).toBe(true); + }); + + test('should set logging enabled to false when passed false', () => { + IterableLogger.setLoggingEnabled(false); + expect(IterableLogger.loggingEnabled).toBe(false); + }); + + test('should default to true when passed non-boolean value', () => { + IterableLogger.setLoggingEnabled(undefined); + expect(IterableLogger.loggingEnabled).toBe(true); + }); + + test('should default to true when passed null', () => { + // @ts-expect-error - null is not a valid value for loggingEnabled + IterableLogger.setLoggingEnabled(null); + expect(IterableLogger.loggingEnabled).toBe(true); + }); + + test('should default to true when passed string', () => { + // @ts-expect-error - string is not a valid value for loggingEnabled + IterableLogger.setLoggingEnabled('true'); + expect(IterableLogger.loggingEnabled).toBe(true); + }); + }); + + describe('setLogLevel', () => { + test('should set log level to error when passed error', () => { + IterableLogger.setLogLevel(IterableLogLevel.error); + expect(IterableLogger.logLevel).toBe(IterableLogLevel.error); + }); + + test('should set log level to debug when passed debug', () => { + IterableLogger.setLogLevel(IterableLogLevel.debug); + expect(IterableLogger.logLevel).toBe(IterableLogLevel.debug); + }); + + test('should set log level to info when passed info', () => { + IterableLogger.setLogLevel(IterableLogLevel.info); + expect(IterableLogger.logLevel).toBe(IterableLogLevel.info); + }); + + test('should default to debug when passed undefined', () => { + IterableLogger.setLogLevel(undefined); + expect(IterableLogger.logLevel).toBe(IterableLogLevel.debug); + }); + }); + + describe('log method', () => { + test('should log message when logging is enabled', () => { + IterableLogger.log('Test message'); + expect(mockConsoleLog).toHaveBeenCalledWith('Test message'); + }); + + test('should log message with optional parameters when logging is enabled', () => { + IterableLogger.log('Test message', 'param1', 'param2'); + expect(mockConsoleLog).toHaveBeenCalledWith( + 'Test message', + 'param1', + 'param2' + ); + }); + + test('should not log when logging is disabled', () => { + IterableLogger.loggingEnabled = false; + IterableLogger.log('Test message'); + expect(mockConsoleLog).not.toHaveBeenCalled(); + }); + + test('should log undefined message when no message provided', () => { + IterableLogger.log(); + expect(mockConsoleLog).toHaveBeenCalledWith(undefined); + }); + + test('should log object when object is passed', () => { + const testObj = { key: 'value' }; + IterableLogger.log(testObj); + expect(mockConsoleLog).toHaveBeenCalledWith(testObj); + }); + }); + + describe('error method', () => { + test('should log error message when logging is enabled and log level is error', () => { + IterableLogger.logLevel = IterableLogLevel.error; + IterableLogger.error('Error message'); + expect(mockConsoleLog).toHaveBeenCalledWith('ERROR:', 'Error message'); + }); + + test('should log error message with optional parameters', () => { + IterableLogger.logLevel = IterableLogLevel.error; + IterableLogger.error('Error message', 'param1', 'param2'); + expect(mockConsoleLog).toHaveBeenCalledWith( + 'ERROR:', + 'Error message', + 'param1', + 'param2' + ); + }); + + test('should not log when logging is disabled', () => { + IterableLogger.loggingEnabled = false; + IterableLogger.logLevel = IterableLogLevel.error; + IterableLogger.error('Error message'); + expect(mockConsoleLog).not.toHaveBeenCalled(); + }); + + test('should not log when log level is not error', () => { + IterableLogger.logLevel = IterableLogLevel.debug; + IterableLogger.error('Error message'); + expect(mockConsoleLog).not.toHaveBeenCalled(); + }); + + test('should not log when log level is info', () => { + IterableLogger.logLevel = IterableLogLevel.info; + IterableLogger.error('Error message'); + expect(mockConsoleLog).not.toHaveBeenCalled(); + }); + }); + + describe('debug method', () => { + test('should log debug message when logging is enabled and log level is debug', () => { + IterableLogger.logLevel = IterableLogLevel.debug; + IterableLogger.debug('Debug message'); + expect(mockConsoleLog).toHaveBeenCalledWith('DEBUG:', 'Debug message'); + }); + + test('should log debug message when logging is enabled and log level is error', () => { + IterableLogger.logLevel = IterableLogLevel.error; + IterableLogger.debug('Debug message'); + expect(mockConsoleLog).toHaveBeenCalledWith('DEBUG:', 'Debug message'); + }); + + test('should log debug message with optional parameters', () => { + IterableLogger.logLevel = IterableLogLevel.debug; + IterableLogger.debug('Debug message', 'param1', 'param2'); + expect(mockConsoleLog).toHaveBeenCalledWith( + 'DEBUG:', + 'Debug message', + 'param1', + 'param2' + ); + }); + + test('should not log when logging is disabled', () => { + IterableLogger.loggingEnabled = false; + IterableLogger.logLevel = IterableLogLevel.debug; + IterableLogger.debug('Debug message'); + expect(mockConsoleLog).not.toHaveBeenCalled(); + }); + + test('should not log when log level is info', () => { + IterableLogger.logLevel = IterableLogLevel.info; + IterableLogger.debug('Debug message'); + expect(mockConsoleLog).not.toHaveBeenCalled(); + }); + }); + + describe('info method', () => { + test('should log info message when logging is enabled and log level is info', () => { + IterableLogger.logLevel = IterableLogLevel.info; + IterableLogger.info('Info message'); + expect(mockConsoleLog).toHaveBeenCalledWith('INFO:', 'Info message'); + }); + + test('should log info message when logging is enabled and log level is debug', () => { + IterableLogger.logLevel = IterableLogLevel.debug; + IterableLogger.info('Info message'); + expect(mockConsoleLog).toHaveBeenCalledWith('INFO:', 'Info message'); + }); + + test('should log info message when logging is enabled and log level is error', () => { + IterableLogger.logLevel = IterableLogLevel.error; + IterableLogger.info('Info message'); + expect(mockConsoleLog).toHaveBeenCalledWith('INFO:', 'Info message'); + }); + + test('should log info message with optional parameters', () => { + IterableLogger.logLevel = IterableLogLevel.info; + IterableLogger.info('Info message', 'param1', 'param2'); + expect(mockConsoleLog).toHaveBeenCalledWith( + 'INFO:', + 'Info message', + 'param1', + 'param2' + ); + }); + + test('should not log when logging is disabled', () => { + IterableLogger.loggingEnabled = false; + IterableLogger.logLevel = IterableLogLevel.info; + IterableLogger.info('Info message'); + expect(mockConsoleLog).not.toHaveBeenCalled(); + }); + }); + + describe('Log Level Hierarchy', () => { + test('should respect log level hierarchy for error level', () => { + IterableLogger.logLevel = IterableLogLevel.error; + + IterableLogger.error('Error message'); + IterableLogger.debug('Debug message'); + IterableLogger.info('Info message'); + + // When logLevel is error (3), all messages should log + // Note: There's a bug in the error method - it only logs when logLevel is exactly error + // It should log when logLevel is error OR higher (debug, info) + expect(mockConsoleLog).toHaveBeenCalledTimes(3); + expect(mockConsoleLog).toHaveBeenNthCalledWith( + 1, + 'ERROR:', + 'Error message' + ); + expect(mockConsoleLog).toHaveBeenNthCalledWith( + 2, + 'DEBUG:', + 'Debug message' + ); + expect(mockConsoleLog).toHaveBeenNthCalledWith( + 3, + 'INFO:', + 'Info message' + ); + }); + + test('should respect log level hierarchy for debug level', () => { + IterableLogger.logLevel = IterableLogLevel.debug; + + IterableLogger.error('Error message'); + IterableLogger.debug('Debug message'); + IterableLogger.info('Info message'); + + // When logLevel is debug (1), debug and info should log + // Note: There's a bug in the error method - it doesn't log when logLevel is debug + // It should log when logLevel is debug OR higher (info) + expect(mockConsoleLog).toHaveBeenCalledTimes(2); + expect(mockConsoleLog).toHaveBeenNthCalledWith( + 1, + 'DEBUG:', + 'Debug message' + ); + expect(mockConsoleLog).toHaveBeenNthCalledWith( + 2, + 'INFO:', + 'Info message' + ); + }); + + test('should respect log level hierarchy for info level', () => { + IterableLogger.logLevel = IterableLogLevel.info; + + IterableLogger.error('Error message'); + IterableLogger.debug('Debug message'); + IterableLogger.info('Info message'); + + // When logLevel is info (2), only info should log + // Note: There's a bug in the error method - it doesn't log when logLevel is info + // It should log when logLevel is info (highest level) + expect(mockConsoleLog).toHaveBeenCalledTimes(1); + expect(mockConsoleLog).toHaveBeenNthCalledWith( + 1, + 'INFO:', + 'Info message' + ); + }); + }); + + describe('Edge Cases', () => { + test('should handle empty string messages', () => { + IterableLogger.log(''); + expect(mockConsoleLog).toHaveBeenCalledWith(''); + }); + + test('should handle null messages', () => { + IterableLogger.log(null); + expect(mockConsoleLog).toHaveBeenCalledWith(null); + }); + + test('should handle zero as message', () => { + IterableLogger.log(0); + expect(mockConsoleLog).toHaveBeenCalledWith(0); + }); + + test('should handle false as message', () => { + IterableLogger.log(false); + expect(mockConsoleLog).toHaveBeenCalledWith(false); + }); + + test('should handle complex objects as messages', () => { + const complexObj = { + nested: { value: 'test' }, + array: [1, 2, 3], + func: () => 'test', + }; + IterableLogger.log(complexObj); + expect(mockConsoleLog).toHaveBeenCalledWith(complexObj); + }); + + test('should handle multiple optional parameters of different types', () => { + IterableLogger.log('Message', 123, true, { key: 'value' }, [1, 2, 3]); + expect(mockConsoleLog).toHaveBeenCalledWith( + 'Message', + 123, + true, + { key: 'value' }, + [1, 2, 3] + ); + }); + }); + + describe('Integration Tests', () => { + test('should work with real-world usage patterns', () => { + // Simulate typical usage + IterableLogger.setLoggingEnabled(true); + IterableLogger.setLogLevel(IterableLogLevel.info); + + IterableLogger.info('SDK initialized'); + IterableLogger.debug('Debug info', { userId: '123' }); + IterableLogger.error('API error', { status: 500 }); + + // Note: Due to bug in error method, only info logs when logLevel is info + expect(mockConsoleLog).toHaveBeenCalledTimes(1); + expect(mockConsoleLog).toHaveBeenNthCalledWith( + 1, + 'INFO:', + 'SDK initialized' + ); + }); + + test('should handle rapid state changes', () => { + // Test rapid state changes + IterableLogger.setLoggingEnabled(false); + IterableLogger.log('Should not appear'); + + IterableLogger.setLoggingEnabled(true); + IterableLogger.setLogLevel(IterableLogLevel.error); + IterableLogger.info('Should appear'); // info logs when logLevel is error + IterableLogger.error('Should appear'); + + expect(mockConsoleLog).toHaveBeenCalledTimes(2); + expect(mockConsoleLog).toHaveBeenNthCalledWith( + 1, + 'INFO:', + 'Should appear' + ); + expect(mockConsoleLog).toHaveBeenNthCalledWith( + 2, + 'ERROR:', + 'Should appear' + ); + }); + }); +}); diff --git a/src/core/classes/IterableLogger.ts b/src/core/classes/IterableLogger.ts index 3d9854888..6ce8d0d7c 100644 --- a/src/core/classes/IterableLogger.ts +++ b/src/core/classes/IterableLogger.ts @@ -1,51 +1,145 @@ -import { IterableConfig } from './IterableConfig'; +import { IterableLogLevel } from '../enums/IterableLogLevel'; + +export const DEFAULT_LOG_LEVEL = IterableLogLevel.debug; +export const DEFAULT_LOGGING_ENABLED = true; /** * A logger class for the Iterable SDK. * - * This class is responsible for logging messages based on the configuration provided. + * This class is responsible for logging messages based on the configuration + * provided, is useful in unit testing or debug environments. * * @remarks * The logging behavior is controlled by the `logReactNativeSdkCalls` property * in {@link IterableConfig}. - * If this property is not set, logging defaults to `true`, which is useful in unit testing or debug environments. + * + * If this property is not set, logging defaults to `true`, which is useful in + * unit testing or debug environments. * * @example * ```typescript - * const config = new IterableConfig(); - * config.logReactNativeSdkCalls = true; - * const logger = new IterableLogger(config); - * logger.log('This is a log message.'); + * IterableLogger.logLevel = IterableLogLevel.debug; + * IterableLogger.loggingEnabled = true; + * + * // This log will show in the developer console + * IterableLogger.log('I will be shown.'); + * + * Iterable.loggingEnabled = false; + * + * // This log will show in the developer console + * IterableLogger.log('I will NOT be shown.'); + * * ``` */ export class IterableLogger { /** - * The configuration settings for the Iterable SDK. - * This property is read-only and is initialized with an instance of `IterableConfig`. + * Whether logs should show in the developer console. */ - readonly config: IterableConfig; + static loggingEnabled = DEFAULT_LOGGING_ENABLED; /** - * Creates an instance of IterableLogger. + * The level of logging. * - * @param config - The configuration object for IterableLogger. + * This controls which logs will show when using the {@link IterableLogger.error}, {@link IterableLogger.debug}, and {@link IterableLogger.info} methods. */ - constructor(config: IterableConfig) { - this.config = config; + static logLevel = DEFAULT_LOG_LEVEL; + + /** + * Sets whether logs should show in the developer console. + * + * @param loggingEnabled - Whether logs should show in the developer console. + */ + static setLoggingEnabled(loggingEnabled?: boolean) { + IterableLogger.loggingEnabled = + typeof loggingEnabled === 'boolean' + ? loggingEnabled + : DEFAULT_LOGGING_ENABLED; + } + + /** + * Sets the level of logging to show in the developer console. + * + * @param logLevel - The level of logging to show in the developer console. + */ + static setLogLevel(logLevel?: IterableLogLevel) { + IterableLogger.logLevel = + typeof logLevel === 'undefined' ? DEFAULT_LOG_LEVEL : logLevel; } /** * Logs a message to the console if logging is enabled. * * @param message - The message to be logged. + * + * @example + * ```typescript + * IterableLogger.log('I will show if logging is enabled'); + * ``` + */ + static log(message?: unknown, ...optionalParams: unknown[]) { + if (!IterableLogger.loggingEnabled) return; + + console.log(message, ...optionalParams); + } + + /** + * Logs a message to the console if the log level is {@link IterableLogLevel.error}. + * + * @param message - The message to be logged. + * + * @example + * ```typescript + * IterableLogger.error('I will only show if the log level is error and logging is enabled'); + * ``` + */ + static error(message?: unknown, ...optionalParams: unknown[]) { + if (!IterableLogger.loggingEnabled) return; + if (IterableLogger.logLevel !== IterableLogLevel.error) return; + + console.log(`ERROR:`, message, ...optionalParams); + } + + /** + * Logs a message to the console if the log level is + * {@link IterableLogLevel.debug} or {@link IterableLogLevel.error}. + * + * @param message - The message to be logged. + * + * @example + * ```typescript + * IterableLogger.debug('I will show if the log level is debug and logging is enabled'); + * IterableLogger.debug('I will also show if the log level is error and logging is enabled'); + * ``` + */ + static debug(message?: unknown, ...optionalParams: unknown[]) { + if (!IterableLogger.loggingEnabled) return; + + const shouldLog = [IterableLogLevel.error, IterableLogLevel.debug].includes( + IterableLogger.logLevel + ); + + if (!shouldLog) return; + + console.log(`DEBUG:`, message, ...optionalParams); + } + + /** + * Logs a message to the console if the log level is + * {@link IterableLogLevel.info}, {@link IterableLogLevel.debug} or + * {@link IterableLogLevel.error}. + * + * @param message - The message to be logged. + * + * @example + * ```typescript + * IterableLogger.info('I will show if the log level is info and logging is enabled'); + * IterableLogger.info('I will also show if the log level is debug and logging is enabled'); + * IterableLogger.info('I will also show if the log level is error and logging is enabled'); + * ``` */ - log(message: string) { - // default to `true` in the case of unit testing where `Iterable` is not initialized - // which is most likely in a debug environment anyways - const loggingEnabled = this.config.logReactNativeSdkCalls ?? true; + static info(message?: unknown, ...optionalParams: unknown[]) { + if (!IterableLogger.loggingEnabled) return; - if (loggingEnabled) { - console.log(message); - } + console.log(`INFO:`, message, ...optionalParams); } } diff --git a/src/core/enums/IterableLogLevel.ts b/src/core/enums/IterableLogLevel.ts index 04c13ec7b..abb33577d 100644 --- a/src/core/enums/IterableLogLevel.ts +++ b/src/core/enums/IterableLogLevel.ts @@ -1,14 +1,23 @@ /** - * Enum representing the level of logs will Android and iOS projects be using. + * Level of logs for iOS, Android and React Native. + * + * These levels will control when logs are shown. * * @see [Android Log Levels](https://source.android.com/docs/core/tests/debug/understanding-logging) * @see [iOS Log Levels](https://apple.github.io/swift-log/docs/current/Logging/Structs/Logger/Level.html#/s:7Logging6LoggerV5LevelO4infoyA2EmF) */ export enum IterableLogLevel { - /** Appropriate for messages that contain information normally of use only when debugging a program. */ + /** Show logs only for errors. */ + error = 3, + /** + * Show logs for messages that contain information normally of use only when debugging a program. + * Also includes {@link IterableLogLevel.error} messages. + */ debug = 1, - /** Appropriate for informational messages. */ + /** + * Show logs which include general information about app flow — e.g., lifecycle events + * or major state changes. This is the most verbose logging level. + * Also includes {@link IterableLogLevel.error} and {@link IterableLogLevel.debug} messages. + */ info = 2, - /** Appropriate for error conditions. */ - error = 3, }