diff --git a/src/batchUploader.ts b/src/batchUploader.ts index 9f456dad..b9b598d9 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 2b36a88d..e357a645 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 a88d1332..00000000 --- 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 00000000..6ec19c05 --- /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/logging/errorCodes.ts b/src/logging/errorCodes.ts new file mode 100644 index 00000000..ae99429b --- /dev/null +++ b/src/logging/errorCodes.ts @@ -0,0 +1,7 @@ +import { valueof } from '../utils'; + +export type ErrorCodes = valueof; + +export const ErrorCodes = { + UNHANDLED_EXCEPTION: 'UNHANDLED_EXCEPTION', +} as const; \ No newline at end of file diff --git a/src/logging/reportingLogger.ts b/src/logging/reportingLogger.ts new file mode 100644 index 00000000..9ee5b5ad --- /dev/null +++ b/src/logging/reportingLogger.ts @@ -0,0 +1,140 @@ + +import { ErrorCodes } from "./errorCodes"; +import { IWSDKError, WSDKErrorSeverity } from "./wsdk-error"; +import { FetchUploader, XHRUploader } from "../uploaders"; + +export interface IReportingLogger { + error(msg: string, code?: ErrorCodes, stack?: string): void; + warning(msg: string, code?: ErrorCodes): void; +} + +export class ReportingLogger implements IReportingLogger { + private readonly isEnabled: boolean; + private readonly reporter: string = 'mp-wsdk'; + private readonly integration: string = 'mp-wsdk'; + private readonly rateLimiter: IRateLimiter; + + constructor( + private baseUrl: string, + private readonly isFeatureFlagEnabled: boolean, + private readonly sdkVersion: string, + private readonly accountId: string, + private readonly roktLauncherInstanceGuid: string, + rateLimiter?: IRateLimiter, + ) { + this.isEnabled = this.isReportingEnabled(); + this.rateLimiter = rateLimiter ?? new RateLimiter(); + } + + public error(msg: string, code: ErrorCodes, name?: string, stack?: string) { + this.sendLog(WSDKErrorSeverity.ERROR, msg, code, name, stack); + }; + + public warning(msg: string, code: ErrorCodes, name?: string) { + this.sendLog(WSDKErrorSeverity.WARNING, msg, code, name); + }; + + private sendLog( + severity: WSDKErrorSeverity, + msg: string, + code: ErrorCodes, + name?: string, + stack?: string + ): void { + if(!this.canSendLog(severity)) + return; + + const wsdkError: IWSDKError = { + name: name, + message: msg, + additionalInformation: { + message: msg, + version: this.sdkVersion, + url: window?.location?.href, + }, + severity: severity, + code: code, + stack: stack, + reporter: this.reporter, + integration: this.integration, + }; + + this.sendLogToServer(wsdkError); + } + + private isReportingEnabled(): boolean { + return ( + this.isRoktDomainPresent() && + (this.isFeatureFlagEnabled || + this.isDebugModeEnabled()) + ); + } + + private isRoktDomainPresent(): boolean { + return Boolean(window['ROKT_DOMAIN']); + } + + private isDebugModeEnabled(): boolean { + return ( + window. + location?. + search?. + toLowerCase()?. + includes('mp_enable_logging=true') ?? false + ); + } + + private canSendLog(severity: WSDKErrorSeverity): boolean { + return this.isEnabled && !this.isRateLimited(severity); + } + + private isRateLimited(severity: WSDKErrorSeverity): boolean { + return this.rateLimiter.incrementAndCheck(severity); + } + + private sendLogToServer(wsdkError: IWSDKError) { + const uploadUrl = `${this.baseUrl}/v1/log`; + const uploader = window.fetch + ? new FetchUploader(uploadUrl) + : new XHRUploader(uploadUrl); + + const headers = { + Accept: 'text/plain;charset=UTF-8', + 'Content-Type': 'text/plain;charset=UTF-8', + 'rokt-launcher-instance-guid': this.roktLauncherInstanceGuid, + }; + + if (this.accountId) { + headers['rokt-account-id'] = this.accountId; + } + + uploader.upload({ + headers: headers, + method: 'POST', + body: JSON.stringify(wsdkError), + }); + }; +} + +export interface IRateLimiter { + incrementAndCheck(severity: WSDKErrorSeverity): boolean; +} + +export class RateLimiter implements IRateLimiter { + private readonly rateLimits: Map = new Map([ + [WSDKErrorSeverity.ERROR, 10], + [WSDKErrorSeverity.WARNING, 10], + [WSDKErrorSeverity.INFO, 10], + ]); + private logCount: Map = new Map(); + + public incrementAndCheck(severity: WSDKErrorSeverity): boolean { + const count = this.logCount.get(severity) || 0; + const limit = this.rateLimits.get(severity) || 10; + + const newCount = count + 1; + this.logCount.set(severity, newCount); + + return newCount > limit; + } +} \ No newline at end of file diff --git a/src/logging/wsdk-error.ts b/src/logging/wsdk-error.ts new file mode 100644 index 00000000..c51f2fa0 --- /dev/null +++ b/src/logging/wsdk-error.ts @@ -0,0 +1,20 @@ +import { ErrorCodes } from "./errorCodes"; + +export type WSDKErrorSeverity = (typeof WSDKErrorSeverity)[keyof typeof WSDKErrorSeverity]; +export const WSDKErrorSeverity = { + ERROR: 'ERROR', + INFO: 'INFO', + WARNING: 'WARNING', +} as const; + +export interface IWSDKError { + name: string; + message: string; + stack?: string; + code?: ErrorCodes; + reporter?: string; + integration?: string; + severity?: WSDKErrorSeverity; + additionalInformation?: Record; + handled?: boolean; +} \ No newline at end of file diff --git a/src/mp-instance.ts b/src/mp-instance.ts index fd32ef2b..1448e494 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 6e73ebc4..e1dc97bb 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 @@ -307,8 +314,10 @@ export interface SDKInitConfig identityCallback?: IdentityCallback; launcherOptions?: IRoktLauncherOptions; + isWebSdkLoggingEnabled?: boolean; rq?: Function[] | any[]; + logger?: IConsoleLogger; } export interface DataPlanConfig { diff --git a/src/uploaders.ts b/src/uploaders.ts index e28606c4..c4e6b7e4 100644 --- a/src/uploaders.ts +++ b/src/uploaders.ts @@ -5,6 +5,7 @@ export interface IFetchPayload { headers: { Accept: string; 'Content-Type'?: string; + 'rokt-account-id'?: string; }; body?: string; } diff --git a/test/jest/logger.spec.ts b/test/jest/logger.spec.ts new file mode 100644 index 00000000..def3a637 --- /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/jest/reportingLogger.spec.ts b/test/jest/reportingLogger.spec.ts new file mode 100644 index 00000000..3f2344f3 --- /dev/null +++ b/test/jest/reportingLogger.spec.ts @@ -0,0 +1,154 @@ +import { IRateLimiter, RateLimiter, ReportingLogger } from '../../src/logging/reportingLogger'; +import { WSDKErrorSeverity } from '../../src/logging/wsdk-error'; +import { ErrorCodes } from '../../src/logging/errorCodes'; + +describe('ReportingLogger', () => { + let logger: ReportingLogger; + const baseUrl = 'https://test-url.com'; + const sdkVersion = '1.2.3'; + let mockFetch: jest.Mock; + const accountId = '1234567890'; + const roktLauncherInstanceGuid = '1234567890'; + beforeEach(() => { + mockFetch = jest.fn().mockResolvedValue({ ok: true }); + global.fetch = mockFetch; + + delete (globalThis as any).location; + (globalThis as any).location = { + href: 'https://e.com', + search: '' + }; + + Object.assign(globalThis, { + navigator: { userAgent: 'ua' }, + mParticle: { config: { isWebSdkLoggingEnabled: true } }, + ROKT_DOMAIN: 'set', + fetch: mockFetch + }); + logger = new ReportingLogger(baseUrl, true, sdkVersion, accountId, roktLauncherInstanceGuid); + }); + + afterEach(() => { + jest.clearAllMocks(); + delete (window as any).ROKT_DOMAIN; + delete (window as any).mParticle; + }); + + it('sends error logs with correct params', () => { + logger.error('msg', ErrorCodes.UNHANDLED_EXCEPTION, 'stack'); + expect(mockFetch).toHaveBeenCalled(); + const fetchCall = mockFetch.mock.calls[0]; + expect(fetchCall[0]).toContain('/v1/log'); + const body = JSON.parse(fetchCall[1].body); + expect(body).toMatchObject({ + severity: WSDKErrorSeverity.ERROR, + code: ErrorCodes.UNHANDLED_EXCEPTION, + }); + expect(fetchCall[1].headers).toMatchObject({ + 'Accept': 'text/plain;charset=UTF-8', + 'Content-Type': 'text/plain;charset=UTF-8', + 'rokt-launcher-instance-guid': roktLauncherInstanceGuid, + 'rokt-account-id': accountId, + }); + }); + + it('sends warning logs with correct params', () => { + logger.warning('warn', ErrorCodes.UNHANDLED_EXCEPTION); + expect(mockFetch).toHaveBeenCalled(); + const fetchCall = mockFetch.mock.calls[0]; + expect(fetchCall[0]).toContain('/v1/log'); + const body = JSON.parse(fetchCall[1].body); + expect(body).toMatchObject({ + severity: WSDKErrorSeverity.WARNING, + code: ErrorCodes.UNHANDLED_EXCEPTION, + }); + expect(fetchCall[1].headers).toMatchObject({ + 'Accept': 'text/plain;charset=UTF-8', + 'Content-Type': 'text/plain;charset=UTF-8', + 'rokt-launcher-instance-guid': roktLauncherInstanceGuid, + 'rokt-account-id': accountId, + }); + }); + + it('does not log if ROKT_DOMAIN missing', () => { + delete (globalThis as any).ROKT_DOMAIN; + logger = new ReportingLogger(baseUrl, true, sdkVersion, accountId, roktLauncherInstanceGuid); + logger.error('x', ErrorCodes.UNHANDLED_EXCEPTION); + expect(mockFetch).not.toHaveBeenCalled(); + }); + + it('does not log if feature flag and debug mode off', () => { + window.location.search = ''; + logger = new ReportingLogger(baseUrl, false, sdkVersion, accountId, roktLauncherInstanceGuid); + logger.error('x', ErrorCodes.UNHANDLED_EXCEPTION); + expect(mockFetch).not.toHaveBeenCalled(); + }); + + it('logs if debug mode on even if feature flag off', () => { + window.mParticle.config.isWebSdkLoggingEnabled = false; + window.location.search = '?mp_enable_logging=true'; + logger = new ReportingLogger(baseUrl, false, sdkVersion, accountId, roktLauncherInstanceGuid); + logger.error('x', ErrorCodes.UNHANDLED_EXCEPTION); + expect(mockFetch).toHaveBeenCalled(); + }); + + it('rate limits after 3 errors', () => { + let count = 0; + const mockRateLimiter: IRateLimiter = { + incrementAndCheck: jest.fn().mockImplementation((severity) => { + return ++count > 3; + }), + }; + logger = new ReportingLogger(baseUrl, true, sdkVersion, accountId, roktLauncherInstanceGuid, mockRateLimiter); + + for (let i = 0; i < 5; i++) logger.error('err', ErrorCodes.UNHANDLED_EXCEPTION); + expect(mockFetch).toHaveBeenCalledTimes(3); + }); + + it('does not send account id when accountId is empty', () => { + logger = new ReportingLogger(baseUrl, true, sdkVersion, '', roktLauncherInstanceGuid); + logger.error('msg', ErrorCodes.UNHANDLED_EXCEPTION); + expect(mockFetch).toHaveBeenCalled(); + const fetchCall = mockFetch.mock.calls[0]; + expect(fetchCall[1].headers['rokt-account-id']).toBeUndefined(); + }); +}); + +describe('RateLimiter', () => { + let rateLimiter: RateLimiter; + beforeEach(() => { + rateLimiter = new RateLimiter(); + }); + + it('allows up to 10 error logs then rate limits', () => { + for (let i = 0; i < 10; i++) { + expect(rateLimiter.incrementAndCheck(WSDKErrorSeverity.ERROR)).toBe(false); + } + expect(rateLimiter.incrementAndCheck(WSDKErrorSeverity.ERROR)).toBe(true); + expect(rateLimiter.incrementAndCheck(WSDKErrorSeverity.ERROR)).toBe(true); + }); + + it('allows up to 10 warning logs then rate limits', () => { + for (let i = 0; i < 10; i++) { + expect(rateLimiter.incrementAndCheck(WSDKErrorSeverity.WARNING)).toBe(false); + } + expect(rateLimiter.incrementAndCheck(WSDKErrorSeverity.WARNING)).toBe(true); + expect(rateLimiter.incrementAndCheck(WSDKErrorSeverity.WARNING)).toBe(true); + }); + + it('allows up to 10 info logs then rate limits', () => { + for (let i = 0; i < 10; i++) { + expect(rateLimiter.incrementAndCheck(WSDKErrorSeverity.INFO)).toBe(false); + } + expect(rateLimiter.incrementAndCheck(WSDKErrorSeverity.INFO)).toBe(true); + expect(rateLimiter.incrementAndCheck(WSDKErrorSeverity.INFO)).toBe(true); + }); + + it('tracks rate limits independently per severity', () => { + for (let i = 0; i < 10; i++) { + rateLimiter.incrementAndCheck(WSDKErrorSeverity.ERROR); + } + expect(rateLimiter.incrementAndCheck(WSDKErrorSeverity.ERROR)).toBe(true); + expect(rateLimiter.incrementAndCheck(WSDKErrorSeverity.WARNING)).toBe(false); + }); +}); diff --git a/test/src/tests-audience-manager.ts b/test/src/tests-audience-manager.ts index 9bee9a8e..db6111fc 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 681a0b0d..7d1a9819 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 {