From 388b1ac98f329a4ed553fc33254607214c791a0f Mon Sep 17 00:00:00 2001 From: Guillermo Tamanaha Date: Mon, 24 Nov 2025 16:39:49 -0300 Subject: [PATCH 01/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 3ebff5c7c..7394673ae 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); @@ -1549,8 +1550,9 @@ function createIdentityCache(mpInstance) { }); } -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 b5f47a919..0b9b77394 100644 --- a/src/sdkRuntimeModels.ts +++ b/src/sdkRuntimeModels.ts @@ -311,6 +311,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 dc68dd3ffaf411ed39b1142eed5c8d8a65e72498 Mon Sep 17 00:00:00 2001 From: Guillermo Tamanaha Date: Tue, 25 Nov 2025 13:29:55 -0300 Subject: [PATCH 02/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 8a2e1bc6ce07b39c004073c1f3174061315ffaf4 Mon Sep 17 00:00:00 2001 From: Guillermo Tamanaha Date: Tue, 25 Nov 2025 13:36:34 -0300 Subject: [PATCH 03/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 d10087953e4f8ec0c13ddf1c66946845a4ec3896 Mon Sep 17 00:00:00 2001 From: Guillermo Tamanaha Date: Thu, 27 Nov 2025 10:26:37 -0300 Subject: [PATCH 04/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 7394673ae..3ebff5c7c 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); @@ -1550,9 +1549,8 @@ function createIdentityCache(mpInstance) { }); } -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 f0182bcf0aa605109fdb3b0467d005a0600766c1 Mon Sep 17 00:00:00 2001 From: Guillermo Tamanaha Date: Thu, 27 Nov 2025 11:13:51 -0300 Subject: [PATCH 05/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 4c5d663bd4bd5bae107783a6640a89a8cf569a25 Mon Sep 17 00:00:00 2001 From: Guillermo Tamanaha Date: Thu, 27 Nov 2025 13:10:46 -0300 Subject: [PATCH 06/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 36e2a4f3836bdc32dd78f915c5aa19fc510b327e Mon Sep 17 00:00:00 2001 From: Guillermo Tamanaha Date: Thu, 27 Nov 2025 17:03:00 -0300 Subject: [PATCH 07/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 6a94aeba0f027e11a19b100512ee99a731d12bb9 Mon Sep 17 00:00:00 2001 From: Guillermo Tamanaha Date: Thu, 27 Nov 2025 17:08:27 -0300 Subject: [PATCH 08/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 c5085ea6a086c917714a8dd86cff5596b356dc5a Mon Sep 17 00:00:00 2001 From: Guillermo Tamanaha Date: Thu, 27 Nov 2025 17:15:55 -0300 Subject: [PATCH 09/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 db1d40c866a35ae98e7159ab965ad62c5ff399a2 Mon Sep 17 00:00:00 2001 From: Guillermo Tamanaha Date: Tue, 2 Dec 2025 14:37:13 -0300 Subject: [PATCH 10/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 d0787853bb3532ea02df37c2e975e4bc178bec09 Mon Sep 17 00:00:00 2001 From: Guillermo Tamanaha Date: Tue, 2 Dec 2025 14:38:51 -0300 Subject: [PATCH 11/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 a88b8df9d7df856aecb8ca7efc4fa752d367fa63 Mon Sep 17 00:00:00 2001 From: Guillermo Tamanaha Date: Fri, 5 Dec 2025 12:20:44 -0300 Subject: [PATCH 12/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 2db99dd8570a5c8babb362894f60ac9462f20c00 Mon Sep 17 00:00:00 2001 From: Guillermo Tamanaha Date: Tue, 9 Dec 2025 13:52:35 -0300 Subject: [PATCH 13/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 f7db566ecc161b7d9b2ae76a7aab056defc935e3 Mon Sep 17 00:00:00 2001 From: Guillermo Tamanaha Date: Wed, 10 Dec 2025 09:29:07 -0300 Subject: [PATCH 14/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 f3e25febf216630251507b8acd13e0221cfb1029 Mon Sep 17 00:00:00 2001 From: Guillermo Tamanaha Date: Wed, 10 Dec 2025 09:35:59 -0300 Subject: [PATCH 15/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 b972c57803dd1a6df225565d98b03549d119832a Mon Sep 17 00:00:00 2001 From: Alexander Sapountzis Date: Wed, 17 Dec 2025 16:30:39 -0500 Subject: [PATCH 16/19] Revise implementation of ReportingLogger --- src/constants.ts | 4 ++ src/identityApiClient.ts | 8 ++- src/logger.ts | 16 +++-- src/logging/logRequest.ts | 38 ++++++----- src/logging/reportingLogger.ts | 121 +++++++++++++++++++++------------ src/mp-instance.ts | 26 ++++++- src/roktManager.ts | 1 + src/sdkRuntimeModels.ts | 7 +- src/store.ts | 2 + 9 files changed, 150 insertions(+), 73 deletions(-) diff --git a/src/constants.ts b/src/constants.ts index bf513a5d1..c5788b568 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -139,6 +139,8 @@ const Constants = { identityUrl: 'identity.mparticle.com/v1/', aliasUrl: 'jssdks.mparticle.com/v1/identity/', userAudienceUrl: 'nativesdks.mparticle.com/v1/', + loggingUrl: 'apps.rokt.com/v1/log/', + errorUrl: 'apps.rokt.com/v1/errors/', }, // These are the paths that are used to construct the CNAME urls CNAMEUrlPaths: { @@ -148,6 +150,8 @@ const Constants = { configUrl: '/tags/JS/v2/', identityUrl: '/identity/v1/', aliasUrl: '/webevents/v1/identity/', + loggingUrl: '/v1/log/', + errorUrl: '/v1/errors/', }, Base64CookieKeys: { csm: 1, diff --git a/src/identityApiClient.ts b/src/identityApiClient.ts index e357a645d..a32e0975d 100644 --- a/src/identityApiClient.ts +++ b/src/identityApiClient.ts @@ -25,6 +25,7 @@ import { IIdentityResponse, } from './identity-user-interfaces'; import { IMParticleWebSDKInstance } from './mp-instance'; +import { ErrorCodes } from './logging/errorCodes'; const { HTTPCodes, Messages, IdentityMethods } = Constants; @@ -300,10 +301,13 @@ export default function IdentityAPIClient( ); } catch (err) { mpInstance._Store.identityCallInFlight = false; - + const errorMessage = (err as Error).message || err.toString(); - Logger.error('Error sending identity request to servers' + ' - ' + errorMessage); + Logger.error( + 'Error sending identity request to servers' + ' - ' + errorMessage, + ErrorCodes.UNHANDLED_EXCEPTION + ); invokeCallback( callback, HTTPCodes.noHttpCoverage, diff --git a/src/logger.ts b/src/logger.ts index 6ec19c056..324cf9dd0 100644 --- a/src/logger.ts +++ b/src/logger.ts @@ -1,4 +1,6 @@ import { LogLevelType, SDKInitConfig, SDKLoggerApi } from './sdkRuntimeModels'; +import { IReportingLogger } from './logging/reportingLogger'; +import { ErrorCodes } from './logging/errorCodes'; export type ILoggerConfig = Pick; export type IConsoleLogger = Partial>; @@ -6,10 +8,14 @@ export type IConsoleLogger = Partial; + code: ErrorCode; + severity: WSDKErrorSeverity; + stackTrace?: string; + deviceInfo?: string; + integration?: string; + reporter?: string; + url?: string; + }; + +export type LogRequestBody = ErrorsRequestBody; \ No newline at end of file diff --git a/src/logging/reportingLogger.ts b/src/logging/reportingLogger.ts index b4ef54c03..a3799157f 100644 --- a/src/logging/reportingLogger.ts +++ b/src/logging/reportingLogger.ts @@ -1,11 +1,11 @@ - import { ErrorCodes } from "./errorCodes"; -import { LogRequest, LogRequestSeverity } from "./logRequest"; -import { FetchUploader, XHRUploader } from "../uploaders"; +import { LogRequestBody, WSDKErrorSeverity } from "./logRequest"; +import { FetchUploader, IFetchPayload } from "../uploaders"; +// QUESTION: Should we collapse the interface with the class? export interface IReportingLogger { - error(msg: string, code?: ErrorCodes, stackTrace?: string): void; - warning(msg: string, code?: ErrorCodes): void; + error(msg: string, code: ErrorCodes, stackTrace?: string): void; + warning(msg: string, code: ErrorCodes): void; } export class ReportingLogger implements IReportingLogger { @@ -13,30 +13,54 @@ 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'; - + private loggingUrl: string; + private errorUrl: string; + private accountId: string; + private integrationName: string; + constructor( - private baseUrl: string, private readonly sdkVersion: string, - private readonly accountId: string, - rateLimiter?: IRateLimiter, + rateLimiter?: IRateLimiter, // QUESTION: Do we need this in the constructor? + private readonly launcherInstanceGuid?: string, ) { this.isEnabled = this.isReportingEnabled(); + console.warn('ReportingLogger: isEnabled', this.isEnabled); this.rateLimiter = rateLimiter ?? new RateLimiter(); } - public error(msg: string, code?: ErrorCodes, stackTrace?: string) { - this.sendLog(LogRequestSeverity.Error, msg, code ?? ErrorCodes.UNHANDLED_EXCEPTION, stackTrace); + public setLoggingUrl(url: string) { + this.loggingUrl = url; + } + + public setErrorUrl(url: string) { + this.errorUrl = url; + } + + public setAccountId(accountId: string) { + this.accountId = accountId; + } + + public setIntegrationName(integrationName: string) { + this.integrationName = integrationName; + } + + // TODO: Add an `info` method to the logger for `/v1/log` + + 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 getVersion(): string { + return this.integrationName ?? this.sdkVersion; + } + + // QUESTION: Should we split this into `sendError` and `sendLog`? private sendLog( - severity: LogRequestSeverity, + severity: WSDKErrorSeverity, msg: string, code: ErrorCodes, stackTrace?: string @@ -44,16 +68,16 @@ export class ReportingLogger implements IReportingLogger { if(!this.canSendLog(severity)) return; - const logRequest: LogRequest = { + const logRequest: LogRequestBody = { additionalInformation: { message: msg, - version: this.sdkVersion, + version: this.getVersion(), }, severity: severity, code: code, url: this.getUrl(), deviceInfo: this.getUserAgent(), - stackTrace: stackTrace ?? '', + stackTrace: stackTrace ?? 'this is my stack trace', reporter: this.reporter, integration: this.integration, }; @@ -62,6 +86,8 @@ export class ReportingLogger implements IReportingLogger { } private isReportingEnabled(): boolean { + // QUESTION: Should isDebugModeEnabled take precedence over + // isFeatureFlagEnabled and rokt domain present? return ( this.isRoktDomainPresent() && (this.isFeatureFlagEnabled() || @@ -90,53 +116,62 @@ 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; + return window.location.href; } private getUserAgent(): string { - return window?.navigator?.userAgent ?? this.DEFAULT_USER_AGENT; + return window.navigator.userAgent; } - private sendLogToServer(logRequest: LogRequest) { - const uploadUrl = `${this.baseUrl}/v1/log`; - const uploader = window.fetch - ? new FetchUploader(uploadUrl) - : new XHRUploader(uploadUrl); + private getLoggingUrl = (): string => `https://${this.loggingUrl}`; + private getErrorUrl = (): string => `https://${this.errorUrl}`; + + private getHeaders(): IFetchPayload['headers'] { + const headers: Record = { + Accept: 'text/plain;charset=UTF-8', + 'Content-Type': 'application/json', + 'rokt-launcher-instance-guid': this.launcherInstanceGuid, + 'rokt-launcher-version': this.getVersion(), + 'rokt-wsdk-version': 'joint', + }; + return headers as IFetchPayload['headers']; + } - uploader.upload({ + private sendLogToServer(logRequest: LogRequestBody) { + const uploadUrl = this.getErrorUrl(); + const uploader = new FetchUploader(uploadUrl); + const payload: IFetchPayload = { 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 - }, + headers: this.getHeaders(), body: JSON.stringify(logRequest), - }); + }; + + uploader.upload(payload); }; } 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/mp-instance.ts b/src/mp-instance.ts index 3ebff5c7c..3b766758c 100644 --- a/src/mp-instance.ts +++ b/src/mp-instance.ts @@ -51,6 +51,7 @@ import { IPersistence } from './persistence.interfaces'; import ForegroundTimer from './foregroundTimeTracker'; import RoktManager, { IRoktOptions } from './roktManager'; import filteredMparticleUser from './filteredMparticleUser'; +import { IReportingLogger, ReportingLogger } from './logging/reportingLogger'; export interface IErrorLogMessage { message?: string; @@ -82,6 +83,7 @@ export interface IMParticleWebSDKInstance extends MParticleWebSDK { _IntegrationCapture: IntegrationCapture; _NativeSdkHelpers: INativeSdkHelpers; _Persistence: IPersistence; + _ReportingLogger: IReportingLogger; _RoktManager: RoktManager; _SessionManager: ISessionManager; _ServerModel: IServerModel; @@ -90,6 +92,7 @@ export interface IMParticleWebSDKInstance extends MParticleWebSDK { _preInit: IPreInit; _timeOnSiteTimer: ForegroundTimer; setLauncherInstanceGuid: () => void; + getLauncherInstanceGuid: () => string; captureTiming(metricName: string); } @@ -223,11 +226,11 @@ export default function mParticleInstance(this: IMParticleWebSDKInstance, instan } }; - this._resetForTests = function(config, keepPersistence, instance) { + this._resetForTests = function(config, keepPersistence, instance, reportingLogger?: IReportingLogger) { if (instance._Store) { delete instance._Store; } - instance.Logger = new Logger(config); + instance.Logger = new Logger(config, reportingLogger); instance._Store = new Store(config, instance); instance._Store.isLocalStorageAvailable = instance._Persistence.determineLocalStorageAvailability( window.localStorage @@ -1352,6 +1355,10 @@ export default function mParticleInstance(this: IMParticleWebSDKInstance, instan window[launcherInstanceGuidKey] = self._Helpers.generateUniqueId(); } }; + + this.getLauncherInstanceGuid = function() { + return window[launcherInstanceGuidKey]; + }; this.captureTiming = function(metricsName) { if (typeof window !== 'undefined' && window.performance?.mark) { @@ -1418,6 +1425,7 @@ function completeSDKInitialization(apiKey, config, mpInstance) { // Configure Rokt Manager with user and filtered user const roktConfig: IKitConfigs = parseConfig(config, 'Rokt', 181); if (roktConfig) { + mpInstance._ReportingLogger.accountId = roktConfig.settings?.accountId ?? ''; const { userAttributeFilters } = roktConfig; const roktFilteredUser = filteredMparticleUser( currentUserMPID, @@ -1550,10 +1558,22 @@ function createIdentityCache(mpInstance) { } function runPreConfigFetchInitialization(mpInstance, apiKey, config) { - mpInstance.Logger = new Logger(config); + // QUESTION: Should Store come before ReportingLogger? + // Logger needs the store to generate the url + // But Store needs the Logger to log errors + mpInstance._ReportingLogger = new ReportingLogger( + Constants.sdkVersion, + undefined, // QUESTION: Do we need a RateLimiter?? + mpInstance.getLauncherInstanceGuid() + ); + mpInstance.Logger = new Logger(config, mpInstance._ReportingLogger); mpInstance._Store = new Store(config, mpInstance, apiKey); window.mParticle.Store = mpInstance._Store; mpInstance.Logger.verbose(StartingInitialization); + + // TODO: Extract urls from config into a url builder + mpInstance._ReportingLogger.loggingUrl = mpInstance._Store.SDKConfig.loggingUrl; + mpInstance._ReportingLogger.errorUrl = mpInstance._Store.SDKConfig.errorUrl; // Check to see if localStorage is available before main configuration runs // since we will need this for the current implementation of user persistence diff --git a/src/roktManager.ts b/src/roktManager.ts index 0039d7d35..38cbbafcf 100644 --- a/src/roktManager.ts +++ b/src/roktManager.ts @@ -155,6 +155,7 @@ export default class RoktManager { } public attachKit(kit: IRoktKit): void { + // TODO: Pass back integrationName via kit this.kit = kit; this.processMessageQueue(); } diff --git a/src/sdkRuntimeModels.ts b/src/sdkRuntimeModels.ts index 0b9b77394..c9e62b404 100644 --- a/src/sdkRuntimeModels.ts +++ b/src/sdkRuntimeModels.ts @@ -41,6 +41,7 @@ import { IErrorLogMessage, IMParticleWebSDKInstance, IntegrationDelays } from '. import Constants from './constants'; import RoktManager, { IRoktLauncherOptions } from './roktManager'; import { IConsoleLogger } from './logger'; +import { ErrorCodes } from './logging/errorCodes'; // TODO: Resolve this with version in @mparticle/web-sdk export type SDKEventCustomFlags = Dictionary; @@ -362,9 +363,9 @@ export interface SDKHelpersApi { } export interface SDKLoggerApi { - error(arg0: string): void; - verbose(arg0: string): void; - warning(arg0: string): void; + error(msg: string, code?: ErrorCodes): void; + verbose(msg: string): void; + warning(msg: string, code?: ErrorCodes): void; setLogLevel(logLevel: LogLevelType): void; } diff --git a/src/store.ts b/src/store.ts index 64d560dc8..f17fe4971 100644 --- a/src/store.ts +++ b/src/store.ts @@ -96,6 +96,8 @@ export interface SDKConfig { webviewBridgeName?: string; workspaceToken?: string; requiredWebviewBridgeName?: string; + loggingUrl?: string; + errorUrl?: string; } function createSDKConfig(config: SDKInitConfig): SDKConfig { From b3525cdfce4c931ebdcd5a1aa7963b02d0c40fed Mon Sep 17 00:00:00 2001 From: Alexander Sapountzis Date: Thu, 18 Dec 2025 16:46:35 -0500 Subject: [PATCH 17/19] Revised implementation --- src/logger.ts | 2 +- src/logging/errorCodes.ts | 1 + src/logging/reportingLogger.ts | 113 +++++++++---------- src/mp-instance.ts | 25 +++-- src/sdkConfigManager.ts | 177 ++++++++++++++++++++++++++++++ src/store.ts | 45 ++------ test/jest/reportingLogger.spec.ts | 74 +++++++++---- 7 files changed, 308 insertions(+), 129 deletions(-) create mode 100644 src/sdkConfigManager.ts diff --git a/src/logger.ts b/src/logger.ts index 324cf9dd0..59e03d0c8 100644 --- a/src/logger.ts +++ b/src/logger.ts @@ -34,7 +34,7 @@ export class Logger { if (this.logger.warning && (this.logLevel === LogLevelType.Verbose || this.logLevel === LogLevelType.Warning)) { this.logger.warning(msg); - this.reportingLogger.warning(msg, code); + this.reportingLogger?.warning(msg, code); } } diff --git a/src/logging/errorCodes.ts b/src/logging/errorCodes.ts index ae99429b1..11d3b7045 100644 --- a/src/logging/errorCodes.ts +++ b/src/logging/errorCodes.ts @@ -3,5 +3,6 @@ import { valueof } from '../utils'; export type ErrorCodes = valueof; export const ErrorCodes = { + UNKNOWN_ERROR: 'UNKNOWN_ERROR', UNHANDLED_EXCEPTION: 'UNHANDLED_EXCEPTION', } as const; \ No newline at end of file diff --git a/src/logging/reportingLogger.ts b/src/logging/reportingLogger.ts index a3799157f..f0c47cdf3 100644 --- a/src/logging/reportingLogger.ts +++ b/src/logging/reportingLogger.ts @@ -1,11 +1,12 @@ import { ErrorCodes } from "./errorCodes"; import { LogRequestBody, WSDKErrorSeverity } from "./logRequest"; import { FetchUploader, IFetchPayload } from "../uploaders"; +import { IStore, SDKConfig } from "../store"; // QUESTION: Should we collapse the interface with the class? export interface IReportingLogger { - error(msg: string, code: ErrorCodes, stackTrace?: string): void; - warning(msg: string, code: ErrorCodes): void; + error(msg: string, code?: ErrorCodes, stackTrace?: string): void; + warning(msg: string, code?: ErrorCodes): void; } export class ReportingLogger implements IReportingLogger { @@ -13,78 +14,82 @@ export class ReportingLogger implements IReportingLogger { private readonly reporter: string = 'mp-wsdk'; private readonly integration: string = 'mp-wsdk'; private readonly rateLimiter: IRateLimiter; - private loggingUrl: string; - private errorUrl: string; - private accountId: string; private integrationName: string; + private store: IStore; constructor( + private readonly config: SDKConfig, private readonly sdkVersion: string, - rateLimiter?: IRateLimiter, // QUESTION: Do we need this in the constructor? private readonly launcherInstanceGuid?: string, + rateLimiter?: IRateLimiter, ) { this.isEnabled = this.isReportingEnabled(); - console.warn('ReportingLogger: isEnabled', this.isEnabled); this.rateLimiter = rateLimiter ?? new RateLimiter(); } - public setLoggingUrl(url: string) { - this.loggingUrl = url; - } - - public setErrorUrl(url: string) { - this.errorUrl = url; - } - - public setAccountId(accountId: string) { - this.accountId = accountId; - } - public setIntegrationName(integrationName: string) { this.integrationName = integrationName; } + + public setStore(store: IStore) { + this.store = store; + } - // TODO: Add an `info` method to the logger for `/v1/log` + public info(msg: string, code?: ErrorCodes) { + this.sendLog(WSDKErrorSeverity.INFO, msg, code); + } - public error(msg: string, code: ErrorCodes, stackTrace?: string) { - this.sendLog(WSDKErrorSeverity.ERROR, msg, code, stackTrace); + public error(msg: string, code?: ErrorCodes, stackTrace?: string) { + this.sendError(WSDKErrorSeverity.ERROR, msg, code, stackTrace); }; - public warning(msg: string, code: ErrorCodes) { - this.sendLog(WSDKErrorSeverity.WARNING, msg, code); + public warning(msg: string, code?: ErrorCodes) { + this.sendError(WSDKErrorSeverity.WARNING, msg, code); }; - private getVersion(): string { - return this.integrationName ?? this.sdkVersion; - } - - // QUESTION: Should we split this into `sendError` and `sendLog`? - private sendLog( - severity: WSDKErrorSeverity, - msg: string, - code: ErrorCodes, - stackTrace?: string - ): void { + private sendToServer(url: string,severity: WSDKErrorSeverity, msg: string, code?: ErrorCodes, stackTrace?: string): void { if(!this.canSendLog(severity)) return; + + const logRequest = this.getLogRequest(severity, msg, code, stackTrace); + const uploader = new FetchUploader(url); + const payload: IFetchPayload = { + method: 'POST', + headers: this.getHeaders(), + body: JSON.stringify(logRequest), + }; + uploader.upload(payload); + } - const logRequest: LogRequestBody = { + private sendLog(severity: WSDKErrorSeverity, msg: string, code?: ErrorCodes, stackTrace?: string): void { + const url = this.getLoggingUrl(); + this.sendToServer(url, severity, msg, code, stackTrace); + } + private sendError(severity: WSDKErrorSeverity, msg: string, code?: ErrorCodes, stackTrace?: string): void { + const url = this.getErrorUrl(); + this.sendToServer(url, severity, msg, code, stackTrace); + } + + private getLogRequest(severity: WSDKErrorSeverity, msg: string, code?: ErrorCodes, stackTrace?: string): LogRequestBody { + return { additionalInformation: { message: msg, version: this.getVersion(), }, severity: severity, - code: code, + code: code ?? ErrorCodes.UNKNOWN_ERROR, url: this.getUrl(), deviceInfo: this.getUserAgent(), - stackTrace: stackTrace ?? 'this is my stack trace', + stackTrace: stackTrace ?? '', reporter: this.reporter, - integration: this.integration, + integration: this.integration }; - - this.sendLogToServer(logRequest); } + private getVersion(): string { + return this.integrationName ?? this.sdkVersion; + } + private isReportingEnabled(): boolean { // QUESTION: Should isDebugModeEnabled take precedence over // isFeatureFlagEnabled and rokt domain present? @@ -100,10 +105,7 @@ export class ReportingLogger implements IReportingLogger { } private isFeatureFlagEnabled(): boolean { - return window. - mParticle?. - config?. - isWebSdkLoggingEnabled ?? false; + return this.config.isWebSdkLoggingEnabled; } private isDebugModeEnabled(): boolean { @@ -132,8 +134,8 @@ export class ReportingLogger implements IReportingLogger { return window.navigator.userAgent; } - private getLoggingUrl = (): string => `https://${this.loggingUrl}`; - private getErrorUrl = (): string => `https://${this.errorUrl}`; + private getLoggingUrl = (): string => `https://${this.config.loggingUrl}`; + private getErrorUrl = (): string => `https://${this.config.errorUrl}`; private getHeaders(): IFetchPayload['headers'] { const headers: Record = { @@ -143,20 +145,13 @@ export class ReportingLogger implements IReportingLogger { 'rokt-launcher-version': this.getVersion(), 'rokt-wsdk-version': 'joint', }; + + if (this.store?.getRoktAccountId()) { + headers['rokt-account-id'] = this.store.getRoktAccountId(); + } + return headers as IFetchPayload['headers']; } - - private sendLogToServer(logRequest: LogRequestBody) { - const uploadUrl = this.getErrorUrl(); - const uploader = new FetchUploader(uploadUrl); - const payload: IFetchPayload = { - method: 'POST', - headers: this.getHeaders(), - body: JSON.stringify(logRequest), - }; - - uploader.upload(payload); - }; } export interface IRateLimiter { diff --git a/src/mp-instance.ts b/src/mp-instance.ts index 3b766758c..a0fb9d03f 100644 --- a/src/mp-instance.ts +++ b/src/mp-instance.ts @@ -41,7 +41,7 @@ import { LocalStorageVault } from './vault'; import { removeExpiredIdentityCacheDates } from './identity-utils'; import IntegrationCapture from './integrationCapture'; import { IPreInit, processReadyQueue } from './pre-init-utils'; -import { BaseEvent, MParticleWebSDK, SDKHelpersApi } from './sdkRuntimeModels'; +import { BaseEvent, MParticleWebSDK, SDKHelpersApi, SDKInitConfig } from './sdkRuntimeModels'; import { Dictionary, SDKEventAttrs } from '@mparticle/web-sdk'; import { IIdentity } from './identity.interfaces'; import { IEvents } from './events.interfaces'; @@ -52,6 +52,7 @@ import ForegroundTimer from './foregroundTimeTracker'; import RoktManager, { IRoktOptions } from './roktManager'; import filteredMparticleUser from './filteredMparticleUser'; import { IReportingLogger, ReportingLogger } from './logging/reportingLogger'; +import { SDKConfigManager } from './sdkConfigManager'; export interface IErrorLogMessage { message?: string; @@ -1425,7 +1426,7 @@ function completeSDKInitialization(apiKey, config, mpInstance) { // Configure Rokt Manager with user and filtered user const roktConfig: IKitConfigs = parseConfig(config, 'Rokt', 181); if (roktConfig) { - mpInstance._ReportingLogger.accountId = roktConfig.settings?.accountId ?? ''; + mpInstance._Store.setRoktAccountId(roktConfig.settings?.accountId ?? undefined); const { userAttributeFilters } = roktConfig; const roktFilteredUser = filteredMparticleUser( currentUserMPID, @@ -1558,22 +1559,22 @@ function createIdentityCache(mpInstance) { } function runPreConfigFetchInitialization(mpInstance, apiKey, config) { - // QUESTION: Should Store come before ReportingLogger? - // Logger needs the store to generate the url - // But Store needs the Logger to log errors + + const sdkConfig = new SDKConfigManager(config, apiKey).getSDKConfig(); mpInstance._ReportingLogger = new ReportingLogger( + sdkConfig, Constants.sdkVersion, - undefined, // QUESTION: Do we need a RateLimiter?? - mpInstance.getLauncherInstanceGuid() + mpInstance.getLauncherInstanceGuid(), ); mpInstance.Logger = new Logger(config, mpInstance._ReportingLogger); - mpInstance._Store = new Store(config, mpInstance, apiKey); + mpInstance._Store = new Store( + { ...config, ...sdkConfig } as SDKInitConfig, + mpInstance, + apiKey + ); window.mParticle.Store = mpInstance._Store; + mpInstance._ReportingLogger.setStore(mpInstance._Store); mpInstance.Logger.verbose(StartingInitialization); - - // TODO: Extract urls from config into a url builder - mpInstance._ReportingLogger.loggingUrl = mpInstance._Store.SDKConfig.loggingUrl; - mpInstance._ReportingLogger.errorUrl = mpInstance._Store.SDKConfig.errorUrl; // Check to see if localStorage is available before main configuration runs // since we will need this for the current implementation of user persistence diff --git a/src/sdkConfigManager.ts b/src/sdkConfigManager.ts new file mode 100644 index 000000000..aaf293885 --- /dev/null +++ b/src/sdkConfigManager.ts @@ -0,0 +1,177 @@ +import Constants from "./constants"; +import { SDKInitConfig } from "./sdkRuntimeModels"; +import { IFeatureFlags, SDKConfig } from "./store"; +import { Dictionary, isEmpty, parseNumber, returnConvertedBoolean } from "./utils"; + +export class SDKConfigManager { + private sdkConfig: SDKConfig; + + constructor(config: SDKInitConfig, apiKey: string){ + const sdkConfig = {} as SDKConfig; + + for (const prop in Constants.DefaultConfig) { + if (Constants.DefaultConfig.hasOwnProperty(prop)) { + config[prop] = Constants.DefaultConfig[prop]; + } + } + + if (config) { + for (const prop in config) { + if (config.hasOwnProperty(prop)) { + sdkConfig[prop] = config[prop]; + } + } + } + + for (const prop in Constants.DefaultBaseUrls) { + sdkConfig[prop] = Constants.DefaultBaseUrls[prop]; + + } + + sdkConfig.flags = this.processFlags(config); + sdkConfig.deviceId = config.deviceId ?? undefined; + sdkConfig.isDevelopmentMode = returnConvertedBoolean(config.isDevelopmentMode) ?? false; + sdkConfig.isWebSdkLoggingEnabled = returnConvertedBoolean(config.isWebSdkLoggingEnabled) ?? false; + sdkConfig.logLevel = config.logLevel ?? undefined; + + const baseUrls: Dictionary = processBaseUrls( + config, + sdkConfig.flags, + apiKey, + ); + + for (const baseUrlKeys in baseUrls) { + sdkConfig[baseUrlKeys] = baseUrls[baseUrlKeys]; + } + + this.sdkConfig = sdkConfig; + } + + public getSDKConfig(): SDKConfig { + return this.sdkConfig; + } + + private processFlags(config: SDKInitConfig): IFeatureFlags { + const flags: IFeatureFlags = {}; + const { + ReportBatching, + EventBatchingIntervalMillis, + OfflineStorage, + DirectUrlRouting, + CacheIdentity, + AudienceAPI, + CaptureIntegrationSpecificIds, + CaptureIntegrationSpecificIdsV2, + AstBackgroundEvents + } = Constants.FeatureFlags; + + if (!config.flags) { + return {}; + } + + // https://go.mparticle.com/work/SQDSDKS-6317 + // Passed in config flags take priority over defaults + flags[ReportBatching] = config.flags[ReportBatching] || false; + // The server returns stringified numbers, sowe need to parse + flags[EventBatchingIntervalMillis] = + parseNumber(config.flags[EventBatchingIntervalMillis]) || + Constants.DefaultConfig.uploadInterval; + flags[OfflineStorage] = config.flags[OfflineStorage] || '0'; + flags[DirectUrlRouting] = config.flags[DirectUrlRouting] === 'True'; + flags[CacheIdentity] = config.flags[CacheIdentity] === 'True'; + flags[AudienceAPI] = config.flags[AudienceAPI] === 'True'; + flags[CaptureIntegrationSpecificIds] = config.flags[CaptureIntegrationSpecificIds] === 'True'; + flags[CaptureIntegrationSpecificIdsV2] = (config.flags[CaptureIntegrationSpecificIdsV2] || 'none'); + flags[AstBackgroundEvents] = config.flags[AstBackgroundEvents] === 'True'; + return flags; + } + + +} + +function processBaseUrls(config: SDKInitConfig, flags: IFeatureFlags, apiKey: string): Dictionary { + // an API key is not present in a webview only mode. In this case, no baseUrls are needed + if (!apiKey) { + return {}; + } + + // When direct URL routing is false, update baseUrls based custom urls + // passed to the config + if (flags.directURLRouting) { + return processDirectBaseUrls(config, apiKey); + } else { + return processCustomBaseUrls(config); + } +} + +function processCustomBaseUrls(config: SDKInitConfig): Dictionary { + const defaultBaseUrls: Dictionary = Constants.DefaultBaseUrls; + const CNAMEUrlPaths: Dictionary = Constants.CNAMEUrlPaths; + + // newBaseUrls are default if the customer is not using a CNAME + // If a customer passes either config.domain or config.v3SecureServiceUrl, + // config.identityUrl, etc, the customer is using a CNAME. + // config.domain will take priority if a customer passes both. + const newBaseUrls: Dictionary = {}; + // If config.domain exists, the customer is using a CNAME. We append the url paths to the provided domain. + // This flag is set on the Rokt/MP snippet (starting at version 2.6), meaning config.domain will alwys be empty + // if a customer is using a snippet prior to 2.6. + if (!isEmpty(config.domain)) { + for (let pathKey in CNAMEUrlPaths) { + newBaseUrls[pathKey] = `${config.domain}${CNAMEUrlPaths[pathKey]}`; + } + + return newBaseUrls; + } + + for (let baseUrlKey in defaultBaseUrls) { + newBaseUrls[baseUrlKey] = + config[baseUrlKey] || defaultBaseUrls[baseUrlKey]; + } + + return newBaseUrls; +} + +function processDirectBaseUrls( + config: SDKInitConfig, + apiKey: string +): Dictionary { + const defaultBaseUrls = Constants.DefaultBaseUrls; + const directBaseUrls: Dictionary = {}; + // When Direct URL Routing is true, we create a new set of baseUrls that + // include the silo in the urls. mParticle API keys are prefixed with the + // silo and a hyphen (ex. "us1-", "us2-", "eu1-"). us1 was the first silo, + // and before other silos existed, there were no prefixes and all apiKeys + // were us1. As such, if we split on a '-' and the resulting array length + // is 1, then it is an older APIkey that should route to us1. + // When splitKey.length is greater than 1, then splitKey[0] will be + // us1, us2, eu1, au1, or st1, etc as new silos are added + const DEFAULT_SILO = 'us1'; + const splitKey: Array = apiKey.split('-'); + const routingPrefix: string = + splitKey.length <= 1 ? DEFAULT_SILO : splitKey[0]; + + for (let baseUrlKey in defaultBaseUrls) { + // Any custom endpoints passed to mpConfig will take priority over direct + // mapping to the silo. The most common use case is a customer provided CNAME. + if (baseUrlKey === 'configUrl') { + directBaseUrls[baseUrlKey] = + config[baseUrlKey] || defaultBaseUrls[baseUrlKey]; + continue; + } + + if (config.hasOwnProperty(baseUrlKey)) { + directBaseUrls[baseUrlKey] = config[baseUrlKey]; + } else { + const urlparts = defaultBaseUrls[baseUrlKey].split('.'); + + directBaseUrls[baseUrlKey] = [ + urlparts[0], + routingPrefix, + ...urlparts.slice(1), + ].join('.'); + } + } + + return directBaseUrls; +} \ No newline at end of file diff --git a/src/store.ts b/src/store.ts index f17fe4971..6c28023f9 100644 --- a/src/store.ts +++ b/src/store.ts @@ -98,6 +98,7 @@ export interface SDKConfig { requiredWebviewBridgeName?: string; loggingUrl?: string; errorUrl?: string; + isWebSdkLoggingEnabled?: boolean; } function createSDKConfig(config: SDKInitConfig): SDKConfig { @@ -202,6 +203,7 @@ export interface IStore { integrationDelayTimeoutStart: number; // UNIX Timestamp webviewBridgeEnabled?: boolean; wrapperSDKInfo: WrapperSDKInfo; + roktAccountId: string; persistenceData?: IPersistenceMinified; @@ -223,6 +225,8 @@ export interface IStore { setUserAttributes?(mpid: MPID, attributes: UserAttributes): void; getUserIdentities?(mpid: MPID): UserIdentities; setUserIdentities?(mpid: MPID, userIdentities: UserIdentities): void; + getRoktAccountId?(): string; + setRoktAccountId?(accountId: string): void; addMpidToSessionHistory?(mpid: MPID, previousMpid?: MPID): void; hasInvalidIdentifyRequest?: () => boolean; @@ -287,6 +291,7 @@ export default function Store( version: null, isInfoSet: false, }, + roktAccountId: null, // Placeholder for in-memory persistence model persistenceData: { @@ -305,41 +310,6 @@ export default function Store( this.SDKConfig = createSDKConfig(config); if (config) { - if (!config.hasOwnProperty('flags')) { - this.SDKConfig.flags = {}; - } - - // We process the initial config that is passed via the SDK init - // and then we will reprocess the config within the processConfig - // function when the config is updated from the server - // https://go.mparticle.com/work/SQDSDKS-6317 - this.SDKConfig.flags = processFlags(config); - - if (config.deviceId) { - this.deviceId = config.deviceId; - } - if (config.hasOwnProperty('isDevelopmentMode')) { - this.SDKConfig.isDevelopmentMode = returnConvertedBoolean( - config.isDevelopmentMode - ); - } else { - this.SDKConfig.isDevelopmentMode = false; - } - - const baseUrls: Dictionary = processBaseUrls( - config, - this.SDKConfig.flags, - apiKey - ); - - for (const baseUrlKeys in baseUrls) { - this.SDKConfig[baseUrlKeys] = baseUrls[baseUrlKeys]; - } - - if (config.hasOwnProperty('logLevel')) { - this.SDKConfig.logLevel = config.logLevel; - } - this.SDKConfig.useNativeSdk = !!config.useNativeSdk; this.SDKConfig.kits = config.kits || {}; @@ -665,6 +635,11 @@ export default function Store( this.setUserIdentities = (mpid: MPID, userIdentities: UserIdentities) => { this._setPersistence(mpid, 'ui', userIdentities); }; + + this.getRoktAccountId = () => this.roktAccountId; + this.setRoktAccountId = (accountId: string) => { + this.roktAccountId = accountId; + }; this.addMpidToSessionHistory = (mpid: MPID, previousMPID?: MPID): void => { const indexOfMPID = this.currentSessionMPIDs.indexOf(mpid); diff --git a/test/jest/reportingLogger.spec.ts b/test/jest/reportingLogger.spec.ts index 02d9a33b0..61a7f8349 100644 --- a/test/jest/reportingLogger.spec.ts +++ b/test/jest/reportingLogger.spec.ts @@ -1,6 +1,7 @@ import { IRateLimiter, RateLimiter, ReportingLogger } from '../../src/logging/reportingLogger'; -import { LogRequestSeverity } from '../../src/logging/logRequest'; +import { WSDKErrorSeverity } from '../../src/logging/logRequest'; import { ErrorCodes } from '../../src/logging/errorCodes'; +import { SDKConfig } from '../../src/store'; describe('ReportingLogger', () => { let logger: ReportingLogger; @@ -24,7 +25,11 @@ describe('ReportingLogger', () => { ROKT_DOMAIN: 'set', fetch: mockFetch }); - logger = new ReportingLogger(baseUrl, sdkVersion, accountId); + logger = new ReportingLogger( + { loggingUrl: baseUrl, errorUrl: baseUrl } as SDKConfig, + sdkVersion, + 'test-launcher-instance-guid' + ); }); afterEach(() => { @@ -40,7 +45,7 @@ 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' }); @@ -53,14 +58,18 @@ describe('ReportingLogger', () => { expect(fetchCall[0]).toContain('/v1/log'); const body = JSON.parse(fetchCall[1].body); expect(body).toMatchObject({ - severity: LogRequestSeverity.Warning + severity: WSDKErrorSeverity.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(baseUrl, sdkVersion, accountId); + logger = new ReportingLogger( + { loggingUrl: baseUrl, errorUrl: baseUrl } as SDKConfig, + sdkVersion, + 'test-launcher-instance-guid' + ); logger.error('x'); expect(mockFetch).not.toHaveBeenCalled(); }); @@ -68,7 +77,11 @@ describe('ReportingLogger', () => { 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 = new ReportingLogger( + { loggingUrl: baseUrl, errorUrl: baseUrl } as SDKConfig, + sdkVersion, + 'test-launcher-instance-guid' + ); logger.error('x'); expect(mockFetch).not.toHaveBeenCalled(); }); @@ -76,7 +89,11 @@ 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); + logger = new ReportingLogger( + { loggingUrl: baseUrl, errorUrl: baseUrl } as SDKConfig, + sdkVersion, + 'test-launcher-instance-guid' + ); logger.error('x'); expect(mockFetch).toHaveBeenCalled(); }); @@ -88,14 +105,23 @@ describe('ReportingLogger', () => { return ++count > 3; }), }; - logger = new ReportingLogger(baseUrl, sdkVersion, accountId, mockRateLimiter); + logger = new ReportingLogger( + { loggingUrl: baseUrl, errorUrl: baseUrl } as SDKConfig, + sdkVersion, + 'test-launcher-instance-guid', + 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 = new ReportingLogger( + { loggingUrl: baseUrl, errorUrl: baseUrl } as SDKConfig, + sdkVersion, + 'test-launcher-instance-guid' + ); logger.error('msg'); expect(mockFetch).toHaveBeenCalled(); const fetchCall = mockFetch.mock.calls[0]; @@ -103,7 +129,11 @@ describe('ReportingLogger', () => { }); it('uses default user agent when user agent is empty', () => { - logger = new ReportingLogger(baseUrl, sdkVersion, accountId); + logger = new ReportingLogger( + { loggingUrl: baseUrl, errorUrl: baseUrl } as SDKConfig, + sdkVersion, + 'test-launcher-instance-guid' + ); delete (globalThis as any).navigator; delete (globalThis as any).location; logger.error('msg'); @@ -122,33 +152,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 e0d5885f7eda7269ec10850ed8a01646add2b37e Mon Sep 17 00:00:00 2001 From: Alexander Sapountzis Date: Thu, 18 Dec 2025 16:50:00 -0500 Subject: [PATCH 18/19] Introduce IDENTITY_REQUEST error code and update error logging for identity requests --- src/identityApiClient.ts | 2 +- src/logging/errorCodes.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/identityApiClient.ts b/src/identityApiClient.ts index a32e0975d..f75b65078 100644 --- a/src/identityApiClient.ts +++ b/src/identityApiClient.ts @@ -306,7 +306,7 @@ export default function IdentityAPIClient( Logger.error( 'Error sending identity request to servers' + ' - ' + errorMessage, - ErrorCodes.UNHANDLED_EXCEPTION + ErrorCodes.IDENTITY_REQUEST ); invokeCallback( callback, diff --git a/src/logging/errorCodes.ts b/src/logging/errorCodes.ts index 11d3b7045..1b3894345 100644 --- a/src/logging/errorCodes.ts +++ b/src/logging/errorCodes.ts @@ -5,4 +5,5 @@ export type ErrorCodes = valueof; export const ErrorCodes = { UNKNOWN_ERROR: 'UNKNOWN_ERROR', UNHANDLED_EXCEPTION: 'UNHANDLED_EXCEPTION', + IDENTITY_REQUEST: 'IDENTITY_REQUEST', } as const; \ No newline at end of file From 73ec2eef592efc874eaaf6473aab6334a7b0dbea Mon Sep 17 00:00:00 2001 From: Alexander Sapountzis Date: Fri, 19 Dec 2025 15:28:22 -0500 Subject: [PATCH 19/19] Remove unnecessary semicolons --- src/logging/reportingLogger.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/logging/reportingLogger.ts b/src/logging/reportingLogger.ts index f0c47cdf3..f28b0fe04 100644 --- a/src/logging/reportingLogger.ts +++ b/src/logging/reportingLogger.ts @@ -41,11 +41,11 @@ export class ReportingLogger implements IReportingLogger { public error(msg: string, code?: ErrorCodes, stackTrace?: string) { this.sendError(WSDKErrorSeverity.ERROR, msg, code, stackTrace); - }; + } public warning(msg: string, code?: ErrorCodes) { this.sendError(WSDKErrorSeverity.WARNING, msg, code); - }; + } private sendToServer(url: string,severity: WSDKErrorSeverity, msg: string, code?: ErrorCodes, stackTrace?: string): void { if(!this.canSendLog(severity))