From 4cc5aa8cae87b20864ca6ecd459e513cfbe27248 Mon Sep 17 00:00:00 2001 From: gtamanaha Date: Tue, 25 Nov 2025 13:13:01 -0300 Subject: [PATCH 01/19] feat: Migrate Logger to Typescript (#1120) --- src/batchUploader.ts | 6 +- src/identityApiClient.ts | 23 +++-- src/logger.js | 61 ------------- src/logger.ts | 66 ++++++++++++++ src/mp-instance.ts | 3 +- src/sdkRuntimeModels.ts | 10 ++- test/jest/logger.spec.ts | 136 +++++++++++++++++++++++++++++ test/src/tests-audience-manager.ts | 2 +- test/src/tests-batchUploader.ts | 2 +- 9 files changed, 229 insertions(+), 80 deletions(-) delete mode 100644 src/logger.js create mode 100644 src/logger.ts create mode 100644 test/jest/logger.spec.ts 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 { From 7358464f4e10821a378e4ad88019ba39e16da9cd Mon Sep 17 00:00:00 2001 From: Guillermo Tamanaha Date: Mon, 24 Nov 2025 16:39:49 -0300 Subject: [PATCH 02/19] feat: Create Logging Service --- src/apiClient.ts | 24 ++++++ src/logging/errorCodes.ts | 5 ++ src/logging/logMessage.ts | 0 src/logging/logRequest.ts | 21 +++++ src/logging/reportingLogger.ts | 128 ++++++++++++++++++++++++++++++ src/mp-instance.ts | 18 +++-- src/sdkRuntimeModels.ts | 1 + test/jest/reportingLogger.spec.ts | 116 +++++++++++++++++++++++++++ 8 files changed, 305 insertions(+), 8 deletions(-) create mode 100644 src/logging/errorCodes.ts create mode 100644 src/logging/logMessage.ts create mode 100644 src/logging/logRequest.ts create mode 100644 src/logging/reportingLogger.ts create mode 100644 test/jest/reportingLogger.spec.ts diff --git a/src/apiClient.ts b/src/apiClient.ts index 5e0a2fe76..f18d9f427 100644 --- a/src/apiClient.ts +++ b/src/apiClient.ts @@ -10,6 +10,7 @@ import { IMParticleUser, ISDKUserAttributes } from './identity-user-interfaces'; import { AsyncUploader, FetchUploader, XHRUploader } from './uploaders'; import { IMParticleWebSDKInstance } from './mp-instance'; import { appendUserInfo } from './user-utils'; +import { LogRequest } from './logging/logRequest'; export interface IAPIClient { uploader: BatchUploader | null; @@ -27,6 +28,7 @@ export interface IAPIClient { forwarder: MPForwarder, event: IUploadObject ) => void; + sendLogToServer: (log: LogRequest) => void; } export interface IForwardingStatsData { @@ -231,4 +233,26 @@ export default function APIClient( } } }; + + this.sendLogToServer = function(logRequest: LogRequest) { + const baseUrl = mpInstance._Helpers.createServiceUrl( + mpInstance._Store.SDKConfig.v2SecureServiceUrl, + mpInstance._Store.devToken + ); + const uploadUrl = `apps.stage.rokt.com/v1/log/v1/log`; + // const uploadUrl = `${baseUrl}/v1/log`; + + const uploader = window.fetch + ? new FetchUploader(uploadUrl) + : new XHRUploader(uploadUrl); + + uploader.upload({ + method: 'POST', + headers: { + Accept: 'text/plain;charset=UTF-8', + 'Content-Type': 'text/plain;charset=UTF-8', + }, + body: JSON.stringify(logRequest), + }); + }; } diff --git a/src/logging/errorCodes.ts b/src/logging/errorCodes.ts new file mode 100644 index 000000000..c45d2ef75 --- /dev/null +++ b/src/logging/errorCodes.ts @@ -0,0 +1,5 @@ +export type ErrorCodes = (typeof ErrorCodes)[keyof typeof ErrorCodes]; + +export const ErrorCodes = { + UNHANDLED_EXCEPTION: 'UNHANDLED_EXCEPTION', +} as const; \ No newline at end of file diff --git a/src/logging/logMessage.ts b/src/logging/logMessage.ts new file mode 100644 index 000000000..e69de29bb diff --git a/src/logging/logRequest.ts b/src/logging/logRequest.ts new file mode 100644 index 000000000..8cb248e14 --- /dev/null +++ b/src/logging/logRequest.ts @@ -0,0 +1,21 @@ +import { ErrorCodes } from "./errorCodes"; + +export enum LogRequestSeverity { + Error = 'error', + Warning = 'warning', + Info = 'info', +} + +export interface LogRequest { + additionalInformation: { + message: string; + version: string; + }; + severity: LogRequestSeverity; + code: ErrorCodes; + url: string; + deviceInfo: string; + stackTrace: string; + reporter: string; + integration: string; +} \ No newline at end of file diff --git a/src/logging/reportingLogger.ts b/src/logging/reportingLogger.ts new file mode 100644 index 000000000..d30a74ee1 --- /dev/null +++ b/src/logging/reportingLogger.ts @@ -0,0 +1,128 @@ +import { LogLevelType, SDKLoggerApi } from "../sdkRuntimeModels"; +import { IAPIClient } from "../apiClient"; +import { ErrorCodes } from "./errorCodes"; +import { LogRequest, LogRequestSeverity } from "./logRequest"; + +export interface IReportingLogger { + error(msg: string, code?: ErrorCodes, stackTrace?: string): void; + warning(msg: string, code?: ErrorCodes): void; +} + +export class ReportingLogger implements IReportingLogger { + private readonly isEnabled: boolean; + private readonly apiClient: IAPIClient; + private readonly reporter: string = 'mp-wsdk'; + private readonly integration: string = 'mp-wsdk'; + private readonly rateLimiter: RateLimiter = new RateLimiter(); + + constructor( + apiClient: IAPIClient, + private readonly sdkVersion: string, + ) { + this.isEnabled = this.isReportingEnabled(); + this.apiClient = apiClient; + this.rateLimiter = new RateLimiter(); + } + + public error(msg: string, code?: ErrorCodes, stackTrace?: string) { + this.sendLog(LogRequestSeverity.Error, msg, code ?? ErrorCodes.UNHANDLED_EXCEPTION, stackTrace); + }; + + public warning(msg: string, code?: ErrorCodes) { + this.sendLog(LogRequestSeverity.Warning, msg, code ?? ErrorCodes.UNHANDLED_EXCEPTION); + }; + + private sendLog( + severity: LogRequestSeverity, + msg: string, + code: ErrorCodes, + stackTrace?: string + ): void { + if(!this.canSendLog(severity)) + return; + + const logRequest: LogRequest = { + additionalInformation: { + message: msg, + version: this.sdkVersion, + }, + severity: severity, + code: code, + url: this.getUrl(), + deviceInfo: this.getUserAgent(), + stackTrace: stackTrace ?? '', + reporter: this.reporter, + integration: this.integration, + }; + + this.apiClient.sendLogToServer(logRequest); + } + + private isReportingEnabled() { + return ( + this.isRoktDomainPresent() && + (this.isFeatureFlagEnabled() || + this.isDebugModeEnabled()) + ); + } + + private isRoktDomainPresent() { + return window['ROKT_DOMAIN']; + } + + private isFeatureFlagEnabled() { + return window. + mParticle?. + config?. + isWebSdkLoggingEnabled ?? false; + } + + private isDebugModeEnabled() { + return ( + window. + location?. + search?. + toLowerCase()?. + includes('mp_enable_logging=true') ?? false + ); + } + + private canSendLog(severity: LogRequestSeverity): boolean { + return this.isEnabled && !this.isRateLimited(severity); + } + + private isRateLimited(severity: LogRequestSeverity): boolean { + return this.rateLimiter.incrementAndCheck(severity); + } + + private getUrl(): string { + return window.location.href; + } + + private getUserAgent(): string { + return window.navigator.userAgent; + } +} + +export interface IRateLimiter { + incrementAndCheck(severity: LogRequestSeverity): boolean; +} + +export class RateLimiter implements IRateLimiter { + private readonly rateLimits: Map = new Map([ + [LogRequestSeverity.Error, 10], + [LogRequestSeverity.Warning, 10], + [LogRequestSeverity.Info, 10], + ]); + private logCount: Map = new Map(); + + public incrementAndCheck(severity: LogRequestSeverity): 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/mp-instance.ts b/src/mp-instance.ts index 1448e4941..e973f6eb2 100644 --- a/src/mp-instance.ts +++ b/src/mp-instance.ts @@ -162,7 +162,10 @@ export default function mParticleInstance(this: IMParticleWebSDKInstance, instan ); } - runPreConfigFetchInitialization(this, apiKey, config); + const kitBlocker = createKitBlocker(config, this); + runPreConfigFetchInitialization(this, apiKey, config, kitBlocker); + debugger; + this.Logger.error('gt error test'); // config code - Fetch config when requestConfig = true, otherwise, proceed with SDKInitialization // Since fetching the configuration is asynchronous, we must pass completeSDKInitialization @@ -185,10 +188,10 @@ export default function mParticleInstance(this: IMParticleWebSDKInstance, instan result ); - completeSDKInitialization(apiKey, mergedConfig, this); + completeSDKInitialization(apiKey, mergedConfig, this, kitBlocker); }); } else { - completeSDKInitialization(apiKey, config, this); + completeSDKInitialization(apiKey, config, this, kitBlocker); } } else { console.error( @@ -1361,11 +1364,9 @@ export default function mParticleInstance(this: IMParticleWebSDKInstance, instan } // Some (server) config settings need to be returned before they are set on SDKConfig in a self hosted environment -function completeSDKInitialization(apiKey, config, mpInstance) { - const kitBlocker = createKitBlocker(config, mpInstance); +function completeSDKInitialization(apiKey, config, mpInstance, kitBlocker: KitBlocker) { const { getFeatureFlag } = mpInstance._Helpers; - mpInstance._APIClient = new APIClient(mpInstance, kitBlocker); mpInstance._Forwarders = new Forwarders(mpInstance, kitBlocker); mpInstance._Store.processConfig(config); @@ -1551,8 +1552,9 @@ function createIdentityCache(mpInstance) { return new LocalStorageVault(cacheKey, { logger: mpInstance.Logger }); } -function runPreConfigFetchInitialization(mpInstance, apiKey, config) { - mpInstance.Logger = new Logger(config); +function runPreConfigFetchInitialization(mpInstance, apiKey, config, kitBlocker: KitBlocker) { + mpInstance._APIClient = new APIClient(mpInstance, kitBlocker); + mpInstance.Logger = new Logger(config, mpInstance._APIClient); mpInstance._Store = new Store(config, mpInstance, apiKey); window.mParticle.Store = mpInstance._Store; mpInstance.Logger.verbose(StartingInitialization); diff --git a/src/sdkRuntimeModels.ts b/src/sdkRuntimeModels.ts index 579c9832e..e1dc97bb3 100644 --- a/src/sdkRuntimeModels.ts +++ b/src/sdkRuntimeModels.ts @@ -314,6 +314,7 @@ export interface SDKInitConfig identityCallback?: IdentityCallback; launcherOptions?: IRoktLauncherOptions; + isWebSdkLoggingEnabled?: boolean; rq?: Function[] | any[]; logger?: IConsoleLogger; diff --git a/test/jest/reportingLogger.spec.ts b/test/jest/reportingLogger.spec.ts new file mode 100644 index 000000000..7d531662b --- /dev/null +++ b/test/jest/reportingLogger.spec.ts @@ -0,0 +1,116 @@ +import { RateLimiter, ReportingLogger } from '../../src/logging/reportingLogger'; +import { LogRequestSeverity } from '../../src/logging/logRequest'; +import { ErrorCodes } from '../../src/logging/errorCodes'; + +describe('ReportingLogger', () => { + let apiClient: any; + let logger: ReportingLogger; + const sdkVersion = '1.2.3'; + + beforeEach(() => { + apiClient = { sendLogToServer: jest.fn() }; + + // Mock location object to allow modifying search property + delete (window as any).location; + (window as any).location = { + href: 'https://e.com', + search: '' + }; + + Object.assign(window, { + navigator: { userAgent: 'ua' }, + mParticle: { config: { isWebSdkLoggingEnabled: true } }, + ROKT_DOMAIN: 'set' + }); + logger = new ReportingLogger(apiClient, sdkVersion); + }); + + 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(apiClient.sendLogToServer).toHaveBeenCalledWith(expect.objectContaining({ + severity: LogRequestSeverity.Error, + code: ErrorCodes.UNHANDLED_EXCEPTION, + stackTrace: 'stack' + })); + }); + + it('sends warning logs with correct params', () => { + logger.warning('warn'); + expect(apiClient.sendLogToServer).toHaveBeenCalledWith(expect.objectContaining({ + severity: LogRequestSeverity.Warning + })); + }); + + it('does not log if ROKT_DOMAIN missing', () => { + delete (window as any).ROKT_DOMAIN; + logger = new ReportingLogger(apiClient, sdkVersion); + logger.error('x'); + expect(apiClient.sendLogToServer).not.toHaveBeenCalled(); + }); + + it('does not log if feature flag and debug mode off', () => { + window.mParticle.config.isWebSdkLoggingEnabled = false; + window.location.search = ''; + logger = new ReportingLogger(apiClient, sdkVersion); + logger.error('x'); + expect(apiClient.sendLogToServer).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(apiClient, sdkVersion); + logger.error('x'); + expect(apiClient.sendLogToServer).toHaveBeenCalled(); + }); + + it('rate limits after 10 errors', () => { + for (let i = 0; i < 12; i++) logger.error('err'); + expect(apiClient.sendLogToServer).toHaveBeenCalledTimes(10); + }); +}); + +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(LogRequestSeverity.Error)).toBe(false); + } + expect(rateLimiter.incrementAndCheck(LogRequestSeverity.Error)).toBe(true); + expect(rateLimiter.incrementAndCheck(LogRequestSeverity.Error)).toBe(true); + }); + + it('allows up to 10 warning logs then rate limits', () => { + for (let i = 0; i < 10; i++) { + expect(rateLimiter.incrementAndCheck(LogRequestSeverity.Warning)).toBe(false); + } + expect(rateLimiter.incrementAndCheck(LogRequestSeverity.Warning)).toBe(true); + expect(rateLimiter.incrementAndCheck(LogRequestSeverity.Warning)).toBe(true); + }); + + it('allows up to 10 info logs then rate limits', () => { + for (let i = 0; i < 10; i++) { + expect(rateLimiter.incrementAndCheck(LogRequestSeverity.Info)).toBe(false); + } + expect(rateLimiter.incrementAndCheck(LogRequestSeverity.Info)).toBe(true); + expect(rateLimiter.incrementAndCheck(LogRequestSeverity.Info)).toBe(true); + }); + + it('tracks rate limits independently per severity', () => { + for (let i = 0; i < 10; i++) { + rateLimiter.incrementAndCheck(LogRequestSeverity.Error); + } + expect(rateLimiter.incrementAndCheck(LogRequestSeverity.Error)).toBe(true); + expect(rateLimiter.incrementAndCheck(LogRequestSeverity.Warning)).toBe(false); + }); +}); From 7abe3febd2a0c8e845202a098b29527ef44960e0 Mon Sep 17 00:00:00 2001 From: Guillermo Tamanaha Date: Tue, 25 Nov 2025 13:29:55 -0300 Subject: [PATCH 03/19] fix(apiClient): Update upload URL construction in APIClient and clean up imports in reportingLogger --- src/apiClient.ts | 5 ++--- src/logging/reportingLogger.ts | 1 - 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/apiClient.ts b/src/apiClient.ts index f18d9f427..05b39da56 100644 --- a/src/apiClient.ts +++ b/src/apiClient.ts @@ -239,9 +239,8 @@ export default function APIClient( mpInstance._Store.SDKConfig.v2SecureServiceUrl, mpInstance._Store.devToken ); - const uploadUrl = `apps.stage.rokt.com/v1/log/v1/log`; - // const uploadUrl = `${baseUrl}/v1/log`; - + + const uploadUrl = `${baseUrl}/v1/log`; const uploader = window.fetch ? new FetchUploader(uploadUrl) : new XHRUploader(uploadUrl); diff --git a/src/logging/reportingLogger.ts b/src/logging/reportingLogger.ts index d30a74ee1..076137a87 100644 --- a/src/logging/reportingLogger.ts +++ b/src/logging/reportingLogger.ts @@ -1,4 +1,3 @@ -import { LogLevelType, SDKLoggerApi } from "../sdkRuntimeModels"; import { IAPIClient } from "../apiClient"; import { ErrorCodes } from "./errorCodes"; import { LogRequest, LogRequestSeverity } from "./logRequest"; From c646b0ceca169c0f9caf71b2210019b1c151ef1d Mon Sep 17 00:00:00 2001 From: Guillermo Tamanaha Date: Tue, 25 Nov 2025 13:36:34 -0300 Subject: [PATCH 04/19] fix(tests): Update reportingLogger tests to use globalThis instead of window for location and ROKT_DOMAIN --- test/jest/reportingLogger.spec.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/jest/reportingLogger.spec.ts b/test/jest/reportingLogger.spec.ts index 7d531662b..3a6da69eb 100644 --- a/test/jest/reportingLogger.spec.ts +++ b/test/jest/reportingLogger.spec.ts @@ -11,13 +11,13 @@ describe('ReportingLogger', () => { apiClient = { sendLogToServer: jest.fn() }; // Mock location object to allow modifying search property - delete (window as any).location; - (window as any).location = { + delete (globalThis as any).location; + (globalThis as any).location = { href: 'https://e.com', search: '' }; - Object.assign(window, { + Object.assign(globalThis, { navigator: { userAgent: 'ua' }, mParticle: { config: { isWebSdkLoggingEnabled: true } }, ROKT_DOMAIN: 'set' @@ -48,7 +48,7 @@ describe('ReportingLogger', () => { }); it('does not log if ROKT_DOMAIN missing', () => { - delete (window as any).ROKT_DOMAIN; + delete (globalThis as any).ROKT_DOMAIN; logger = new ReportingLogger(apiClient, sdkVersion); logger.error('x'); expect(apiClient.sendLogToServer).not.toHaveBeenCalled(); From 5cef096b09225e715f3ff5da0e48913ef413a38f Mon Sep 17 00:00:00 2001 From: Guillermo Tamanaha Date: Thu, 27 Nov 2025 10:26:37 -0300 Subject: [PATCH 05/19] refactor(mParticleInstance): Simplify initialization by removing kitBlocker parameter from related functions --- src/mp-instance.ts | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/src/mp-instance.ts b/src/mp-instance.ts index e973f6eb2..1448e4941 100644 --- a/src/mp-instance.ts +++ b/src/mp-instance.ts @@ -162,10 +162,7 @@ export default function mParticleInstance(this: IMParticleWebSDKInstance, instan ); } - const kitBlocker = createKitBlocker(config, this); - runPreConfigFetchInitialization(this, apiKey, config, kitBlocker); - debugger; - this.Logger.error('gt error test'); + runPreConfigFetchInitialization(this, apiKey, config); // config code - Fetch config when requestConfig = true, otherwise, proceed with SDKInitialization // Since fetching the configuration is asynchronous, we must pass completeSDKInitialization @@ -188,10 +185,10 @@ export default function mParticleInstance(this: IMParticleWebSDKInstance, instan result ); - completeSDKInitialization(apiKey, mergedConfig, this, kitBlocker); + completeSDKInitialization(apiKey, mergedConfig, this); }); } else { - completeSDKInitialization(apiKey, config, this, kitBlocker); + completeSDKInitialization(apiKey, config, this); } } else { console.error( @@ -1364,9 +1361,11 @@ export default function mParticleInstance(this: IMParticleWebSDKInstance, instan } // Some (server) config settings need to be returned before they are set on SDKConfig in a self hosted environment -function completeSDKInitialization(apiKey, config, mpInstance, kitBlocker: KitBlocker) { +function completeSDKInitialization(apiKey, config, mpInstance) { + const kitBlocker = createKitBlocker(config, mpInstance); const { getFeatureFlag } = mpInstance._Helpers; + mpInstance._APIClient = new APIClient(mpInstance, kitBlocker); mpInstance._Forwarders = new Forwarders(mpInstance, kitBlocker); mpInstance._Store.processConfig(config); @@ -1552,9 +1551,8 @@ function createIdentityCache(mpInstance) { return new LocalStorageVault(cacheKey, { logger: mpInstance.Logger }); } -function runPreConfigFetchInitialization(mpInstance, apiKey, config, kitBlocker: KitBlocker) { - mpInstance._APIClient = new APIClient(mpInstance, kitBlocker); - mpInstance.Logger = new Logger(config, mpInstance._APIClient); +function runPreConfigFetchInitialization(mpInstance, apiKey, config) { + mpInstance.Logger = new Logger(config); mpInstance._Store = new Store(config, mpInstance, apiKey); window.mParticle.Store = mpInstance._Store; mpInstance.Logger.verbose(StartingInitialization); From 8fa5adbbf6aaf7d390d34d73f5d3c4f49a1baf2e Mon Sep 17 00:00:00 2001 From: Guillermo Tamanaha Date: Thu, 27 Nov 2025 11:13:51 -0300 Subject: [PATCH 06/19] test(apiClient): Add unit tests for sendLogToServer method with FetchUploader and XHRUploader --- test/jest/apiClient.spec.ts | 129 ++++++++++++++++++++++++++++++++++++ 1 file changed, 129 insertions(+) create mode 100644 test/jest/apiClient.spec.ts diff --git a/test/jest/apiClient.spec.ts b/test/jest/apiClient.spec.ts new file mode 100644 index 000000000..422fa986f --- /dev/null +++ b/test/jest/apiClient.spec.ts @@ -0,0 +1,129 @@ +import { describe, test, expect, beforeEach, afterEach, jest } from '@jest/globals'; +import { LogRequest, LogRequestSeverity } from '../../src/logging/logRequest'; +import { ErrorCodes } from '../../src/logging/errorCodes'; +import APIClient from '../../src/apiClient'; + +jest.mock('../../src/uploaders', () => { + const fetchUploadMock = jest.fn(() => Promise.resolve({} as Response)); + const xhrUploadMock = jest.fn(() => Promise.resolve({} as Response)); + + class MockFetchUploader { + constructor(public url: string) {} + upload = fetchUploadMock; + } + class MockXHRUploader { + constructor(public url: string) {} + upload = xhrUploadMock; + } + + (global as any).__fetchUploadSpy = fetchUploadMock; + (global as any).__xhrUploadSpy = xhrUploadMock; + + return { + AsyncUploader: class {}, + FetchUploader: MockFetchUploader, + XHRUploader: MockXHRUploader, + }; +}); + +describe('apiClient.sendLogToServer', () => { + let mpInstance: any; + let logRequest: LogRequest; + let originalWindow: any; + let originalFetch: any; + let kitBlocker: any = { + kitBlockingEnabled: false, + dataPlanMatchLookups: {}, + }; + + beforeEach(() => { + jest.resetModules(); + + originalWindow = (global as any).window; + originalFetch = (global as any).window?.fetch; + + mpInstance = { + _Helpers: { + createServiceUrl: jest.fn((url: string, token: string) => `https://api.fake.com/${token}`) + }, + _Store: { + SDKConfig: { v2SecureServiceUrl: 'someUrl' }, + devToken: 'testToken123' + } + }; + + logRequest = { + additionalInformation: { + message: 'test', + version: '1.0.0' + }, + severity: LogRequestSeverity.Error, + code: ErrorCodes.UNHANDLED_EXCEPTION, + url: 'https://example.com', + deviceInfo: 'test', + stackTrace: 'test', + reporter: 'test', + integration: 'test' + }; + + // @ts-ignore + (global as any).window = {}; + + const fetchSpy = (global as any).__fetchUploadSpy as jest.Mock; + const xhrSpy = (global as any).__xhrUploadSpy as jest.Mock; + if (fetchSpy) fetchSpy.mockClear(); + if (xhrSpy) xhrSpy.mockClear(); + }); + + afterEach(() => { + (global as any).window = originalWindow; + if (originalFetch !== undefined) { + (global as any).window.fetch = originalFetch; + } + jest.clearAllMocks(); + }); + + test('should use FetchUploader if window.fetch is available', () => { + (global as any).window.fetch = jest.fn(); + + const uploadSpy = (global as any).__fetchUploadSpy as jest.Mock; + const client = new APIClient(mpInstance, kitBlocker); + + client.sendLogToServer(logRequest); + + validateUploadCall(uploadSpy, logRequest, mpInstance); + }); + + test('should use XHRUploader if window.fetch is not available', () => { + delete (global as any).window.fetch; + + const uploadSpy = (global as any).__xhrUploadSpy as jest.Mock; + const client = new APIClient(mpInstance, kitBlocker); + + client.sendLogToServer(logRequest); + + validateUploadCall(uploadSpy, logRequest, mpInstance); + }); + + function validateUploadCall(uploadSpy: jest.Mock, expectedLogRequest: LogRequest, mpInstance: any) { + expect(uploadSpy).toHaveBeenCalledTimes(1); + expect(uploadSpy.mock.calls.length).toBeGreaterThan(0); + const firstCall = uploadSpy.mock.calls[0] as any[]; + expect(firstCall).toBeDefined(); + const call = firstCall[0]; + expect(call).toBeDefined(); + expect((call as any).method).toBe('POST'); + expect((call as any).body).toBe(JSON.stringify(expectedLogRequest)); + expect((call as any).headers).toMatchObject({ + Accept: 'text/plain;charset=UTF-8', + 'Content-Type': 'text/plain;charset=UTF-8' + }); + expect(mpInstance._Helpers.createServiceUrl).toHaveBeenCalledWith( + mpInstance._Store.SDKConfig.v2SecureServiceUrl, + mpInstance._Store.devToken + ); + } +}); + + + From 85f99db60e86a5a18641eec85c2e7d2c9f200df5 Mon Sep 17 00:00:00 2001 From: Guillermo Tamanaha Date: Thu, 27 Nov 2025 13:10:46 -0300 Subject: [PATCH 07/19] refactor(reportingLogger): Add return types to private methods for improved type safety --- src/logging/reportingLogger.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/logging/reportingLogger.ts b/src/logging/reportingLogger.ts index 076137a87..a612749e2 100644 --- a/src/logging/reportingLogger.ts +++ b/src/logging/reportingLogger.ts @@ -57,7 +57,7 @@ export class ReportingLogger implements IReportingLogger { this.apiClient.sendLogToServer(logRequest); } - private isReportingEnabled() { + private isReportingEnabled(): boolean { return ( this.isRoktDomainPresent() && (this.isFeatureFlagEnabled() || @@ -65,18 +65,18 @@ export class ReportingLogger implements IReportingLogger { ); } - private isRoktDomainPresent() { - return window['ROKT_DOMAIN']; + private isRoktDomainPresent(): boolean { + return Boolean(window['ROKT_DOMAIN']); } - private isFeatureFlagEnabled() { + private isFeatureFlagEnabled(): boolean { return window. mParticle?. config?. isWebSdkLoggingEnabled ?? false; } - private isDebugModeEnabled() { + private isDebugModeEnabled(): boolean { return ( window. location?. From dcd3d848e6e5d9d04b4aba75455227d87bd5c8b7 Mon Sep 17 00:00:00 2001 From: Guillermo Tamanaha Date: Thu, 27 Nov 2025 17:03:00 -0300 Subject: [PATCH 08/19] refactor(reportingLogger): Allow optional rate limiter in constructor and update tests for new rate limiting behavior --- src/logging/reportingLogger.ts | 5 +++-- test/jest/reportingLogger.spec.ts | 16 ++++++++++++---- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/src/logging/reportingLogger.ts b/src/logging/reportingLogger.ts index a612749e2..ccfccca33 100644 --- a/src/logging/reportingLogger.ts +++ b/src/logging/reportingLogger.ts @@ -12,15 +12,16 @@ export class ReportingLogger implements IReportingLogger { private readonly apiClient: IAPIClient; private readonly reporter: string = 'mp-wsdk'; private readonly integration: string = 'mp-wsdk'; - private readonly rateLimiter: RateLimiter = new RateLimiter(); + private readonly rateLimiter: RateLimiter; constructor( apiClient: IAPIClient, private readonly sdkVersion: string, + rateLimiter?: RateLimiter, ) { this.isEnabled = this.isReportingEnabled(); this.apiClient = apiClient; - this.rateLimiter = new RateLimiter(); + this.rateLimiter = rateLimiter ?? new RateLimiter(); } public error(msg: string, code?: ErrorCodes, stackTrace?: string) { diff --git a/test/jest/reportingLogger.spec.ts b/test/jest/reportingLogger.spec.ts index 3a6da69eb..1134a8159 100644 --- a/test/jest/reportingLogger.spec.ts +++ b/test/jest/reportingLogger.spec.ts @@ -10,7 +10,6 @@ describe('ReportingLogger', () => { beforeEach(() => { apiClient = { sendLogToServer: jest.fn() }; - // Mock location object to allow modifying search property delete (globalThis as any).location; (globalThis as any).location = { href: 'https://e.com', @@ -70,9 +69,18 @@ describe('ReportingLogger', () => { expect(apiClient.sendLogToServer).toHaveBeenCalled(); }); - it('rate limits after 10 errors', () => { - for (let i = 0; i < 12; i++) logger.error('err'); - expect(apiClient.sendLogToServer).toHaveBeenCalledTimes(10); + it('rate limits after 3 errors', () => { + const mockRateLimiter = { + incrementAndCheck: jest.fn().mockImplementation((severity) => { + // allow only first 3, then start rate limiting + mockRateLimiter.count = (mockRateLimiter.count || 0) + 1; + return mockRateLimiter.count > 3; + }), + }; + logger = new ReportingLogger(apiClient, sdkVersion, mockRateLimiter as any); + + for (let i = 0; i < 5; i++) logger.error('err'); + expect(apiClient.sendLogToServer).toHaveBeenCalledTimes(3); }); }); From 04c9e79aa62ba142c2d0d8fd0411993756d06ded Mon Sep 17 00:00:00 2001 From: Guillermo Tamanaha Date: Thu, 27 Nov 2025 17:08:27 -0300 Subject: [PATCH 09/19] refactor(reportingLogger): Change rateLimiter type to IRateLimiter and update related tests --- src/logging/reportingLogger.ts | 4 ++-- test/jest/reportingLogger.spec.ts | 11 +++++------ 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/src/logging/reportingLogger.ts b/src/logging/reportingLogger.ts index ccfccca33..b2561e0f1 100644 --- a/src/logging/reportingLogger.ts +++ b/src/logging/reportingLogger.ts @@ -12,12 +12,12 @@ export class ReportingLogger implements IReportingLogger { private readonly apiClient: IAPIClient; private readonly reporter: string = 'mp-wsdk'; private readonly integration: string = 'mp-wsdk'; - private readonly rateLimiter: RateLimiter; + private readonly rateLimiter: IRateLimiter; constructor( apiClient: IAPIClient, private readonly sdkVersion: string, - rateLimiter?: RateLimiter, + rateLimiter?: IRateLimiter, ) { this.isEnabled = this.isReportingEnabled(); this.apiClient = apiClient; diff --git a/test/jest/reportingLogger.spec.ts b/test/jest/reportingLogger.spec.ts index 1134a8159..870f8041a 100644 --- a/test/jest/reportingLogger.spec.ts +++ b/test/jest/reportingLogger.spec.ts @@ -1,4 +1,4 @@ -import { RateLimiter, ReportingLogger } from '../../src/logging/reportingLogger'; +import { IRateLimiter, RateLimiter, ReportingLogger } from '../../src/logging/reportingLogger'; import { LogRequestSeverity } from '../../src/logging/logRequest'; import { ErrorCodes } from '../../src/logging/errorCodes'; @@ -70,14 +70,13 @@ describe('ReportingLogger', () => { }); it('rate limits after 3 errors', () => { - const mockRateLimiter = { + let count = 0; + const mockRateLimiter: IRateLimiter = { incrementAndCheck: jest.fn().mockImplementation((severity) => { - // allow only first 3, then start rate limiting - mockRateLimiter.count = (mockRateLimiter.count || 0) + 1; - return mockRateLimiter.count > 3; + return ++count > 3; }), }; - logger = new ReportingLogger(apiClient, sdkVersion, mockRateLimiter as any); + logger = new ReportingLogger(apiClient, sdkVersion, mockRateLimiter); for (let i = 0; i < 5; i++) logger.error('err'); expect(apiClient.sendLogToServer).toHaveBeenCalledTimes(3); From cb0222ecb5d051ac71e7e248e1ed93109ffdd72a Mon Sep 17 00:00:00 2001 From: Guillermo Tamanaha Date: Thu, 27 Nov 2025 17:15:55 -0300 Subject: [PATCH 10/19] fix(tests): Replace global references from 'global' to 'globalThis' in apiClient tests for improved compatibility --- test/jest/apiClient.spec.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/test/jest/apiClient.spec.ts b/test/jest/apiClient.spec.ts index 422fa986f..7fc3383f1 100644 --- a/test/jest/apiClient.spec.ts +++ b/test/jest/apiClient.spec.ts @@ -16,8 +16,8 @@ jest.mock('../../src/uploaders', () => { upload = xhrUploadMock; } - (global as any).__fetchUploadSpy = fetchUploadMock; - (global as any).__xhrUploadSpy = xhrUploadMock; + (globalThis as any).__fetchUploadSpy = fetchUploadMock; + (globalThis as any).__xhrUploadSpy = xhrUploadMock; return { AsyncUploader: class {}, @@ -69,22 +69,22 @@ describe('apiClient.sendLogToServer', () => { // @ts-ignore (global as any).window = {}; - const fetchSpy = (global as any).__fetchUploadSpy as jest.Mock; - const xhrSpy = (global as any).__xhrUploadSpy as jest.Mock; + const fetchSpy = (globalThis as any).__fetchUploadSpy as jest.Mock; + const xhrSpy = (globalThis as any).__xhrUploadSpy as jest.Mock; if (fetchSpy) fetchSpy.mockClear(); if (xhrSpy) xhrSpy.mockClear(); }); afterEach(() => { - (global as any).window = originalWindow; + (globalThis as any).window = originalWindow; if (originalFetch !== undefined) { - (global as any).window.fetch = originalFetch; + (globalThis as any).window.fetch = originalFetch; } jest.clearAllMocks(); }); test('should use FetchUploader if window.fetch is available', () => { - (global as any).window.fetch = jest.fn(); + (globalThis as any).window.fetch = jest.fn(); const uploadSpy = (global as any).__fetchUploadSpy as jest.Mock; const client = new APIClient(mpInstance, kitBlocker); From c8dc013956d55847405e90ce0fc3335f9c2a4816 Mon Sep 17 00:00:00 2001 From: Guillermo Tamanaha Date: Tue, 2 Dec 2025 14:37:13 -0300 Subject: [PATCH 11/19] refactor(logging): Move sendLogToServer method from APIClient to ReportingLogger for better encapsulation and update related tests --- src/apiClient.ts | 22 ----- src/logging/reportingLogger.ts | 31 ++++++- test/jest/apiClient.spec.ts | 129 ------------------------------ test/jest/reportingLogger.spec.ts | 55 +++++++++---- 4 files changed, 66 insertions(+), 171 deletions(-) delete mode 100644 test/jest/apiClient.spec.ts diff --git a/src/apiClient.ts b/src/apiClient.ts index 05b39da56..becba3c91 100644 --- a/src/apiClient.ts +++ b/src/apiClient.ts @@ -28,7 +28,6 @@ export interface IAPIClient { forwarder: MPForwarder, event: IUploadObject ) => void; - sendLogToServer: (log: LogRequest) => void; } export interface IForwardingStatsData { @@ -233,25 +232,4 @@ export default function APIClient( } } }; - - this.sendLogToServer = function(logRequest: LogRequest) { - const baseUrl = mpInstance._Helpers.createServiceUrl( - mpInstance._Store.SDKConfig.v2SecureServiceUrl, - mpInstance._Store.devToken - ); - - const uploadUrl = `${baseUrl}/v1/log`; - const uploader = window.fetch - ? new FetchUploader(uploadUrl) - : new XHRUploader(uploadUrl); - - uploader.upload({ - method: 'POST', - headers: { - Accept: 'text/plain;charset=UTF-8', - 'Content-Type': 'text/plain;charset=UTF-8', - }, - body: JSON.stringify(logRequest), - }); - }; } diff --git a/src/logging/reportingLogger.ts b/src/logging/reportingLogger.ts index b2561e0f1..9adc4e54f 100644 --- a/src/logging/reportingLogger.ts +++ b/src/logging/reportingLogger.ts @@ -1,6 +1,8 @@ import { IAPIClient } from "../apiClient"; +import { IMParticleWebSDKInstance } from "../mp-instance"; import { ErrorCodes } from "./errorCodes"; import { LogRequest, LogRequestSeverity } from "./logRequest"; +import { FetchUploader, XHRUploader } from "../uploaders"; export interface IReportingLogger { error(msg: string, code?: ErrorCodes, stackTrace?: string): void; @@ -9,18 +11,18 @@ export interface IReportingLogger { export class ReportingLogger implements IReportingLogger { private readonly isEnabled: boolean; - private readonly apiClient: IAPIClient; private readonly reporter: string = 'mp-wsdk'; private readonly integration: string = 'mp-wsdk'; private readonly rateLimiter: IRateLimiter; + private readonly mpInstance: IMParticleWebSDKInstance; constructor( - apiClient: IAPIClient, + mpInstance: IMParticleWebSDKInstance, private readonly sdkVersion: string, rateLimiter?: IRateLimiter, ) { + this.mpInstance = mpInstance; this.isEnabled = this.isReportingEnabled(); - this.apiClient = apiClient; this.rateLimiter = rateLimiter ?? new RateLimiter(); } @@ -55,7 +57,7 @@ export class ReportingLogger implements IReportingLogger { integration: this.integration, }; - this.apiClient.sendLogToServer(logRequest); + this.sendLogToServer(logRequest); } private isReportingEnabled(): boolean { @@ -102,6 +104,27 @@ export class ReportingLogger implements IReportingLogger { private getUserAgent(): string { return window.navigator.userAgent; } + + private sendLogToServer(logRequest: LogRequest) { + const baseUrl = this.mpInstance._Helpers.createServiceUrl( + this.mpInstance._Store.SDKConfig.v2SecureServiceUrl, + this.mpInstance._Store.devToken + ); + + const uploadUrl = `${baseUrl}/v1/log`; + const uploader = window.fetch + ? new FetchUploader(uploadUrl) + : new XHRUploader(uploadUrl); + + uploader.upload({ + method: 'POST', + headers: { + Accept: 'text/plain;charset=UTF-8', + 'Content-Type': 'text/plain;charset=UTF-8', + }, + body: JSON.stringify(logRequest), + }); + }; } export interface IRateLimiter { diff --git a/test/jest/apiClient.spec.ts b/test/jest/apiClient.spec.ts deleted file mode 100644 index 7fc3383f1..000000000 --- a/test/jest/apiClient.spec.ts +++ /dev/null @@ -1,129 +0,0 @@ -import { describe, test, expect, beforeEach, afterEach, jest } from '@jest/globals'; -import { LogRequest, LogRequestSeverity } from '../../src/logging/logRequest'; -import { ErrorCodes } from '../../src/logging/errorCodes'; -import APIClient from '../../src/apiClient'; - -jest.mock('../../src/uploaders', () => { - const fetchUploadMock = jest.fn(() => Promise.resolve({} as Response)); - const xhrUploadMock = jest.fn(() => Promise.resolve({} as Response)); - - class MockFetchUploader { - constructor(public url: string) {} - upload = fetchUploadMock; - } - class MockXHRUploader { - constructor(public url: string) {} - upload = xhrUploadMock; - } - - (globalThis as any).__fetchUploadSpy = fetchUploadMock; - (globalThis as any).__xhrUploadSpy = xhrUploadMock; - - return { - AsyncUploader: class {}, - FetchUploader: MockFetchUploader, - XHRUploader: MockXHRUploader, - }; -}); - -describe('apiClient.sendLogToServer', () => { - let mpInstance: any; - let logRequest: LogRequest; - let originalWindow: any; - let originalFetch: any; - let kitBlocker: any = { - kitBlockingEnabled: false, - dataPlanMatchLookups: {}, - }; - - beforeEach(() => { - jest.resetModules(); - - originalWindow = (global as any).window; - originalFetch = (global as any).window?.fetch; - - mpInstance = { - _Helpers: { - createServiceUrl: jest.fn((url: string, token: string) => `https://api.fake.com/${token}`) - }, - _Store: { - SDKConfig: { v2SecureServiceUrl: 'someUrl' }, - devToken: 'testToken123' - } - }; - - logRequest = { - additionalInformation: { - message: 'test', - version: '1.0.0' - }, - severity: LogRequestSeverity.Error, - code: ErrorCodes.UNHANDLED_EXCEPTION, - url: 'https://example.com', - deviceInfo: 'test', - stackTrace: 'test', - reporter: 'test', - integration: 'test' - }; - - // @ts-ignore - (global as any).window = {}; - - const fetchSpy = (globalThis as any).__fetchUploadSpy as jest.Mock; - const xhrSpy = (globalThis as any).__xhrUploadSpy as jest.Mock; - if (fetchSpy) fetchSpy.mockClear(); - if (xhrSpy) xhrSpy.mockClear(); - }); - - afterEach(() => { - (globalThis as any).window = originalWindow; - if (originalFetch !== undefined) { - (globalThis as any).window.fetch = originalFetch; - } - jest.clearAllMocks(); - }); - - test('should use FetchUploader if window.fetch is available', () => { - (globalThis as any).window.fetch = jest.fn(); - - const uploadSpy = (global as any).__fetchUploadSpy as jest.Mock; - const client = new APIClient(mpInstance, kitBlocker); - - client.sendLogToServer(logRequest); - - validateUploadCall(uploadSpy, logRequest, mpInstance); - }); - - test('should use XHRUploader if window.fetch is not available', () => { - delete (global as any).window.fetch; - - const uploadSpy = (global as any).__xhrUploadSpy as jest.Mock; - const client = new APIClient(mpInstance, kitBlocker); - - client.sendLogToServer(logRequest); - - validateUploadCall(uploadSpy, logRequest, mpInstance); - }); - - function validateUploadCall(uploadSpy: jest.Mock, expectedLogRequest: LogRequest, mpInstance: any) { - expect(uploadSpy).toHaveBeenCalledTimes(1); - expect(uploadSpy.mock.calls.length).toBeGreaterThan(0); - const firstCall = uploadSpy.mock.calls[0] as any[]; - expect(firstCall).toBeDefined(); - const call = firstCall[0]; - expect(call).toBeDefined(); - expect((call as any).method).toBe('POST'); - expect((call as any).body).toBe(JSON.stringify(expectedLogRequest)); - expect((call as any).headers).toMatchObject({ - Accept: 'text/plain;charset=UTF-8', - 'Content-Type': 'text/plain;charset=UTF-8' - }); - expect(mpInstance._Helpers.createServiceUrl).toHaveBeenCalledWith( - mpInstance._Store.SDKConfig.v2SecureServiceUrl, - mpInstance._Store.devToken - ); - } -}); - - - diff --git a/test/jest/reportingLogger.spec.ts b/test/jest/reportingLogger.spec.ts index 870f8041a..3dd2c02c8 100644 --- a/test/jest/reportingLogger.spec.ts +++ b/test/jest/reportingLogger.spec.ts @@ -3,12 +3,26 @@ import { LogRequestSeverity } from '../../src/logging/logRequest'; import { ErrorCodes } from '../../src/logging/errorCodes'; describe('ReportingLogger', () => { - let apiClient: any; + let mpInstance: any; let logger: ReportingLogger; const sdkVersion = '1.2.3'; + let mockFetch: jest.Mock; beforeEach(() => { - apiClient = { sendLogToServer: jest.fn() }; + mockFetch = jest.fn().mockResolvedValue({ ok: true }); + global.fetch = mockFetch; + + mpInstance = { + _Helpers: { + createServiceUrl: jest.fn().mockReturnValue('https://test-url.com') + }, + _Store: { + SDKConfig: { + v2SecureServiceUrl: 'https://secure-service.com' + }, + devToken: 'test-token' + } + }; delete (globalThis as any).location; (globalThis as any).location = { @@ -19,9 +33,10 @@ describe('ReportingLogger', () => { Object.assign(globalThis, { navigator: { userAgent: 'ua' }, mParticle: { config: { isWebSdkLoggingEnabled: true } }, - ROKT_DOMAIN: 'set' + ROKT_DOMAIN: 'set', + fetch: mockFetch }); - logger = new ReportingLogger(apiClient, sdkVersion); + logger = new ReportingLogger(mpInstance, sdkVersion); }); afterEach(() => { @@ -32,41 +47,49 @@ describe('ReportingLogger', () => { it('sends error logs with correct params', () => { logger.error('msg', ErrorCodes.UNHANDLED_EXCEPTION, 'stack'); - expect(apiClient.sendLogToServer).toHaveBeenCalledWith(expect.objectContaining({ + 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: LogRequestSeverity.Error, code: ErrorCodes.UNHANDLED_EXCEPTION, stackTrace: 'stack' - })); + }); }); it('sends warning logs with correct params', () => { logger.warning('warn'); - expect(apiClient.sendLogToServer).toHaveBeenCalledWith(expect.objectContaining({ + 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: LogRequestSeverity.Warning - })); + }); }); it('does not log if ROKT_DOMAIN missing', () => { delete (globalThis as any).ROKT_DOMAIN; - logger = new ReportingLogger(apiClient, sdkVersion); + logger = new ReportingLogger(mpInstance, sdkVersion); logger.error('x'); - expect(apiClient.sendLogToServer).not.toHaveBeenCalled(); + expect(mockFetch).not.toHaveBeenCalled(); }); it('does not log if feature flag and debug mode off', () => { window.mParticle.config.isWebSdkLoggingEnabled = false; window.location.search = ''; - logger = new ReportingLogger(apiClient, sdkVersion); + logger = new ReportingLogger(mpInstance, sdkVersion); logger.error('x'); - expect(apiClient.sendLogToServer).not.toHaveBeenCalled(); + 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(apiClient, sdkVersion); + logger = new ReportingLogger(mpInstance, sdkVersion); logger.error('x'); - expect(apiClient.sendLogToServer).toHaveBeenCalled(); + expect(mockFetch).toHaveBeenCalled(); }); it('rate limits after 3 errors', () => { @@ -76,10 +99,10 @@ describe('ReportingLogger', () => { return ++count > 3; }), }; - logger = new ReportingLogger(apiClient, sdkVersion, mockRateLimiter); + logger = new ReportingLogger(mpInstance, sdkVersion, mockRateLimiter); for (let i = 0; i < 5; i++) logger.error('err'); - expect(apiClient.sendLogToServer).toHaveBeenCalledTimes(3); + expect(mockFetch).toHaveBeenCalledTimes(3); }); }); From 8746bd1e5beff1d61e2e8b4f281bcacc0cc996b1 Mon Sep 17 00:00:00 2001 From: Guillermo Tamanaha Date: Tue, 2 Dec 2025 14:38:51 -0300 Subject: [PATCH 12/19] refactor(apiClient): Remove unused LogRequest import to clean up code --- src/apiClient.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/apiClient.ts b/src/apiClient.ts index becba3c91..5e0a2fe76 100644 --- a/src/apiClient.ts +++ b/src/apiClient.ts @@ -10,7 +10,6 @@ import { IMParticleUser, ISDKUserAttributes } from './identity-user-interfaces'; import { AsyncUploader, FetchUploader, XHRUploader } from './uploaders'; import { IMParticleWebSDKInstance } from './mp-instance'; import { appendUserInfo } from './user-utils'; -import { LogRequest } from './logging/logRequest'; export interface IAPIClient { uploader: BatchUploader | null; From cac1c3c8d547213c6527a106fc9a9df17e8edaf8 Mon Sep 17 00:00:00 2001 From: Guillermo Tamanaha Date: Fri, 5 Dec 2025 12:20:44 -0300 Subject: [PATCH 13/19] feat(reportingLogger): Add 'rokt-account-id' header and update constructor to accept accountId, with corresponding test adjustments --- src/logging/reportingLogger.ts | 2 ++ src/uploaders.ts | 1 + test/jest/reportingLogger.spec.ts | 13 +++++++------ 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/logging/reportingLogger.ts b/src/logging/reportingLogger.ts index 9adc4e54f..25f2575a2 100644 --- a/src/logging/reportingLogger.ts +++ b/src/logging/reportingLogger.ts @@ -19,6 +19,7 @@ export class ReportingLogger implements IReportingLogger { constructor( mpInstance: IMParticleWebSDKInstance, private readonly sdkVersion: string, + private readonly accountId: string, rateLimiter?: IRateLimiter, ) { this.mpInstance = mpInstance; @@ -121,6 +122,7 @@ export class ReportingLogger implements IReportingLogger { headers: { Accept: 'text/plain;charset=UTF-8', 'Content-Type': 'text/plain;charset=UTF-8', + 'rokt-account-id': this.accountId }, body: JSON.stringify(logRequest), }); diff --git a/src/uploaders.ts b/src/uploaders.ts index e28606c46..c4e6b7e49 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/reportingLogger.spec.ts b/test/jest/reportingLogger.spec.ts index 3dd2c02c8..308bfdd00 100644 --- a/test/jest/reportingLogger.spec.ts +++ b/test/jest/reportingLogger.spec.ts @@ -7,7 +7,7 @@ describe('ReportingLogger', () => { let logger: ReportingLogger; const sdkVersion = '1.2.3'; let mockFetch: jest.Mock; - + const accountId = '1234567890'; beforeEach(() => { mockFetch = jest.fn().mockResolvedValue({ ok: true }); global.fetch = mockFetch; @@ -36,7 +36,7 @@ describe('ReportingLogger', () => { ROKT_DOMAIN: 'set', fetch: mockFetch }); - logger = new ReportingLogger(mpInstance, sdkVersion); + logger = new ReportingLogger(mpInstance, sdkVersion, accountId); }); afterEach(() => { @@ -67,11 +67,12 @@ describe('ReportingLogger', () => { expect(body).toMatchObject({ severity: LogRequestSeverity.Warning }); + expect(fetchCall[1].headers['rokt-account-id']).toBe(accountId); }); it('does not log if ROKT_DOMAIN missing', () => { delete (globalThis as any).ROKT_DOMAIN; - logger = new ReportingLogger(mpInstance, sdkVersion); + logger = new ReportingLogger(mpInstance, sdkVersion, accountId); logger.error('x'); expect(mockFetch).not.toHaveBeenCalled(); }); @@ -79,7 +80,7 @@ describe('ReportingLogger', () => { it('does not log if feature flag and debug mode off', () => { window.mParticle.config.isWebSdkLoggingEnabled = false; window.location.search = ''; - logger = new ReportingLogger(mpInstance, sdkVersion); + logger = new ReportingLogger(mpInstance, sdkVersion, accountId); logger.error('x'); expect(mockFetch).not.toHaveBeenCalled(); }); @@ -87,7 +88,7 @@ describe('ReportingLogger', () => { 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(mpInstance, sdkVersion); + logger = new ReportingLogger(mpInstance, sdkVersion, accountId); logger.error('x'); expect(mockFetch).toHaveBeenCalled(); }); @@ -99,7 +100,7 @@ describe('ReportingLogger', () => { return ++count > 3; }), }; - logger = new ReportingLogger(mpInstance, sdkVersion, mockRateLimiter); + logger = new ReportingLogger(mpInstance, sdkVersion, accountId, mockRateLimiter); for (let i = 0; i < 5; i++) logger.error('err'); expect(mockFetch).toHaveBeenCalledTimes(3); From 5581588056933b9b8b11f35683e08ec6d7c5e426 Mon Sep 17 00:00:00 2001 From: Guillermo Tamanaha Date: Tue, 9 Dec 2025 13:52:35 -0300 Subject: [PATCH 14/19] refactor(errorCodes, reportingLogger): Replace ErrorCodes type definition with valueof utility and update ReportingLogger to use baseUrl; adjust tests accordingly --- src/logging/errorCodes.ts | 4 +++- src/logging/logMessage.ts | 0 src/logging/reportingLogger.ts | 19 ++++++------------ test/jest/reportingLogger.spec.ts | 32 ++++++++++++++----------------- 4 files changed, 23 insertions(+), 32 deletions(-) delete mode 100644 src/logging/logMessage.ts diff --git a/src/logging/errorCodes.ts b/src/logging/errorCodes.ts index c45d2ef75..ae99429b1 100644 --- a/src/logging/errorCodes.ts +++ b/src/logging/errorCodes.ts @@ -1,4 +1,6 @@ -export type ErrorCodes = (typeof ErrorCodes)[keyof typeof ErrorCodes]; +import { valueof } from '../utils'; + +export type ErrorCodes = valueof; export const ErrorCodes = { UNHANDLED_EXCEPTION: 'UNHANDLED_EXCEPTION', diff --git a/src/logging/logMessage.ts b/src/logging/logMessage.ts deleted file mode 100644 index e69de29bb..000000000 diff --git a/src/logging/reportingLogger.ts b/src/logging/reportingLogger.ts index 25f2575a2..9d6c1a35f 100644 --- a/src/logging/reportingLogger.ts +++ b/src/logging/reportingLogger.ts @@ -1,5 +1,4 @@ -import { IAPIClient } from "../apiClient"; -import { IMParticleWebSDKInstance } from "../mp-instance"; + import { ErrorCodes } from "./errorCodes"; import { LogRequest, LogRequestSeverity } from "./logRequest"; import { FetchUploader, XHRUploader } from "../uploaders"; @@ -14,15 +13,14 @@ export class ReportingLogger implements IReportingLogger { private readonly reporter: string = 'mp-wsdk'; private readonly integration: string = 'mp-wsdk'; private readonly rateLimiter: IRateLimiter; - private readonly mpInstance: IMParticleWebSDKInstance; - + private readonly DEFAULT_ACCOUNT_ID: string = 'no-account-id-set'; + constructor( - mpInstance: IMParticleWebSDKInstance, + private baseUrl: string, private readonly sdkVersion: string, private readonly accountId: string, rateLimiter?: IRateLimiter, ) { - this.mpInstance = mpInstance; this.isEnabled = this.isReportingEnabled(); this.rateLimiter = rateLimiter ?? new RateLimiter(); } @@ -107,12 +105,7 @@ export class ReportingLogger implements IReportingLogger { } private sendLogToServer(logRequest: LogRequest) { - const baseUrl = this.mpInstance._Helpers.createServiceUrl( - this.mpInstance._Store.SDKConfig.v2SecureServiceUrl, - this.mpInstance._Store.devToken - ); - - const uploadUrl = `${baseUrl}/v1/log`; + const uploadUrl = `${this.baseUrl}/v1/log`; const uploader = window.fetch ? new FetchUploader(uploadUrl) : new XHRUploader(uploadUrl); @@ -122,7 +115,7 @@ export class ReportingLogger implements IReportingLogger { headers: { Accept: 'text/plain;charset=UTF-8', 'Content-Type': 'text/plain;charset=UTF-8', - 'rokt-account-id': this.accountId + 'rokt-account-id': this.accountId || this.DEFAULT_ACCOUNT_ID }, body: JSON.stringify(logRequest), }); diff --git a/test/jest/reportingLogger.spec.ts b/test/jest/reportingLogger.spec.ts index 308bfdd00..c2ed33032 100644 --- a/test/jest/reportingLogger.spec.ts +++ b/test/jest/reportingLogger.spec.ts @@ -3,8 +3,8 @@ import { LogRequestSeverity } from '../../src/logging/logRequest'; import { ErrorCodes } from '../../src/logging/errorCodes'; describe('ReportingLogger', () => { - let mpInstance: any; let logger: ReportingLogger; + const baseUrl = 'https://test-url.com'; const sdkVersion = '1.2.3'; let mockFetch: jest.Mock; const accountId = '1234567890'; @@ -12,18 +12,6 @@ describe('ReportingLogger', () => { mockFetch = jest.fn().mockResolvedValue({ ok: true }); global.fetch = mockFetch; - mpInstance = { - _Helpers: { - createServiceUrl: jest.fn().mockReturnValue('https://test-url.com') - }, - _Store: { - SDKConfig: { - v2SecureServiceUrl: 'https://secure-service.com' - }, - devToken: 'test-token' - } - }; - delete (globalThis as any).location; (globalThis as any).location = { href: 'https://e.com', @@ -36,7 +24,7 @@ describe('ReportingLogger', () => { ROKT_DOMAIN: 'set', fetch: mockFetch }); - logger = new ReportingLogger(mpInstance, sdkVersion, accountId); + logger = new ReportingLogger(baseUrl, sdkVersion, accountId); }); afterEach(() => { @@ -72,7 +60,7 @@ describe('ReportingLogger', () => { it('does not log if ROKT_DOMAIN missing', () => { delete (globalThis as any).ROKT_DOMAIN; - logger = new ReportingLogger(mpInstance, sdkVersion, accountId); + logger = new ReportingLogger(baseUrl, sdkVersion, accountId); logger.error('x'); expect(mockFetch).not.toHaveBeenCalled(); }); @@ -80,7 +68,7 @@ describe('ReportingLogger', () => { it('does not log if feature flag and debug mode off', () => { window.mParticle.config.isWebSdkLoggingEnabled = false; window.location.search = ''; - logger = new ReportingLogger(mpInstance, sdkVersion, accountId); + logger = new ReportingLogger(baseUrl, sdkVersion, accountId); logger.error('x'); expect(mockFetch).not.toHaveBeenCalled(); }); @@ -88,7 +76,7 @@ describe('ReportingLogger', () => { 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(mpInstance, sdkVersion, accountId); + logger = new ReportingLogger(baseUrl, sdkVersion, accountId); logger.error('x'); expect(mockFetch).toHaveBeenCalled(); }); @@ -100,11 +88,19 @@ describe('ReportingLogger', () => { return ++count > 3; }), }; - logger = new ReportingLogger(mpInstance, sdkVersion, accountId, mockRateLimiter); + logger = new ReportingLogger(baseUrl, sdkVersion, accountId, mockRateLimiter); for (let i = 0; i < 5; i++) logger.error('err'); expect(mockFetch).toHaveBeenCalledTimes(3); }); + + it('uses default account id when accountId is empty', () => { + logger = new ReportingLogger(baseUrl, sdkVersion, undefined); + logger.error('msg'); + expect(mockFetch).toHaveBeenCalled(); + const fetchCall = mockFetch.mock.calls[0]; + expect(fetchCall[1].headers['rokt-account-id']).toBe('no-account-id-set'); + }); }); describe('RateLimiter', () => { From 42a67c074722c4e25b7054c206666699480511fd Mon Sep 17 00:00:00 2001 From: Guillermo Tamanaha Date: Wed, 10 Dec 2025 09:29:07 -0300 Subject: [PATCH 15/19] feat(reportingLogger): Introduce default user agent and URL handling in ReportingLogger, with corresponding tests for fallback behavior --- src/logging/reportingLogger.ts | 6 ++++-- test/jest/reportingLogger.spec.ts | 11 +++++++++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/src/logging/reportingLogger.ts b/src/logging/reportingLogger.ts index 9d6c1a35f..2b048c2d4 100644 --- a/src/logging/reportingLogger.ts +++ b/src/logging/reportingLogger.ts @@ -14,6 +14,8 @@ export class ReportingLogger implements IReportingLogger { private readonly integration: string = 'mp-wsdk'; private readonly rateLimiter: IRateLimiter; private readonly DEFAULT_ACCOUNT_ID: string = 'no-account-id-set'; + private readonly DEFAULT_USER_AGENT: string = 'no-user-agent-set'; + private readonly DEFAULT_URL: string = 'no-url-set'; constructor( private baseUrl: string, @@ -97,11 +99,11 @@ export class ReportingLogger implements IReportingLogger { } private getUrl(): string { - return window.location.href; + return window?.location?.href ?? this.DEFAULT_URL; } private getUserAgent(): string { - return window.navigator.userAgent; + return window?.navigator?.userAgent ?? this.DEFAULT_USER_AGENT; } private sendLogToServer(logRequest: LogRequest) { diff --git a/test/jest/reportingLogger.spec.ts b/test/jest/reportingLogger.spec.ts index c2ed33032..04efbd3c3 100644 --- a/test/jest/reportingLogger.spec.ts +++ b/test/jest/reportingLogger.spec.ts @@ -101,6 +101,17 @@ describe('ReportingLogger', () => { const fetchCall = mockFetch.mock.calls[0]; expect(fetchCall[1].headers['rokt-account-id']).toBe('no-account-id-set'); }); + + it('uses default user agent when user agent is empty', () => { + logger = new ReportingLogger(baseUrl, sdkVersion, accountId); + delete (globalThis as any).navigator; + delete (globalThis as any).location; + logger.error('msg'); + expect(mockFetch).toHaveBeenCalled(); + const fetchCall = mockFetch.mock.calls[0]; + const body = JSON.parse(fetchCall[1].body); + expect(body).toMatchObject({ deviceInfo: 'no-user-agent-set', url: 'no-url-set' }); + }); }); describe('RateLimiter', () => { From 4de0bc71d6c9b6f02c9df0552d2b56b6d22a98dd Mon Sep 17 00:00:00 2001 From: Guillermo Tamanaha Date: Wed, 10 Dec 2025 09:35:59 -0300 Subject: [PATCH 16/19] fix(reportingLogger): Update default account ID to '0' and adjust corresponding test expectation --- src/logging/reportingLogger.ts | 2 +- test/jest/reportingLogger.spec.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/logging/reportingLogger.ts b/src/logging/reportingLogger.ts index 2b048c2d4..b4ef54c03 100644 --- a/src/logging/reportingLogger.ts +++ b/src/logging/reportingLogger.ts @@ -13,7 +13,7 @@ export class ReportingLogger implements IReportingLogger { private readonly reporter: string = 'mp-wsdk'; private readonly integration: string = 'mp-wsdk'; private readonly rateLimiter: IRateLimiter; - private readonly DEFAULT_ACCOUNT_ID: string = 'no-account-id-set'; + private readonly DEFAULT_ACCOUNT_ID: string = '0'; private readonly DEFAULT_USER_AGENT: string = 'no-user-agent-set'; private readonly DEFAULT_URL: string = 'no-url-set'; diff --git a/test/jest/reportingLogger.spec.ts b/test/jest/reportingLogger.spec.ts index 04efbd3c3..02d9a33b0 100644 --- a/test/jest/reportingLogger.spec.ts +++ b/test/jest/reportingLogger.spec.ts @@ -99,7 +99,7 @@ describe('ReportingLogger', () => { logger.error('msg'); expect(mockFetch).toHaveBeenCalled(); const fetchCall = mockFetch.mock.calls[0]; - expect(fetchCall[1].headers['rokt-account-id']).toBe('no-account-id-set'); + expect(fetchCall[1].headers['rokt-account-id']).toBe('0'); }); it('uses default user agent when user agent is empty', () => { From c26864627331d00f6eda31d3af4fb173899bafed Mon Sep 17 00:00:00 2001 From: Guillermo Tamanaha Date: Mon, 5 Jan 2026 13:01:12 -0300 Subject: [PATCH 17/19] refactor(reportingLogger): Replace LogRequest with IWSDKError and update severity handling; adjust tests for new error logging structure --- src/logging/reportingLogger.ts | 70 ++++++++-------- src/logging/{logRequest.ts => wsdk-error.ts} | 15 ++-- test/jest/reportingLogger.spec.ts | 85 ++++++++++---------- 3 files changed, 84 insertions(+), 86 deletions(-) rename src/logging/{logRequest.ts => wsdk-error.ts} (50%) diff --git a/src/logging/reportingLogger.ts b/src/logging/reportingLogger.ts index b4ef54c03..a5df17023 100644 --- a/src/logging/reportingLogger.ts +++ b/src/logging/reportingLogger.ts @@ -1,6 +1,6 @@ import { ErrorCodes } from "./errorCodes"; -import { LogRequest, LogRequestSeverity } from "./logRequest"; +import { IWSDKError, WSDKErrorSeverity } from "./wsdk-error"; import { FetchUploader, XHRUploader } from "../uploaders"; export interface IReportingLogger { @@ -13,30 +13,28 @@ export class ReportingLogger implements IReportingLogger { private readonly reporter: string = 'mp-wsdk'; private readonly integration: string = 'mp-wsdk'; private readonly rateLimiter: IRateLimiter; - private readonly DEFAULT_ACCOUNT_ID: string = '0'; - private readonly DEFAULT_USER_AGENT: string = 'no-user-agent-set'; - private readonly DEFAULT_URL: string = 'no-url-set'; constructor( private baseUrl: string, 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, stackTrace?: string) { - this.sendLog(LogRequestSeverity.Error, msg, code ?? ErrorCodes.UNHANDLED_EXCEPTION, stackTrace); + public error(msg: string, code: ErrorCodes, stackTrace?: string) { + this.sendLog(WSDKErrorSeverity.ERROR, msg, code, stackTrace); }; - public warning(msg: string, code?: ErrorCodes) { - this.sendLog(LogRequestSeverity.Warning, msg, code ?? ErrorCodes.UNHANDLED_EXCEPTION); + public warning(msg: string, code: ErrorCodes) { + this.sendLog(WSDKErrorSeverity.WARNING, msg, code); }; private sendLog( - severity: LogRequestSeverity, + severity: WSDKErrorSeverity, msg: string, code: ErrorCodes, stackTrace?: string @@ -44,21 +42,21 @@ export class ReportingLogger implements IReportingLogger { if(!this.canSendLog(severity)) return; - const logRequest: LogRequest = { + const wsdkError: IWSDKError = { additionalInformation: { message: msg, version: this.sdkVersion, }, severity: severity, code: code, - url: this.getUrl(), - deviceInfo: this.getUserAgent(), + url: window?.location?.href, + deviceInfo: window?.navigator?.userAgent, stackTrace: stackTrace ?? '', reporter: this.reporter, integration: this.integration, }; - this.sendLogToServer(logRequest); + this.sendLogToServer(wsdkError); } private isReportingEnabled(): boolean { @@ -90,53 +88,51 @@ export class ReportingLogger implements IReportingLogger { ); } - private canSendLog(severity: LogRequestSeverity): boolean { + private canSendLog(severity: WSDKErrorSeverity): boolean { return this.isEnabled && !this.isRateLimited(severity); } - private isRateLimited(severity: LogRequestSeverity): boolean { + private isRateLimited(severity: WSDKErrorSeverity): boolean { return this.rateLimiter.incrementAndCheck(severity); } - private getUrl(): string { - return window?.location?.href ?? this.DEFAULT_URL; - } - - private getUserAgent(): string { - return window?.navigator?.userAgent ?? this.DEFAULT_USER_AGENT; - } - - private sendLogToServer(logRequest: LogRequest) { + 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', - headers: { - Accept: 'text/plain;charset=UTF-8', - 'Content-Type': 'text/plain;charset=UTF-8', - 'rokt-account-id': this.accountId || this.DEFAULT_ACCOUNT_ID - }, - body: JSON.stringify(logRequest), + body: JSON.stringify(wsdkError), }); }; } export interface IRateLimiter { - incrementAndCheck(severity: LogRequestSeverity): boolean; + incrementAndCheck(severity: WSDKErrorSeverity): boolean; } export class RateLimiter implements IRateLimiter { - private readonly rateLimits: Map = new Map([ - [LogRequestSeverity.Error, 10], - [LogRequestSeverity.Warning, 10], - [LogRequestSeverity.Info, 10], + private readonly rateLimits: Map = new Map([ + [WSDKErrorSeverity.ERROR, 10], + [WSDKErrorSeverity.WARNING, 10], + [WSDKErrorSeverity.INFO, 10], ]); - private logCount: Map = new Map(); + private logCount: Map = new Map(); - public incrementAndCheck(severity: LogRequestSeverity): boolean { + public incrementAndCheck(severity: WSDKErrorSeverity): boolean { const count = this.logCount.get(severity) || 0; const limit = this.rateLimits.get(severity) || 10; diff --git a/src/logging/logRequest.ts b/src/logging/wsdk-error.ts similarity index 50% rename from src/logging/logRequest.ts rename to src/logging/wsdk-error.ts index 8cb248e14..e05d456c5 100644 --- a/src/logging/logRequest.ts +++ b/src/logging/wsdk-error.ts @@ -1,17 +1,18 @@ import { ErrorCodes } from "./errorCodes"; -export enum LogRequestSeverity { - Error = 'error', - Warning = 'warning', - Info = 'info', -} +export type WSDKErrorSeverity = (typeof WSDKErrorSeverity)[keyof typeof WSDKErrorSeverity]; +export const WSDKErrorSeverity = { + ERROR: 'ERROR', + INFO: 'INFO', + WARNING: 'WARNING', +} as const; -export interface LogRequest { +export interface IWSDKError { additionalInformation: { message: string; version: string; }; - severity: LogRequestSeverity; + severity: WSDKErrorSeverity; code: ErrorCodes; url: string; deviceInfo: string; diff --git a/test/jest/reportingLogger.spec.ts b/test/jest/reportingLogger.spec.ts index 02d9a33b0..b3f4fe5ce 100644 --- a/test/jest/reportingLogger.spec.ts +++ b/test/jest/reportingLogger.spec.ts @@ -1,5 +1,5 @@ import { IRateLimiter, RateLimiter, ReportingLogger } from '../../src/logging/reportingLogger'; -import { LogRequestSeverity } from '../../src/logging/logRequest'; +import { WSDKErrorSeverity } from '../../src/logging/wsdk-error'; import { ErrorCodes } from '../../src/logging/errorCodes'; describe('ReportingLogger', () => { @@ -8,6 +8,7 @@ describe('ReportingLogger', () => { 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; @@ -24,7 +25,7 @@ describe('ReportingLogger', () => { ROKT_DOMAIN: 'set', fetch: mockFetch }); - logger = new ReportingLogger(baseUrl, sdkVersion, accountId); + logger = new ReportingLogger(baseUrl, sdkVersion, accountId, roktLauncherInstanceGuid); }); afterEach(() => { @@ -40,44 +41,55 @@ describe('ReportingLogger', () => { expect(fetchCall[0]).toContain('/v1/log'); const body = JSON.parse(fetchCall[1].body); expect(body).toMatchObject({ - severity: LogRequestSeverity.Error, + severity: WSDKErrorSeverity.ERROR, code: ErrorCodes.UNHANDLED_EXCEPTION, - stackTrace: 'stack' + }); + 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'); + 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: LogRequestSeverity.Warning + 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, }); - expect(fetchCall[1].headers['rokt-account-id']).toBe(accountId); }); it('does not log if ROKT_DOMAIN missing', () => { delete (globalThis as any).ROKT_DOMAIN; - logger = new ReportingLogger(baseUrl, sdkVersion, accountId); - logger.error('x'); + logger = new ReportingLogger(baseUrl, 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.mParticle.config.isWebSdkLoggingEnabled = false; window.location.search = ''; - logger = new ReportingLogger(baseUrl, sdkVersion, accountId); - logger.error('x'); + logger = new ReportingLogger(baseUrl, 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, sdkVersion, accountId); - logger.error('x'); + logger = new ReportingLogger(baseUrl, sdkVersion, accountId, roktLauncherInstanceGuid); + logger.error('x', ErrorCodes.UNHANDLED_EXCEPTION); expect(mockFetch).toHaveBeenCalled(); }); @@ -88,29 +100,18 @@ describe('ReportingLogger', () => { return ++count > 3; }), }; - logger = new ReportingLogger(baseUrl, sdkVersion, accountId, mockRateLimiter); + logger = new ReportingLogger(baseUrl, sdkVersion, accountId, roktLauncherInstanceGuid, mockRateLimiter); - for (let i = 0; i < 5; i++) logger.error('err'); + for (let i = 0; i < 5; i++) logger.error('err', ErrorCodes.UNHANDLED_EXCEPTION); expect(mockFetch).toHaveBeenCalledTimes(3); }); - it('uses default account id when accountId is empty', () => { - logger = new ReportingLogger(baseUrl, sdkVersion, undefined); - logger.error('msg'); + it('does not send account id when accountId is empty', () => { + logger = new ReportingLogger(baseUrl, sdkVersion, '', roktLauncherInstanceGuid); + logger.error('msg', ErrorCodes.UNHANDLED_EXCEPTION); expect(mockFetch).toHaveBeenCalled(); const fetchCall = mockFetch.mock.calls[0]; - expect(fetchCall[1].headers['rokt-account-id']).toBe('0'); - }); - - it('uses default user agent when user agent is empty', () => { - logger = new ReportingLogger(baseUrl, sdkVersion, accountId); - delete (globalThis as any).navigator; - delete (globalThis as any).location; - logger.error('msg'); - expect(mockFetch).toHaveBeenCalled(); - const fetchCall = mockFetch.mock.calls[0]; - const body = JSON.parse(fetchCall[1].body); - expect(body).toMatchObject({ deviceInfo: 'no-user-agent-set', url: 'no-url-set' }); + expect(fetchCall[1].headers['rokt-account-id']).toBeUndefined(); }); }); @@ -122,33 +123,33 @@ describe('RateLimiter', () => { it('allows up to 10 error logs then rate limits', () => { for (let i = 0; i < 10; i++) { - expect(rateLimiter.incrementAndCheck(LogRequestSeverity.Error)).toBe(false); + expect(rateLimiter.incrementAndCheck(WSDKErrorSeverity.ERROR)).toBe(false); } - expect(rateLimiter.incrementAndCheck(LogRequestSeverity.Error)).toBe(true); - expect(rateLimiter.incrementAndCheck(LogRequestSeverity.Error)).toBe(true); + 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(LogRequestSeverity.Warning)).toBe(false); + expect(rateLimiter.incrementAndCheck(WSDKErrorSeverity.WARNING)).toBe(false); } - expect(rateLimiter.incrementAndCheck(LogRequestSeverity.Warning)).toBe(true); - expect(rateLimiter.incrementAndCheck(LogRequestSeverity.Warning)).toBe(true); + 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(LogRequestSeverity.Info)).toBe(false); + expect(rateLimiter.incrementAndCheck(WSDKErrorSeverity.INFO)).toBe(false); } - expect(rateLimiter.incrementAndCheck(LogRequestSeverity.Info)).toBe(true); - expect(rateLimiter.incrementAndCheck(LogRequestSeverity.Info)).toBe(true); + 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(LogRequestSeverity.Error); + rateLimiter.incrementAndCheck(WSDKErrorSeverity.ERROR); } - expect(rateLimiter.incrementAndCheck(LogRequestSeverity.Error)).toBe(true); - expect(rateLimiter.incrementAndCheck(LogRequestSeverity.Warning)).toBe(false); + expect(rateLimiter.incrementAndCheck(WSDKErrorSeverity.ERROR)).toBe(true); + expect(rateLimiter.incrementAndCheck(WSDKErrorSeverity.WARNING)).toBe(false); }); }); From 4e7e0cb6cf2d8d6475e5da61fbffcce8fd035f03 Mon Sep 17 00:00:00 2001 From: Guillermo Tamanaha Date: Mon, 5 Jan 2026 13:48:33 -0300 Subject: [PATCH 18/19] refactor(reportingLogger, wsdk-error): Update error logging structure to include name and stack properties; adjust IWSDKError interface accordingly --- src/logging/reportingLogger.ts | 20 +++++++++++--------- src/logging/wsdk-error.ts | 20 +++++++++----------- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/src/logging/reportingLogger.ts b/src/logging/reportingLogger.ts index a5df17023..73d46376c 100644 --- a/src/logging/reportingLogger.ts +++ b/src/logging/reportingLogger.ts @@ -4,7 +4,7 @@ import { IWSDKError, WSDKErrorSeverity } from "./wsdk-error"; import { FetchUploader, XHRUploader } from "../uploaders"; export interface IReportingLogger { - error(msg: string, code?: ErrorCodes, stackTrace?: string): void; + error(msg: string, code?: ErrorCodes, stack?: string): void; warning(msg: string, code?: ErrorCodes): void; } @@ -25,33 +25,35 @@ export class ReportingLogger implements IReportingLogger { this.rateLimiter = rateLimiter ?? new RateLimiter(); } - public error(msg: string, code: ErrorCodes, stackTrace?: string) { - this.sendLog(WSDKErrorSeverity.ERROR, msg, code, stackTrace); + public error(msg: string, code: ErrorCodes, name?: string, stack?: string) { + this.sendLog(WSDKErrorSeverity.ERROR, msg, code, name, stack); }; - public warning(msg: string, code: ErrorCodes) { - this.sendLog(WSDKErrorSeverity.WARNING, msg, code); + public warning(msg: string, code: ErrorCodes, name?: string) { + this.sendLog(WSDKErrorSeverity.WARNING, msg, code, name); }; private sendLog( severity: WSDKErrorSeverity, msg: string, code: ErrorCodes, - stackTrace?: string + 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, - url: window?.location?.href, - deviceInfo: window?.navigator?.userAgent, - stackTrace: stackTrace ?? '', + stack: stack, reporter: this.reporter, integration: this.integration, }; diff --git a/src/logging/wsdk-error.ts b/src/logging/wsdk-error.ts index e05d456c5..c51f2fa0d 100644 --- a/src/logging/wsdk-error.ts +++ b/src/logging/wsdk-error.ts @@ -8,15 +8,13 @@ export const WSDKErrorSeverity = { } as const; export interface IWSDKError { - additionalInformation: { - message: string; - version: string; - }; - severity: WSDKErrorSeverity; - code: ErrorCodes; - url: string; - deviceInfo: string; - stackTrace: string; - reporter: string; - integration: string; + name: string; + message: string; + stack?: string; + code?: ErrorCodes; + reporter?: string; + integration?: string; + severity?: WSDKErrorSeverity; + additionalInformation?: Record; + handled?: boolean; } \ No newline at end of file From 0d4882667ac9829c44034a39cb7c8fad5920dd31 Mon Sep 17 00:00:00 2001 From: Guillermo Tamanaha Date: Mon, 5 Jan 2026 14:24:18 -0300 Subject: [PATCH 19/19] feat(reportingLogger): Add feature flag support to ReportingLogger constructor and update related tests --- src/logging/reportingLogger.ts | 10 ++-------- test/jest/reportingLogger.spec.ts | 13 ++++++------- 2 files changed, 8 insertions(+), 15 deletions(-) diff --git a/src/logging/reportingLogger.ts b/src/logging/reportingLogger.ts index 73d46376c..9ee5b5add 100644 --- a/src/logging/reportingLogger.ts +++ b/src/logging/reportingLogger.ts @@ -16,6 +16,7 @@ export class ReportingLogger implements IReportingLogger { constructor( private baseUrl: string, + private readonly isFeatureFlagEnabled: boolean, private readonly sdkVersion: string, private readonly accountId: string, private readonly roktLauncherInstanceGuid: string, @@ -64,7 +65,7 @@ export class ReportingLogger implements IReportingLogger { private isReportingEnabled(): boolean { return ( this.isRoktDomainPresent() && - (this.isFeatureFlagEnabled() || + (this.isFeatureFlagEnabled || this.isDebugModeEnabled()) ); } @@ -73,13 +74,6 @@ export class ReportingLogger implements IReportingLogger { return Boolean(window['ROKT_DOMAIN']); } - private isFeatureFlagEnabled(): boolean { - return window. - mParticle?. - config?. - isWebSdkLoggingEnabled ?? false; - } - private isDebugModeEnabled(): boolean { return ( window. diff --git a/test/jest/reportingLogger.spec.ts b/test/jest/reportingLogger.spec.ts index b3f4fe5ce..3f2344f3e 100644 --- a/test/jest/reportingLogger.spec.ts +++ b/test/jest/reportingLogger.spec.ts @@ -25,7 +25,7 @@ describe('ReportingLogger', () => { ROKT_DOMAIN: 'set', fetch: mockFetch }); - logger = new ReportingLogger(baseUrl, sdkVersion, accountId, roktLauncherInstanceGuid); + logger = new ReportingLogger(baseUrl, true, sdkVersion, accountId, roktLauncherInstanceGuid); }); afterEach(() => { @@ -72,15 +72,14 @@ describe('ReportingLogger', () => { it('does not log if ROKT_DOMAIN missing', () => { delete (globalThis as any).ROKT_DOMAIN; - logger = new ReportingLogger(baseUrl, sdkVersion, accountId, roktLauncherInstanceGuid); + 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.mParticle.config.isWebSdkLoggingEnabled = false; window.location.search = ''; - logger = new ReportingLogger(baseUrl, sdkVersion, accountId, roktLauncherInstanceGuid); + logger = new ReportingLogger(baseUrl, false, sdkVersion, accountId, roktLauncherInstanceGuid); logger.error('x', ErrorCodes.UNHANDLED_EXCEPTION); expect(mockFetch).not.toHaveBeenCalled(); }); @@ -88,7 +87,7 @@ describe('ReportingLogger', () => { 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, sdkVersion, accountId, roktLauncherInstanceGuid); + logger = new ReportingLogger(baseUrl, false, sdkVersion, accountId, roktLauncherInstanceGuid); logger.error('x', ErrorCodes.UNHANDLED_EXCEPTION); expect(mockFetch).toHaveBeenCalled(); }); @@ -100,14 +99,14 @@ describe('ReportingLogger', () => { return ++count > 3; }), }; - logger = new ReportingLogger(baseUrl, sdkVersion, accountId, roktLauncherInstanceGuid, mockRateLimiter); + 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, sdkVersion, '', roktLauncherInstanceGuid); + logger = new ReportingLogger(baseUrl, true, sdkVersion, '', roktLauncherInstanceGuid); logger.error('msg', ErrorCodes.UNHANDLED_EXCEPTION); expect(mockFetch).toHaveBeenCalled(); const fetchCall = mockFetch.mock.calls[0];