From 732d2f0427c7daa369c2f5ccb7d49f5e1e733944 Mon Sep 17 00:00:00 2001 From: Guillermo Tamanaha Date: Mon, 24 Nov 2025 16:39:49 -0300 Subject: [PATCH 01/11] feat: Create Log Reporting Service --- src/constants.ts | 4 + src/identityApiClient.ts | 10 +- src/logger.ts | 16 ++- src/logging/errorCodes.ts | 9 ++ src/logging/logRequest.ts | 23 ++++ src/logging/reportingLogger.ts | 178 +++++++++++++++++++++++++++++ src/mp-instance.ts | 31 ++++- src/roktManager.ts | 1 + src/sdkConfigManager.ts | 177 ++++++++++++++++++++++++++++ src/sdkRuntimeModels.ts | 8 +- src/store.ts | 47 ++------ src/uploaders.ts | 1 + test/jest/reportingLogger.spec.ts | 184 ++++++++++++++++++++++++++++++ 13 files changed, 639 insertions(+), 50 deletions(-) create mode 100644 src/logging/errorCodes.ts create mode 100644 src/logging/logRequest.ts create mode 100644 src/logging/reportingLogger.ts create mode 100644 src/sdkConfigManager.ts create mode 100644 test/jest/reportingLogger.spec.ts 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 0c686f075..0c49964ba 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; @@ -326,10 +327,13 @@ export default function IdentityAPIClient( const requestCount = mpInstance._Store.identifyRequestCount; mpInstance.captureTiming(`${requestCount}-identityRequestEnd`); } - + 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.IDENTITY_REQUEST + ); + mpInstance.processQueueOnIdentityFailure?.(); invokeCallback(callback, HTTPCodes.noHttpCoverage, errorMessage); } diff --git a/src/logger.ts b/src/logger.ts index 6ec19c056..59e03d0c8 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; + +export const ErrorCodes = { + UNKNOWN_ERROR: 'UNKNOWN_ERROR', + UNHANDLED_EXCEPTION: 'UNHANDLED_EXCEPTION', + IDENTITY_REQUEST: 'IDENTITY_REQUEST', +} as const; \ No newline at end of file diff --git a/src/logging/logRequest.ts b/src/logging/logRequest.ts new file mode 100644 index 000000000..04ec1a86e --- /dev/null +++ b/src/logging/logRequest.ts @@ -0,0 +1,23 @@ +import { ErrorCodes } from "./errorCodes"; +export type ErrorCode = ErrorCodes | string; + +export type WSDKErrorSeverity = (typeof WSDKErrorSeverity)[keyof typeof WSDKErrorSeverity]; +export const WSDKErrorSeverity = { + ERROR: 'ERROR', + INFO: 'INFO', + WARNING: 'WARNING', +} as const; + + +export type ErrorsRequestBody = { + additionalInformation?: Record; + 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 new file mode 100644 index 000000000..f28b0fe04 --- /dev/null +++ b/src/logging/reportingLogger.ts @@ -0,0 +1,178 @@ +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; +} + +export class ReportingLogger implements IReportingLogger { + private readonly isEnabled: boolean; + private readonly reporter: string = 'mp-wsdk'; + private readonly integration: string = 'mp-wsdk'; + private readonly rateLimiter: IRateLimiter; + private integrationName: string; + private store: IStore; + + constructor( + private readonly config: SDKConfig, + private readonly sdkVersion: string, + private readonly launcherInstanceGuid?: string, + rateLimiter?: IRateLimiter, + ) { + this.isEnabled = this.isReportingEnabled(); + this.rateLimiter = rateLimiter ?? new RateLimiter(); + } + + public setIntegrationName(integrationName: string) { + this.integrationName = integrationName; + } + + public setStore(store: IStore) { + this.store = store; + } + + public info(msg: string, code?: ErrorCodes) { + this.sendLog(WSDKErrorSeverity.INFO, msg, code); + } + + 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)) + 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); + } + + 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 ?? ErrorCodes.UNKNOWN_ERROR, + url: this.getUrl(), + deviceInfo: this.getUserAgent(), + stackTrace: stackTrace ?? '', + reporter: this.reporter, + integration: this.integration + }; + } + + private getVersion(): string { + return this.integrationName ?? this.sdkVersion; + } + + private isReportingEnabled(): boolean { + // QUESTION: Should isDebugModeEnabled take precedence over + // isFeatureFlagEnabled and rokt domain present? + return ( + this.isRoktDomainPresent() && + (this.isFeatureFlagEnabled() || + this.isDebugModeEnabled()) + ); + } + + private isRoktDomainPresent(): boolean { + return Boolean(window['ROKT_DOMAIN']); + } + + private isFeatureFlagEnabled(): boolean { + return this.config.isWebSdkLoggingEnabled; + } + + private isDebugModeEnabled(): boolean { + return ( + window. + location?. + search?. + toLowerCase()?. + includes('mp_enable_logging=true') ?? false + ); + } + + private canSendLog(severity: WSDKErrorSeverity): boolean { + return this.isEnabled && !this.isRateLimited(severity); + } + + private isRateLimited(severity: WSDKErrorSeverity): boolean { + return this.rateLimiter.incrementAndCheck(severity); + } + + private getUrl(): string { + return window.location.href; + } + + private getUserAgent(): string { + return window.navigator.userAgent; + } + + private getLoggingUrl = (): string => `https://${this.config.loggingUrl}`; + private getErrorUrl = (): string => `https://${this.config.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', + }; + + if (this.store?.getRoktAccountId()) { + headers['rokt-account-id'] = this.store.getRoktAccountId(); + } + + return headers as IFetchPayload['headers']; + } +} + +export interface IRateLimiter { + incrementAndCheck(severity: WSDKErrorSeverity): boolean; +} + +export class RateLimiter implements IRateLimiter { + private readonly rateLimits: Map = new Map([ + [WSDKErrorSeverity.ERROR, 10], + [WSDKErrorSeverity.WARNING, 10], + [WSDKErrorSeverity.INFO, 10], + ]); + private logCount: Map = new Map(); + + public incrementAndCheck(severity: WSDKErrorSeverity): boolean { + const count = this.logCount.get(severity) || 0; + const limit = this.rateLimits.get(severity) || 10; + + const newCount = count + 1; + this.logCount.set(severity, newCount); + + return newCount > limit; + } +} \ No newline at end of file diff --git a/src/mp-instance.ts b/src/mp-instance.ts index 618feac05..d4359aa98 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,8 @@ import ForegroundTimer from './foregroundTimeTracker'; import RoktManager, { IRoktOptions } from './roktManager'; import filteredMparticleUser from './filteredMparticleUser'; import CookieConsentManager, { ICookieConsentManager } from './cookieConsentManager'; +import { IReportingLogger, ReportingLogger } from './logging/reportingLogger'; +import { SDKConfigManager } from './sdkConfigManager'; export interface IErrorLogMessage { message?: string; @@ -84,6 +86,7 @@ export interface IMParticleWebSDKInstance extends MParticleWebSDK { _NativeSdkHelpers: INativeSdkHelpers; _Persistence: IPersistence; _CookieConsentManager: ICookieConsentManager; + _ReportingLogger: IReportingLogger; _RoktManager: RoktManager; _SessionManager: ISessionManager; _ServerModel: IServerModel; @@ -92,6 +95,7 @@ export interface IMParticleWebSDKInstance extends MParticleWebSDK { _preInit: IPreInit; _timeOnSiteTimer: ForegroundTimer; setLauncherInstanceGuid: () => void; + getLauncherInstanceGuid: () => string; captureTiming(metricName: string); processQueueOnIdentityFailure?: () => void; } @@ -245,11 +249,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 @@ -1380,6 +1384,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) { @@ -1446,6 +1454,7 @@ function completeSDKInitialization(apiKey, config, mpInstance) { // Configure Rokt Manager with user and filtered user const roktConfig: IKitConfigs = parseConfig(config, 'Rokt', 181); if (roktConfig) { + mpInstance._Store.setRoktAccountId(roktConfig.settings?.accountId ?? undefined); const { userAttributeFilters } = roktConfig; const roktFilteredUser = filteredMparticleUser( currentUserMPID, @@ -1579,9 +1588,21 @@ function createIdentityCache(mpInstance) { } function runPreConfigFetchInitialization(mpInstance, apiKey, config) { - mpInstance.Logger = new Logger(config); - mpInstance._Store = new Store(config, mpInstance, apiKey); + + const sdkConfig = new SDKConfigManager(config, apiKey).getSDKConfig(); + mpInstance._ReportingLogger = new ReportingLogger( + sdkConfig, + Constants.sdkVersion, + mpInstance.getLauncherInstanceGuid(), + ); + mpInstance.Logger = new Logger(config, mpInstance._ReportingLogger); + mpInstance._Store = new Store( + { ...config, ...sdkConfig } as SDKInitConfig, + mpInstance, + apiKey + ); window.mParticle.Store = mpInstance._Store; + mpInstance._ReportingLogger.setStore(mpInstance._Store); mpInstance.Logger.verbose(StartingInitialization); // Initialize CookieConsentManager with privacy flags from launcherOptions diff --git a/src/roktManager.ts b/src/roktManager.ts index 2ee27c412..691571498 100644 --- a/src/roktManager.ts +++ b/src/roktManager.ts @@ -180,6 +180,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/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/sdkRuntimeModels.ts b/src/sdkRuntimeModels.ts index b5f47a919..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; @@ -311,6 +312,7 @@ export interface SDKInitConfig identityCallback?: IdentityCallback; launcherOptions?: IRoktLauncherOptions; + isWebSdkLoggingEnabled?: boolean; rq?: Function[] | any[]; logger?: IConsoleLogger; @@ -361,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 9ea908dbe..3a19b1904 100644 --- a/src/store.ts +++ b/src/store.ts @@ -96,6 +96,9 @@ export interface SDKConfig { webviewBridgeName?: string; workspaceToken?: string; requiredWebviewBridgeName?: string; + loggingUrl?: string; + errorUrl?: string; + isWebSdkLoggingEnabled?: boolean; } function createSDKConfig(config: SDKInitConfig): SDKConfig { @@ -202,6 +205,7 @@ export interface IStore { integrationDelayTimeoutStart: number; // UNIX Timestamp webviewBridgeEnabled?: boolean; wrapperSDKInfo: WrapperSDKInfo; + roktAccountId: string; persistenceData?: IPersistenceMinified; @@ -223,6 +227,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; @@ -289,6 +295,7 @@ export default function Store( version: null, isInfoSet: false, }, + roktAccountId: null, // Placeholder for in-memory persistence model persistenceData: { @@ -307,41 +314,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 || {}; @@ -667,6 +639,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/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 new file mode 100644 index 000000000..61a7f8349 --- /dev/null +++ b/test/jest/reportingLogger.spec.ts @@ -0,0 +1,184 @@ +import { IRateLimiter, RateLimiter, ReportingLogger } from '../../src/logging/reportingLogger'; +import { WSDKErrorSeverity } from '../../src/logging/logRequest'; +import { ErrorCodes } from '../../src/logging/errorCodes'; +import { SDKConfig } from '../../src/store'; + +describe('ReportingLogger', () => { + let logger: ReportingLogger; + const baseUrl = 'https://test-url.com'; + const sdkVersion = '1.2.3'; + let mockFetch: jest.Mock; + const accountId = '1234567890'; + beforeEach(() => { + mockFetch = jest.fn().mockResolvedValue({ ok: true }); + global.fetch = mockFetch; + + delete (globalThis as any).location; + (globalThis as any).location = { + href: 'https://e.com', + search: '' + }; + + Object.assign(globalThis, { + navigator: { userAgent: 'ua' }, + mParticle: { config: { isWebSdkLoggingEnabled: true } }, + ROKT_DOMAIN: 'set', + fetch: mockFetch + }); + logger = new ReportingLogger( + { loggingUrl: baseUrl, errorUrl: baseUrl } as SDKConfig, + sdkVersion, + 'test-launcher-instance-guid' + ); + }); + + afterEach(() => { + jest.clearAllMocks(); + delete (window as any).ROKT_DOMAIN; + delete (window as any).mParticle; + }); + + it('sends error logs with correct params', () => { + logger.error('msg', ErrorCodes.UNHANDLED_EXCEPTION, 'stack'); + expect(mockFetch).toHaveBeenCalled(); + const fetchCall = mockFetch.mock.calls[0]; + expect(fetchCall[0]).toContain('/v1/log'); + const body = JSON.parse(fetchCall[1].body); + expect(body).toMatchObject({ + severity: WSDKErrorSeverity.ERROR, + code: ErrorCodes.UNHANDLED_EXCEPTION, + stackTrace: 'stack' + }); + }); + + it('sends warning logs with correct params', () => { + logger.warning('warn'); + expect(mockFetch).toHaveBeenCalled(); + const fetchCall = mockFetch.mock.calls[0]; + expect(fetchCall[0]).toContain('/v1/log'); + const body = JSON.parse(fetchCall[1].body); + expect(body).toMatchObject({ + severity: WSDKErrorSeverity.WARNING + }); + 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( + { loggingUrl: baseUrl, errorUrl: baseUrl } as SDKConfig, + sdkVersion, + 'test-launcher-instance-guid' + ); + logger.error('x'); + 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( + { loggingUrl: baseUrl, errorUrl: baseUrl } as SDKConfig, + sdkVersion, + 'test-launcher-instance-guid' + ); + logger.error('x'); + 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( + { loggingUrl: baseUrl, errorUrl: baseUrl } as SDKConfig, + sdkVersion, + 'test-launcher-instance-guid' + ); + logger.error('x'); + expect(mockFetch).toHaveBeenCalled(); + }); + + it('rate limits after 3 errors', () => { + let count = 0; + const mockRateLimiter: IRateLimiter = { + incrementAndCheck: jest.fn().mockImplementation((severity) => { + return ++count > 3; + }), + }; + logger = new ReportingLogger( + { 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( + { loggingUrl: baseUrl, errorUrl: baseUrl } as SDKConfig, + sdkVersion, + 'test-launcher-instance-guid' + ); + logger.error('msg'); + expect(mockFetch).toHaveBeenCalled(); + const fetchCall = mockFetch.mock.calls[0]; + expect(fetchCall[1].headers['rokt-account-id']).toBe('0'); + }); + + it('uses default user agent when user agent is empty', () => { + logger = new ReportingLogger( + { loggingUrl: baseUrl, errorUrl: baseUrl } as SDKConfig, + sdkVersion, + 'test-launcher-instance-guid' + ); + 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', () => { + let rateLimiter: RateLimiter; + beforeEach(() => { + rateLimiter = new RateLimiter(); + }); + + it('allows up to 10 error logs then rate limits', () => { + for (let i = 0; i < 10; i++) { + expect(rateLimiter.incrementAndCheck(WSDKErrorSeverity.ERROR)).toBe(false); + } + expect(rateLimiter.incrementAndCheck(WSDKErrorSeverity.ERROR)).toBe(true); + expect(rateLimiter.incrementAndCheck(WSDKErrorSeverity.ERROR)).toBe(true); + }); + + it('allows up to 10 warning logs then rate limits', () => { + for (let i = 0; i < 10; i++) { + expect(rateLimiter.incrementAndCheck(WSDKErrorSeverity.WARNING)).toBe(false); + } + expect(rateLimiter.incrementAndCheck(WSDKErrorSeverity.WARNING)).toBe(true); + expect(rateLimiter.incrementAndCheck(WSDKErrorSeverity.WARNING)).toBe(true); + }); + + it('allows up to 10 info logs then rate limits', () => { + for (let i = 0; i < 10; i++) { + expect(rateLimiter.incrementAndCheck(WSDKErrorSeverity.INFO)).toBe(false); + } + expect(rateLimiter.incrementAndCheck(WSDKErrorSeverity.INFO)).toBe(true); + expect(rateLimiter.incrementAndCheck(WSDKErrorSeverity.INFO)).toBe(true); + }); + + it('tracks rate limits independently per severity', () => { + for (let i = 0; i < 10; i++) { + rateLimiter.incrementAndCheck(WSDKErrorSeverity.ERROR); + } + expect(rateLimiter.incrementAndCheck(WSDKErrorSeverity.ERROR)).toBe(true); + expect(rateLimiter.incrementAndCheck(WSDKErrorSeverity.WARNING)).toBe(false); + }); +}); From fc4681b8606177211405022496f755c9b962b5c6 Mon Sep 17 00:00:00 2001 From: Alexander Sapountzis Date: Fri, 13 Feb 2026 12:19:42 -0500 Subject: [PATCH 02/11] Refactor implementation of Reporting Logger to reduce scope --- src/constants.ts | 4 - src/logger.ts | 10 +- src/logging/reportingLogger.ts | 64 ++++++----- src/mp-instance.ts | 23 ++-- src/roktManager.ts | 20 +++- src/sdkConfigManager.ts | 177 ------------------------------ src/store.ts | 44 +++++++- test/jest/reportingLogger.spec.ts | 140 ++++++++++++++++------- 8 files changed, 208 insertions(+), 274 deletions(-) delete mode 100644 src/sdkConfigManager.ts diff --git a/src/constants.ts b/src/constants.ts index c5788b568..bf513a5d1 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -139,8 +139,6 @@ 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: { @@ -150,8 +148,6 @@ 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/logger.ts b/src/logger.ts index 59e03d0c8..798248d96 100644 --- a/src/logger.ts +++ b/src/logger.ts @@ -1,5 +1,5 @@ import { LogLevelType, SDKInitConfig, SDKLoggerApi } from './sdkRuntimeModels'; -import { IReportingLogger } from './logging/reportingLogger'; +import { ReportingLogger } from './logging/reportingLogger'; import { ErrorCodes } from './logging/errorCodes'; export type ILoggerConfig = Pick; @@ -8,10 +8,10 @@ export type IConsoleLogger = Partial this.isWebSdkLoggingEnabled; private isDebugModeEnabled(): boolean { return ( - window. + typeof window !== 'undefined' && + (window. location?. search?. toLowerCase()?. - includes('mp_enable_logging=true') ?? false + includes('mp_enable_logging=true') ?? false) ); } - + private canSendLog(severity: WSDKErrorSeverity): boolean { return this.isEnabled && !this.isRateLimited(severity); } @@ -126,16 +123,16 @@ export class ReportingLogger implements IReportingLogger { return this.rateLimiter.incrementAndCheck(severity); } - private getUrl(): string { - return window.location.href; + private getUrl(): string | undefined { + return typeof window !== 'undefined' ? window.location?.href : undefined; } - private getUserAgent(): string { - return window.navigator.userAgent; + private getUserAgent(): string | undefined { + return typeof window !== 'undefined' ? window.navigator?.userAgent : undefined; } - private getLoggingUrl = (): string => `https://${this.config.loggingUrl}`; - private getErrorUrl = (): string => `https://${this.config.errorUrl}`; + private getLoggingUrl = (): string => `https://${this.loggingUrl}`; + private getErrorUrl = (): string => `https://${this.errorUrl}`; private getHeaders(): IFetchPayload['headers'] { const headers: Record = { @@ -145,9 +142,10 @@ 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(); + + const accountId = this.store?.getRoktAccountId?.(); + if (accountId) { + headers['rokt-account-id'] = accountId; } return headers as IFetchPayload['headers']; @@ -175,4 +173,4 @@ export class RateLimiter implements IRateLimiter { return newCount > limit; } -} \ No newline at end of file +} diff --git a/src/mp-instance.ts b/src/mp-instance.ts index d4359aa98..25ac325c2 100644 --- a/src/mp-instance.ts +++ b/src/mp-instance.ts @@ -52,8 +52,7 @@ import ForegroundTimer from './foregroundTimeTracker'; import RoktManager, { IRoktOptions } from './roktManager'; import filteredMparticleUser from './filteredMparticleUser'; import CookieConsentManager, { ICookieConsentManager } from './cookieConsentManager'; -import { IReportingLogger, ReportingLogger } from './logging/reportingLogger'; -import { SDKConfigManager } from './sdkConfigManager'; +import { ReportingLogger } from './logging/reportingLogger'; export interface IErrorLogMessage { message?: string; @@ -86,14 +85,14 @@ export interface IMParticleWebSDKInstance extends MParticleWebSDK { _NativeSdkHelpers: INativeSdkHelpers; _Persistence: IPersistence; _CookieConsentManager: ICookieConsentManager; - _ReportingLogger: IReportingLogger; + _ReportingLogger: ReportingLogger; _RoktManager: RoktManager; _SessionManager: ISessionManager; _ServerModel: IServerModel; _Store: IStore; _instanceName: string; _preInit: IPreInit; - _timeOnSiteTimer: ForegroundTimer; + _timeOnSiteTimer: ForegroundTimer; setLauncherInstanceGuid: () => void; getLauncherInstanceGuid: () => string; captureTiming(metricName: string); @@ -249,7 +248,7 @@ export default function mParticleInstance(this: IMParticleWebSDKInstance, instan } }; - this._resetForTests = function(config, keepPersistence, instance, reportingLogger?: IReportingLogger) { + this._resetForTests = function(config, keepPersistence, instance, reportingLogger?: ReportingLogger) { if (instance._Store) { delete instance._Store; } @@ -1454,7 +1453,8 @@ function completeSDKInitialization(apiKey, config, mpInstance) { // Configure Rokt Manager with user and filtered user const roktConfig: IKitConfigs = parseConfig(config, 'Rokt', 181); if (roktConfig) { - mpInstance._Store.setRoktAccountId(roktConfig.settings?.accountId ?? undefined); + const accountId = roktConfig.settings?.accountId ?? undefined; + mpInstance._Store.setRoktAccountId(accountId); const { userAttributeFilters } = roktConfig; const roktFilteredUser = filteredMparticleUser( currentUserMPID, @@ -1588,19 +1588,14 @@ function createIdentityCache(mpInstance) { } function runPreConfigFetchInitialization(mpInstance, apiKey, config) { - - const sdkConfig = new SDKConfigManager(config, apiKey).getSDKConfig(); mpInstance._ReportingLogger = new ReportingLogger( - sdkConfig, + config, Constants.sdkVersion, + undefined, mpInstance.getLauncherInstanceGuid(), ); mpInstance.Logger = new Logger(config, mpInstance._ReportingLogger); - mpInstance._Store = new Store( - { ...config, ...sdkConfig } as SDKInitConfig, - mpInstance, - apiKey - ); + mpInstance._Store = new Store(config, mpInstance, apiKey); window.mParticle.Store = mpInstance._Store; mpInstance._ReportingLogger.setStore(mpInstance._Store); mpInstance.Logger.verbose(StartingInitialization); diff --git a/src/roktManager.ts b/src/roktManager.ts index 691571498..e8ddd7367 100644 --- a/src/roktManager.ts +++ b/src/roktManager.ts @@ -59,6 +59,12 @@ export interface RoktKitFilterSettings { filteredUser?: IMParticleUser | null; } +export interface IRoktKitSettings { + accountId?: string; + placementAttributesMapping?: string; + hashedEmailUserIdentityType?: string; +} + export interface IRoktKit { filters: RoktKitFilterSettings; filteredUser: IMParticleUser | null; @@ -69,6 +75,8 @@ export interface IRoktKit { setExtensionData(extensionData: IRoktPartnerExtensionData): void; use: (name: string) => Promise; launcherOptions?: Dictionary; + settings?: IRoktKitSettings; + integrationName?: string; } export interface IRoktOptions { @@ -180,8 +188,16 @@ export default class RoktManager { } public attachKit(kit: IRoktKit): void { - // TODO: Pass back integrationName via kit this.kit = kit; + + if (kit.settings?.accountId) { + this.store.setRoktAccountId(kit.settings.accountId); + } + + if (kit.integrationName) { + this.store.setIntegrationName(kit.integrationName); + } + this.processMessageQueue(); try { @@ -589,4 +605,4 @@ export default class RoktManager { return false; // Values are the same } -} \ No newline at end of file +} diff --git a/src/sdkConfigManager.ts b/src/sdkConfigManager.ts deleted file mode 100644 index aaf293885..000000000 --- a/src/sdkConfigManager.ts +++ /dev/null @@ -1,177 +0,0 @@ -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 3a19b1904..2961b41a2 100644 --- a/src/store.ts +++ b/src/store.ts @@ -123,6 +123,9 @@ function createSDKConfig(config: SDKInitConfig): SDKConfig { sdkConfig[prop] = Constants.DefaultBaseUrls[prop]; } + // Always initialize flags to at least an empty object to prevent undefined access + sdkConfig.flags = sdkConfig.flags || {}; + return sdkConfig; } @@ -206,6 +209,7 @@ export interface IStore { webviewBridgeEnabled?: boolean; wrapperSDKInfo: WrapperSDKInfo; roktAccountId: string; + integrationName: string; persistenceData?: IPersistenceMinified; @@ -229,6 +233,8 @@ export interface IStore { setUserIdentities?(mpid: MPID, userIdentities: UserIdentities): void; getRoktAccountId?(): string; setRoktAccountId?(accountId: string): void; + getIntegrationName?(): string; + setIntegrationName?(integrationName: string): void; addMpidToSessionHistory?(mpid: MPID, previousMpid?: MPID): void; hasInvalidIdentifyRequest?: () => boolean; @@ -296,6 +302,7 @@ export default function Store( isInfoSet: false, }, roktAccountId: null, + integrationName: null, // Placeholder for in-memory persistence model persistenceData: { @@ -314,6 +321,33 @@ export default function Store( this.SDKConfig = createSDKConfig(config); if (config) { + if (!config.hasOwnProperty('flags')) { + this.SDKConfig.flags = {}; + } + + 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]; + } + this.SDKConfig.useNativeSdk = !!config.useNativeSdk; this.SDKConfig.kits = config.kits || {}; @@ -645,6 +679,11 @@ export default function Store( this.roktAccountId = accountId; }; + this.getIntegrationName = () => this.integrationName; + this.setIntegrationName = (integrationName: string) => { + this.integrationName = integrationName; + }; + this.addMpidToSessionHistory = (mpid: MPID, previousMPID?: MPID): void => { const indexOfMPID = this.currentSessionMPIDs.indexOf(mpid); @@ -676,7 +715,10 @@ export default function Store( // We should reprocess the flags and baseUrls in case they have changed when we request an updated config // such as if the SDK is being self-hosted and the flags are different on the server config // https://go.mparticle.com/work/SQDSDKS-6317 - this.SDKConfig.flags = processFlags(config); + // Only update flags if the config actually has flags (don't overwrite with empty object) + if (config.flags) { + this.SDKConfig.flags = processFlags(config); + } const baseUrls: Dictionary = processBaseUrls( config, diff --git a/test/jest/reportingLogger.spec.ts b/test/jest/reportingLogger.spec.ts index 61a7f8349..c732b16cd 100644 --- a/test/jest/reportingLogger.spec.ts +++ b/test/jest/reportingLogger.spec.ts @@ -1,48 +1,66 @@ import { IRateLimiter, RateLimiter, ReportingLogger } from '../../src/logging/reportingLogger'; import { WSDKErrorSeverity } from '../../src/logging/logRequest'; import { ErrorCodes } from '../../src/logging/errorCodes'; -import { SDKConfig } from '../../src/store'; +import { IStore, SDKConfig } from '../../src/store'; describe('ReportingLogger', () => { let logger: ReportingLogger; - const baseUrl = 'https://test-url.com'; + const loggingUrl = 'test-url.com/v1/log'; + const errorUrl = 'test-url.com/v1/errors'; const sdkVersion = '1.2.3'; let mockFetch: jest.Mock; const accountId = '1234567890'; + let mockStore: Partial; + beforeEach(() => { mockFetch = jest.fn().mockResolvedValue({ ok: true }); global.fetch = mockFetch; - - delete (globalThis as any).location; - (globalThis as any).location = { - href: 'https://e.com', - search: '' + + mockStore = { + getRoktAccountId: jest.fn().mockReturnValue(null), + getIntegrationName: jest.fn().mockReturnValue(null) }; - - Object.assign(globalThis, { - navigator: { userAgent: 'ua' }, - mParticle: { config: { isWebSdkLoggingEnabled: true } }, - ROKT_DOMAIN: 'set', - fetch: mockFetch + + Object.defineProperty(window, 'location', { + writable: true, + value: { + href: 'https://e.com', + search: '' + } + }); + + Object.defineProperty(window, 'navigator', { + writable: true, + value: { userAgent: 'ua' } + }); + + Object.defineProperty(window, 'mParticle', { + writable: true, + value: { config: { isWebSdkLoggingEnabled: true } } + }); + + Object.defineProperty(window, 'ROKT_DOMAIN', { + writable: true, + value: 'set' }); + logger = new ReportingLogger( - { loggingUrl: baseUrl, errorUrl: baseUrl } as SDKConfig, + { loggingUrl, errorUrl, isWebSdkLoggingEnabled: true } as SDKConfig, sdkVersion, + mockStore as IStore, 'test-launcher-instance-guid' ); }); afterEach(() => { jest.clearAllMocks(); - delete (window as any).ROKT_DOMAIN; - delete (window as any).mParticle; }); it('sends error logs with correct params', () => { logger.error('msg', ErrorCodes.UNHANDLED_EXCEPTION, 'stack'); expect(mockFetch).toHaveBeenCalled(); const fetchCall = mockFetch.mock.calls[0]; - expect(fetchCall[0]).toContain('/v1/log'); + expect(fetchCall[0]).toContain('/v1/errors'); const body = JSON.parse(fetchCall[1].body); expect(body).toMatchObject({ severity: WSDKErrorSeverity.ERROR, @@ -52,10 +70,11 @@ describe('ReportingLogger', () => { }); it('sends warning logs with correct params', () => { + mockStore.getRoktAccountId = jest.fn().mockReturnValue(accountId); logger.warning('warn'); expect(mockFetch).toHaveBeenCalled(); const fetchCall = mockFetch.mock.calls[0]; - expect(fetchCall[0]).toContain('/v1/log'); + expect(fetchCall[0]).toContain('/v1/errors'); const body = JSON.parse(fetchCall[1].body); expect(body).toMatchObject({ severity: WSDKErrorSeverity.WARNING @@ -64,10 +83,14 @@ describe('ReportingLogger', () => { }); it('does not log if ROKT_DOMAIN missing', () => { - delete (globalThis as any).ROKT_DOMAIN; + Object.defineProperty(window, 'ROKT_DOMAIN', { + writable: true, + value: undefined + }); logger = new ReportingLogger( - { loggingUrl: baseUrl, errorUrl: baseUrl } as SDKConfig, + { loggingUrl, errorUrl, isWebSdkLoggingEnabled: true } as SDKConfig, sdkVersion, + mockStore as IStore, 'test-launcher-instance-guid' ); logger.error('x'); @@ -75,11 +98,18 @@ describe('ReportingLogger', () => { }); it('does not log if feature flag and debug mode off', () => { - window.mParticle.config.isWebSdkLoggingEnabled = false; - window.location.search = ''; + Object.defineProperty(window, 'mParticle', { + writable: true, + value: { config: { isWebSdkLoggingEnabled: false } } + }); + Object.defineProperty(window, 'location', { + writable: true, + value: { href: 'https://e.com', search: '' } + }); logger = new ReportingLogger( - { loggingUrl: baseUrl, errorUrl: baseUrl } as SDKConfig, + { loggingUrl, errorUrl, isWebSdkLoggingEnabled: false } as SDKConfig, sdkVersion, + mockStore as IStore, 'test-launcher-instance-guid' ); logger.error('x'); @@ -87,11 +117,18 @@ 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'; + Object.defineProperty(window, 'mParticle', { + writable: true, + value: { config: { isWebSdkLoggingEnabled: false } } + }); + Object.defineProperty(window, 'location', { + writable: true, + value: { href: 'https://e.com', search: '?mp_enable_logging=true' } + }); logger = new ReportingLogger( - { loggingUrl: baseUrl, errorUrl: baseUrl } as SDKConfig, + { loggingUrl, errorUrl, isWebSdkLoggingEnabled: false } as SDKConfig, sdkVersion, + mockStore as IStore, 'test-launcher-instance-guid' ); logger.error('x'); @@ -106,8 +143,9 @@ describe('ReportingLogger', () => { }), }; logger = new ReportingLogger( - { loggingUrl: baseUrl, errorUrl: baseUrl } as SDKConfig, + { loggingUrl, errorUrl, isWebSdkLoggingEnabled: true } as SDKConfig, sdkVersion, + mockStore as IStore, 'test-launcher-instance-guid', mockRateLimiter ); @@ -116,31 +154,57 @@ describe('ReportingLogger', () => { expect(mockFetch).toHaveBeenCalledTimes(3); }); - it('uses default account id when accountId is empty', () => { + it('does not include account id header when account id is not set', () => { + logger.error('msg'); + expect(mockFetch).toHaveBeenCalled(); + const fetchCall = mockFetch.mock.calls[0]; + expect(fetchCall[1].headers['rokt-account-id']).toBeUndefined(); + }); + + it('omits user agent and url fields when navigator/location are undefined', () => { + Object.defineProperty(window, 'navigator', { + writable: true, + value: undefined + }); + Object.defineProperty(window, 'location', { + writable: true, + value: undefined + }); logger = new ReportingLogger( - { loggingUrl: baseUrl, errorUrl: baseUrl } as SDKConfig, + { loggingUrl, errorUrl, isWebSdkLoggingEnabled: true } as SDKConfig, sdkVersion, + mockStore as IStore, 'test-launcher-instance-guid' ); logger.error('msg'); expect(mockFetch).toHaveBeenCalled(); const fetchCall = mockFetch.mock.calls[0]; - expect(fetchCall[1].headers['rokt-account-id']).toBe('0'); + const body = JSON.parse(fetchCall[1].body); + // undefined values are omitted from JSON.stringify, so these fields won't be present + expect(body).not.toHaveProperty('deviceInfo'); + expect(body).not.toHaveProperty('url'); }); - it('uses default user agent when user agent is empty', () => { - logger = new ReportingLogger( - { loggingUrl: baseUrl, errorUrl: baseUrl } as SDKConfig, + it('can set store after initialization', () => { + const loggerWithoutStore = new ReportingLogger( + { loggingUrl, errorUrl, isWebSdkLoggingEnabled: true } as SDKConfig, sdkVersion, + undefined, 'test-launcher-instance-guid' ); - delete (globalThis as any).navigator; - delete (globalThis as any).location; - logger.error('msg'); + + const newMockStore: Partial = { + getRoktAccountId: jest.fn().mockReturnValue(accountId), + getIntegrationName: jest.fn().mockReturnValue('custom-integration-name') + }; + + loggerWithoutStore.setStore(newMockStore as IStore); + loggerWithoutStore.error('test error'); + expect(mockFetch).toHaveBeenCalled(); const fetchCall = mockFetch.mock.calls[0]; - const body = JSON.parse(fetchCall[1].body); - expect(body).toMatchObject({ deviceInfo: 'no-user-agent-set', url: 'no-url-set' }); + expect(fetchCall[1].headers['rokt-account-id']).toBe(accountId); + expect(fetchCall[1].headers['rokt-launcher-version']).toBe('custom-integration-name'); }); }); From 7ff3c41e8cca75936bf14b1a35b7ff4eabfaff88 Mon Sep 17 00:00:00 2001 From: Alexander Sapountzis Date: Fri, 13 Feb 2026 13:44:02 -0500 Subject: [PATCH 03/11] Address PR Comments --- src/identityApiClient.ts | 2 +- src/logger.ts | 15 ++++----- src/logging/errorCodes.ts | 9 ------ src/logging/reportingLogger.ts | 11 +++---- src/logging/{logRequest.ts => types.ts} | 13 ++++++-- src/sdkRuntimeModels.ts | 2 +- test/jest/logger.spec.ts | 42 +++++++++++++++++++++++++ test/jest/reportingLogger.spec.ts | 3 +- 8 files changed, 69 insertions(+), 28 deletions(-) delete mode 100644 src/logging/errorCodes.ts rename src/logging/{logRequest.ts => types.ts} (62%) diff --git a/src/identityApiClient.ts b/src/identityApiClient.ts index 0c49964ba..70387259b 100644 --- a/src/identityApiClient.ts +++ b/src/identityApiClient.ts @@ -25,7 +25,7 @@ import { IIdentityResponse, } from './identity-user-interfaces'; import { IMParticleWebSDKInstance } from './mp-instance'; -import { ErrorCodes } from './logging/errorCodes'; +import { ErrorCodes } from './logging/types'; const { HTTPCodes, Messages, IdentityMethods } = Constants; diff --git a/src/logger.ts b/src/logger.ts index 798248d96..8757f46fd 100644 --- a/src/logger.ts +++ b/src/logger.ts @@ -1,6 +1,6 @@ import { LogLevelType, SDKInitConfig, SDKLoggerApi } from './sdkRuntimeModels'; import { ReportingLogger } from './logging/reportingLogger'; -import { ErrorCodes } from './logging/errorCodes'; +import { ErrorCodes } from './logging/types'; export type ILoggerConfig = Pick; export type IConsoleLogger = Partial>; @@ -27,24 +27,25 @@ export class Logger { } } - public warning(msg: string, code?: ErrorCodes): void { - if(this.logLevel === LogLevelType.None) + public warning(msg: string): void { + if(this.logLevel === LogLevelType.None) return; - if (this.logger.warning && + if (this.logger.warning && (this.logLevel === LogLevelType.Verbose || this.logLevel === LogLevelType.Warning)) { this.logger.warning(msg); - this.reportingLogger?.warning(msg, code); } } public error(msg: string, code?: ErrorCodes): void { - if(this.logLevel === LogLevelType.None) + if(this.logLevel === LogLevelType.None) return; if (this.logger.error) { this.logger.error(msg); - this.reportingLogger?.error(msg, code); + if (code) { + this.reportingLogger?.error(msg, code); + } } } diff --git a/src/logging/errorCodes.ts b/src/logging/errorCodes.ts deleted file mode 100644 index 1b3894345..000000000 --- a/src/logging/errorCodes.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { valueof } from '../utils'; - -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 diff --git a/src/logging/reportingLogger.ts b/src/logging/reportingLogger.ts index a7223d934..e81d0c86a 100644 --- a/src/logging/reportingLogger.ts +++ b/src/logging/reportingLogger.ts @@ -1,5 +1,4 @@ -import { ErrorCodes } from "./errorCodes"; -import { LogRequestBody, WSDKErrorSeverity } from "./logRequest"; +import { ErrorCodes, LogRequestBody, WSDKErrorSeverity } from "./types"; import { FetchUploader, IFetchPayload } from "../uploaders"; import { IStore, SDKConfig } from "../store"; import { SDKInitConfig } from "../sdkRuntimeModels"; @@ -45,11 +44,11 @@ export class ReportingLogger { this.sendError(WSDKErrorSeverity.WARNING, msg, code); } - private sendToServer(url: string,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 logRequest = this.buildLogRequest(severity, msg, code, stackTrace); const uploader = new FetchUploader(url); const payload: IFetchPayload = { method: 'POST', @@ -68,7 +67,7 @@ export class ReportingLogger { this.sendToServer(url, severity, msg, code, stackTrace); } - private getLogRequest(severity: WSDKErrorSeverity, msg: string, code?: ErrorCodes, stackTrace?: string): LogRequestBody { + private buildLogRequest(severity: WSDKErrorSeverity, msg: string, code?: ErrorCodes, stackTrace?: string): LogRequestBody { return { additionalInformation: { message: msg, diff --git a/src/logging/logRequest.ts b/src/logging/types.ts similarity index 62% rename from src/logging/logRequest.ts rename to src/logging/types.ts index 04ec1a86e..55dce80be 100644 --- a/src/logging/logRequest.ts +++ b/src/logging/types.ts @@ -1,4 +1,13 @@ -import { ErrorCodes } from "./errorCodes"; +import { valueof } from '../utils'; + +export type ErrorCodes = valueof; + +export const ErrorCodes = { + UNKNOWN_ERROR: 'UNKNOWN_ERROR', + UNHANDLED_EXCEPTION: 'UNHANDLED_EXCEPTION', + IDENTITY_REQUEST: 'IDENTITY_REQUEST', +} as const; + export type ErrorCode = ErrorCodes | string; export type WSDKErrorSeverity = (typeof WSDKErrorSeverity)[keyof typeof WSDKErrorSeverity]; @@ -20,4 +29,4 @@ export type ErrorsRequestBody = { url?: string; }; -export type LogRequestBody = ErrorsRequestBody; \ No newline at end of file +export type LogRequestBody = ErrorsRequestBody; diff --git a/src/sdkRuntimeModels.ts b/src/sdkRuntimeModels.ts index c9e62b404..90a30634c 100644 --- a/src/sdkRuntimeModels.ts +++ b/src/sdkRuntimeModels.ts @@ -41,7 +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'; +import { ErrorCodes } from './logging/types'; // TODO: Resolve this with version in @mparticle/web-sdk export type SDKEventCustomFlags = Dictionary; diff --git a/test/jest/logger.spec.ts b/test/jest/logger.spec.ts index def3a637d..6dce7fe1b 100644 --- a/test/jest/logger.spec.ts +++ b/test/jest/logger.spec.ts @@ -1,5 +1,7 @@ import { Logger, ConsoleLogger } from '../../src/logger'; import { LogLevelType } from '../../src/sdkRuntimeModels'; +import { ReportingLogger } from '../../src/logging/reportingLogger'; +import { ErrorCodes } from '../../src/logging/types'; describe('Logger', () => { let mockConsole: any; @@ -103,6 +105,46 @@ describe('Logger', () => { expect(mockConsole.warn).toHaveBeenCalledWith('b'); expect(mockConsole.error).toHaveBeenCalledWith('c'); }); + + describe('ReportingLogger integration', () => { + let mockReportingLogger: jest.Mocked; + + beforeEach(() => { + mockReportingLogger = { + error: jest.fn(), + warning: jest.fn(), + info: jest.fn(), + setStore: jest.fn(), + } as any; + }); + + it('should call reportingLogger.error when error is called with error code', () => { + logger = new Logger({ logLevel: LogLevelType.Verbose }, mockReportingLogger); + + logger.error('test error', ErrorCodes.UNHANDLED_EXCEPTION); + + expect(mockConsole.error).toHaveBeenCalledWith('test error'); + expect(mockReportingLogger.error).toHaveBeenCalledWith('test error', ErrorCodes.UNHANDLED_EXCEPTION); + }); + + it('should NOT call reportingLogger.error when error is called without error code', () => { + logger = new Logger({ logLevel: LogLevelType.Verbose }, mockReportingLogger); + + logger.error('test error'); + + expect(mockConsole.error).toHaveBeenCalledWith('test error'); + expect(mockReportingLogger.error).not.toHaveBeenCalled(); + }); + + it('should NOT call reportingLogger when warning is called', () => { + logger = new Logger({ logLevel: LogLevelType.Verbose }, mockReportingLogger); + + logger.warning('test warning'); + + expect(mockConsole.warn).toHaveBeenCalledWith('test warning'); + expect(mockReportingLogger.warning).not.toHaveBeenCalled(); + }); + }); }); describe('ConsoleLogger', () => { diff --git a/test/jest/reportingLogger.spec.ts b/test/jest/reportingLogger.spec.ts index c732b16cd..6b2b45bb7 100644 --- a/test/jest/reportingLogger.spec.ts +++ b/test/jest/reportingLogger.spec.ts @@ -1,6 +1,5 @@ import { IRateLimiter, RateLimiter, ReportingLogger } from '../../src/logging/reportingLogger'; -import { WSDKErrorSeverity } from '../../src/logging/logRequest'; -import { ErrorCodes } from '../../src/logging/errorCodes'; +import { WSDKErrorSeverity, ErrorCodes } from '../../src/logging/types'; import { IStore, SDKConfig } from '../../src/store'; describe('ReportingLogger', () => { From 28b8892afbc187825d2963d22d67683cc264962d Mon Sep 17 00:00:00 2001 From: Alexander Sapountzis Date: Fri, 13 Feb 2026 13:47:00 -0500 Subject: [PATCH 04/11] Address PR Comments --- src/mp-instance.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mp-instance.ts b/src/mp-instance.ts index 25ac325c2..cf3b1888a 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, SDKInitConfig } from './sdkRuntimeModels'; +import { BaseEvent, MParticleWebSDK, SDKHelpersApi } from './sdkRuntimeModels'; import { Dictionary, SDKEventAttrs } from '@mparticle/web-sdk'; import { IIdentity } from './identity.interfaces'; import { IEvents } from './events.interfaces'; From 0b3dc819e81f24f8e3cd606b8409f4df6615385e Mon Sep 17 00:00:00 2001 From: Alexander Sapountzis Date: Fri, 13 Feb 2026 15:46:03 -0500 Subject: [PATCH 05/11] Address PR Comments --- src/logging/reportingLogger.ts | 63 +++++++++++++++++++--------------- src/mp-instance.ts | 2 +- 2 files changed, 36 insertions(+), 29 deletions(-) diff --git a/src/logging/reportingLogger.ts b/src/logging/reportingLogger.ts index e81d0c86a..189b9b40c 100644 --- a/src/logging/reportingLogger.ts +++ b/src/logging/reportingLogger.ts @@ -3,6 +3,15 @@ import { FetchUploader, IFetchPayload } from "../uploaders"; import { IStore, SDKConfig } from "../store"; import { SDKInitConfig } from "../sdkRuntimeModels"; +interface IReportingLoggerPayload extends IFetchPayload { + headers: IFetchPayload['headers'] & { + 'rokt-launcher-instance-guid'?: string; + 'rokt-launcher-version': string; + 'rokt-wsdk-version': string; + }; + body: string; +} + export class ReportingLogger { private readonly isEnabled: boolean; private readonly reporter: string = 'mp-wsdk'; @@ -20,8 +29,8 @@ export class ReportingLogger { private readonly launcherInstanceGuid?: string, rateLimiter?: IRateLimiter, ) { - this.loggingUrl = config.loggingUrl || 'jssdkcdns.mparticle.com/v1/JS/logs'; - this.errorUrl = config.errorUrl || 'jssdkcdns.mparticle.com/v1/JS/errors'; + this.loggingUrl = `https://${config.loggingUrl || 'jssdkcdns.mparticle.com/v1/JS/logs'}`; + this.errorUrl = `https://${config.errorUrl || 'jssdkcdns.mparticle.com/v1/JS/errors'}`; this.isWebSdkLoggingEnabled = config.isWebSdkLoggingEnabled || false; this.store = store ?? null; this.isEnabled = this.isReportingEnabled(); @@ -48,23 +57,26 @@ export class ReportingLogger { if(!this.canSendLog(severity)) return; - const logRequest = this.buildLogRequest(severity, msg, code, stackTrace); - const uploader = new FetchUploader(url); - const payload: IFetchPayload = { - method: 'POST', - headers: this.getHeaders(), - body: JSON.stringify(logRequest), - }; - uploader.upload(payload); + try { + const logRequest = this.buildLogRequest(severity, msg, code, stackTrace); + const uploader = new FetchUploader(url); + const payload: IReportingLoggerPayload = { + method: 'POST', + headers: this.getHeaders(), + body: JSON.stringify(logRequest), + }; + uploader.upload(payload); + } catch (error) { + console.error('ReportingLogger: Failed to send log', error); + } } private sendLog(severity: WSDKErrorSeverity, msg: string, code?: ErrorCodes, stackTrace?: string): void { - const url = this.getLoggingUrl(); - this.sendToServer(url, severity, msg, code, stackTrace); + this.sendToServer(this.loggingUrl, 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); + this.sendToServer(this.errorUrl, severity, msg, code, stackTrace); } private buildLogRequest(severity: WSDKErrorSeverity, msg: string, code?: ErrorCodes, stackTrace?: string): LogRequestBody { @@ -88,13 +100,8 @@ export class ReportingLogger { } private isReportingEnabled(): boolean { - // QUESTION: Should isDebugModeEnabled take precedence over - // isFeatureFlagEnabled and rokt domain present? - return ( - this.isRoktDomainPresent() && - (this.isFeatureFlagEnabled() || - this.isDebugModeEnabled()) - ); + return this.isDebugModeEnabled() || + (this.isRoktDomainPresent() && this.isFeatureFlagEnabled()); } private isRoktDomainPresent(): boolean { @@ -130,24 +137,24 @@ export class ReportingLogger { return typeof window !== 'undefined' ? window.navigator?.userAgent : undefined; } - private getLoggingUrl = (): string => `https://${this.loggingUrl}`; - private getErrorUrl = (): string => `https://${this.errorUrl}`; - - private getHeaders(): IFetchPayload['headers'] { - const headers: Record = { + private getHeaders(): IReportingLoggerPayload['headers'] { + const headers: IReportingLoggerPayload['headers'] = { 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', }; + if (this.launcherInstanceGuid) { + headers['rokt-launcher-instance-guid'] = this.launcherInstanceGuid; + } + const accountId = this.store?.getRoktAccountId?.(); if (accountId) { headers['rokt-account-id'] = accountId; } - return headers as IFetchPayload['headers']; + return headers; } } diff --git a/src/mp-instance.ts b/src/mp-instance.ts index cf3b1888a..e4db9c40b 100644 --- a/src/mp-instance.ts +++ b/src/mp-instance.ts @@ -1453,7 +1453,7 @@ function completeSDKInitialization(apiKey, config, mpInstance) { // Configure Rokt Manager with user and filtered user const roktConfig: IKitConfigs = parseConfig(config, 'Rokt', 181); if (roktConfig) { - const accountId = roktConfig.settings?.accountId ?? undefined; + const accountId = roktConfig.settings?.accountId; mpInstance._Store.setRoktAccountId(accountId); const { userAttributeFilters } = roktConfig; const roktFilteredUser = filteredMparticleUser( From c095cbd1b0851019d9f5524ce7e78c29f39a3676 Mon Sep 17 00:00:00 2001 From: Alexander Sapountzis Date: Fri, 13 Feb 2026 15:54:09 -0500 Subject: [PATCH 06/11] Add new test cases --- src/logging/reportingLogger.ts | 4 +- test/jest/reportingLogger.spec.ts | 111 ++++++++++++++++++++++++++++++ 2 files changed, 114 insertions(+), 1 deletion(-) diff --git a/src/logging/reportingLogger.ts b/src/logging/reportingLogger.ts index 189b9b40c..1f0446905 100644 --- a/src/logging/reportingLogger.ts +++ b/src/logging/reportingLogger.ts @@ -65,7 +65,9 @@ export class ReportingLogger { headers: this.getHeaders(), body: JSON.stringify(logRequest), }; - uploader.upload(payload); + uploader.upload(payload).catch((error) => { + console.error('ReportingLogger: Failed to send log', error); + }); } catch (error) { console.error('ReportingLogger: Failed to send log', error); } diff --git a/test/jest/reportingLogger.spec.ts b/test/jest/reportingLogger.spec.ts index 6b2b45bb7..d8cde82b4 100644 --- a/test/jest/reportingLogger.spec.ts +++ b/test/jest/reportingLogger.spec.ts @@ -134,6 +134,25 @@ describe('ReportingLogger', () => { expect(mockFetch).toHaveBeenCalled(); }); + it('logs if debug mode on even without ROKT_DOMAIN', () => { + Object.defineProperty(window, 'ROKT_DOMAIN', { + writable: true, + value: undefined + }); + Object.defineProperty(window, 'location', { + writable: true, + value: { href: 'https://e.com', search: '?mp_enable_logging=true' } + }); + logger = new ReportingLogger( + { loggingUrl, errorUrl, isWebSdkLoggingEnabled: false } as SDKConfig, + sdkVersion, + mockStore as IStore, + 'test-launcher-instance-guid' + ); + logger.error('x'); + expect(mockFetch).toHaveBeenCalled(); + }); + it('rate limits after 3 errors', () => { let count = 0; const mockRateLimiter: IRateLimiter = { @@ -205,6 +224,98 @@ describe('ReportingLogger', () => { expect(fetchCall[1].headers['rokt-account-id']).toBe(accountId); expect(fetchCall[1].headers['rokt-launcher-version']).toBe('custom-integration-name'); }); + + it('catches and logs errors during upload', () => { + const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(); + mockFetch.mockRejectedValueOnce(new Error('Network failure')); + + logger.error('test error', ErrorCodes.UNHANDLED_EXCEPTION); + + // Wait for the promise to resolve + return new Promise(resolve => setTimeout(resolve, 0)).then(() => { + expect(consoleErrorSpy).toHaveBeenCalledWith( + 'ReportingLogger: Failed to send log', + expect.any(Error) + ); + consoleErrorSpy.mockRestore(); + }); + }); + + it('omits rokt-launcher-instance-guid header when launcherInstanceGuid is undefined', () => { + logger = new ReportingLogger( + { loggingUrl, errorUrl, isWebSdkLoggingEnabled: true } as SDKConfig, + sdkVersion, + mockStore as IStore, + undefined + ); + + logger.error('test error'); + + expect(mockFetch).toHaveBeenCalled(); + const fetchCall = mockFetch.mock.calls[0]; + expect(fetchCall[1].headers['rokt-launcher-instance-guid']).toBeUndefined(); + expect(fetchCall[1].headers['rokt-launcher-version']).toBeDefined(); + }); + + it('includes all required headers', () => { + mockStore.getRoktAccountId = jest.fn().mockReturnValue(accountId); + mockStore.getIntegrationName = jest.fn().mockReturnValue('custom-integration'); + + logger.error('test error'); + + expect(mockFetch).toHaveBeenCalled(); + const fetchCall = mockFetch.mock.calls[0]; + const headers = fetchCall[1].headers; + + expect(headers['Accept']).toBe('text/plain;charset=UTF-8'); + expect(headers['Content-Type']).toBe('application/json'); + expect(headers['rokt-launcher-instance-guid']).toBe('test-launcher-instance-guid'); + expect(headers['rokt-launcher-version']).toBe('custom-integration'); + expect(headers['rokt-wsdk-version']).toBe('joint'); + expect(headers['rokt-account-id']).toBe(accountId); + }); + + it('constructs full URL with https prefix', () => { + logger.error('test error'); + + expect(mockFetch).toHaveBeenCalled(); + const fetchCall = mockFetch.mock.calls[0]; + expect(fetchCall[0]).toBe(`https://${errorUrl}`); + }); + + it('includes all fields in log request body', () => { + mockStore.getIntegrationName = jest.fn().mockReturnValue('test-integration'); + + logger.error('error message', ErrorCodes.IDENTITY_REQUEST, 'stack trace here'); + + expect(mockFetch).toHaveBeenCalled(); + const fetchCall = mockFetch.mock.calls[0]; + const body = JSON.parse(fetchCall[1].body); + + expect(body).toEqual({ + additionalInformation: { + message: 'error message', + version: 'test-integration' + }, + severity: WSDKErrorSeverity.ERROR, + code: ErrorCodes.IDENTITY_REQUEST, + url: 'https://e.com', + deviceInfo: 'ua', + stackTrace: 'stack trace here', + reporter: 'mp-wsdk', + integration: 'mp-wsdk' + }); + }); + + it('uses default error code when code is undefined', () => { + logger.error('test error'); + + expect(mockFetch).toHaveBeenCalled(); + const fetchCall = mockFetch.mock.calls[0]; + const body = JSON.parse(fetchCall[1].body); + + expect(body.code).toBe(ErrorCodes.UNKNOWN_ERROR); + }); }); describe('RateLimiter', () => { From 6fe5d088c034be26968bb0dc2c35e630ea5be7ef Mon Sep 17 00:00:00 2001 From: Alex S <49695018+alexs-mparticle@users.noreply.github.com> Date: Fri, 13 Feb 2026 15:56:27 -0500 Subject: [PATCH 07/11] Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- test/jest/reportingLogger.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/jest/reportingLogger.spec.ts b/test/jest/reportingLogger.spec.ts index d8cde82b4..8bbf86518 100644 --- a/test/jest/reportingLogger.spec.ts +++ b/test/jest/reportingLogger.spec.ts @@ -70,7 +70,7 @@ describe('ReportingLogger', () => { it('sends warning logs with correct params', () => { mockStore.getRoktAccountId = jest.fn().mockReturnValue(accountId); - logger.warning('warn'); + logger.warning('warn', ErrorCodes.UNHANDLED_EXCEPTION); expect(mockFetch).toHaveBeenCalled(); const fetchCall = mockFetch.mock.calls[0]; expect(fetchCall[0]).toContain('/v1/errors'); From 7e41e157df4ceabbe4d0eef4fa1f88828c23fa98 Mon Sep 17 00:00:00 2001 From: Alexander Sapountzis Date: Tue, 17 Feb 2026 11:38:02 -0500 Subject: [PATCH 08/11] fix: Update logging url generation --- src/constants.ts | 4 ++++ src/logging/reportingLogger.ts | 5 +++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/constants.ts b/src/constants.ts index bf513a5d1..3072e9bee 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-api.com/v1/log', + errorUrl: 'apps.rokt-api.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/logging/reportingLogger.ts b/src/logging/reportingLogger.ts index 1f0446905..b51edc27c 100644 --- a/src/logging/reportingLogger.ts +++ b/src/logging/reportingLogger.ts @@ -2,6 +2,7 @@ import { ErrorCodes, LogRequestBody, WSDKErrorSeverity } from "./types"; import { FetchUploader, IFetchPayload } from "../uploaders"; import { IStore, SDKConfig } from "../store"; import { SDKInitConfig } from "../sdkRuntimeModels"; +import Constants from "../constants"; interface IReportingLoggerPayload extends IFetchPayload { headers: IFetchPayload['headers'] & { @@ -29,8 +30,8 @@ export class ReportingLogger { private readonly launcherInstanceGuid?: string, rateLimiter?: IRateLimiter, ) { - this.loggingUrl = `https://${config.loggingUrl || 'jssdkcdns.mparticle.com/v1/JS/logs'}`; - this.errorUrl = `https://${config.errorUrl || 'jssdkcdns.mparticle.com/v1/JS/errors'}`; + this.loggingUrl = `https://${config.loggingUrl || Constants.DefaultBaseUrls.loggingUrl}`; + this.errorUrl = `https://${config.errorUrl || Constants.DefaultBaseUrls.errorUrl}`; this.isWebSdkLoggingEnabled = config.isWebSdkLoggingEnabled || false; this.store = store ?? null; this.isEnabled = this.isReportingEnabled(); From 2b829cec704bbd08e4e2d9e0594cb5f816092596 Mon Sep 17 00:00:00 2001 From: Alexander Sapountzis Date: Tue, 17 Feb 2026 12:40:08 -0500 Subject: [PATCH 09/11] fix: Add constants for paths --- test/src/tests-store.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/test/src/tests-store.ts b/test/src/tests-store.ts index 774d08222..3d2a2baa5 100644 --- a/test/src/tests-store.ts +++ b/test/src/tests-store.ts @@ -1678,6 +1678,8 @@ describe('Store', () => { v1SecureServiceUrl: 'jssdks.mparticle.com/v1/JS/', v2SecureServiceUrl: 'jssdks.mparticle.com/v2/JS/', v3SecureServiceUrl: 'foo.customer.mp.com/v3/JS/', + loggingUrl: 'apps.rokt-api.com/v1/log', + errorUrl: 'apps.rokt-api.com/v1/errors', }; expect(result).to.deep.equal(expectedResult); @@ -1703,6 +1705,8 @@ describe('Store', () => { v1SecureServiceUrl: 'custom.domain.com/webevents/v1/JS/', v2SecureServiceUrl: 'custom.domain.com/webevents/v2/JS/', v3SecureServiceUrl: 'custom.domain.com/webevents/v3/JS/', + loggingUrl: 'custom.domain.com/v1/log', + errorUrl: 'custom.domain.com/v1/errors', }; expect(result).to.deep.equal(expectedResult); @@ -1735,6 +1739,8 @@ describe('Store', () => { v1SecureServiceUrl: 'custom.domain.com/webevents/v1/JS/', v2SecureServiceUrl: 'custom.domain.com/webevents/v2/JS/', v3SecureServiceUrl: 'custom.domain.com/webevents/v3/JS/', + loggingUrl: 'custom.domain.com/v1/log', + errorUrl: 'custom.domain.com/v1/errors', }; expect(result).to.deep.equal(expectedResult); @@ -1799,6 +1805,8 @@ describe('Store', () => { v1SecureServiceUrl: 'jssdks.us1.mparticle.com/v1/JS/', v2SecureServiceUrl: 'jssdks.us1.mparticle.com/v2/JS/', v3SecureServiceUrl: 'foo.customer.mp.com/v3/JS/', + loggingUrl: 'apps.rokt-api.com/v1/log', + errorUrl: 'apps.rokt-api.com/v1/errors', }; expect(result).to.deep.equal(expectedResult); From 9934b81e97affdf712412d367d2decbd80392329 Mon Sep 17 00:00:00 2001 From: Alexander Sapountzis Date: Tue, 17 Feb 2026 13:05:49 -0500 Subject: [PATCH 10/11] fix: Address PR Comments --- src/logging/reportingLogger.ts | 6 ++-- src/logging/types.ts | 14 ++++----- src/mp-instance.ts | 4 +-- src/roktManager.ts | 2 +- src/sdkRuntimeModels.ts | 57 +++++++++++++++++++++++++--------- src/store.ts | 4 +-- 6 files changed, 57 insertions(+), 30 deletions(-) diff --git a/src/logging/reportingLogger.ts b/src/logging/reportingLogger.ts index b51edc27c..0f0f3d223 100644 --- a/src/logging/reportingLogger.ts +++ b/src/logging/reportingLogger.ts @@ -55,7 +55,7 @@ export class ReportingLogger { } private sendToServer(url: string, severity: WSDKErrorSeverity, msg: string, code?: ErrorCodes, stackTrace?: string): void { - if(!this.canSendLog(severity)) + if (!this.canSendLog(severity)) return; try { @@ -81,7 +81,7 @@ export class ReportingLogger { private sendError(severity: WSDKErrorSeverity, msg: string, code?: ErrorCodes, stackTrace?: string): void { this.sendToServer(this.errorUrl, severity, msg, code, stackTrace); } - + private buildLogRequest(severity: WSDKErrorSeverity, msg: string, code?: ErrorCodes, stackTrace?: string): LogRequestBody { return { additionalInformation: { @@ -92,7 +92,7 @@ export class ReportingLogger { code: code ?? ErrorCodes.UNKNOWN_ERROR, url: this.getUrl(), deviceInfo: this.getUserAgent(), - stackTrace: stackTrace ?? '', + stackTrace: stackTrace, reporter: this.reporter, integration: this.integration }; diff --git a/src/logging/types.ts b/src/logging/types.ts index 55dce80be..46c431158 100644 --- a/src/logging/types.ts +++ b/src/logging/types.ts @@ -1,22 +1,22 @@ import { valueof } from '../utils'; -export type ErrorCodes = valueof; - export const ErrorCodes = { UNKNOWN_ERROR: 'UNKNOWN_ERROR', UNHANDLED_EXCEPTION: 'UNHANDLED_EXCEPTION', IDENTITY_REQUEST: 'IDENTITY_REQUEST', } as const; +export type ErrorCodes = valueof; + export type ErrorCode = ErrorCodes | string; -export type WSDKErrorSeverity = (typeof WSDKErrorSeverity)[keyof typeof WSDKErrorSeverity]; export const WSDKErrorSeverity = { - ERROR: 'ERROR', - INFO: 'INFO', - WARNING: 'WARNING', + ERROR: 'ERROR', + INFO: 'INFO', + WARNING: 'WARNING', } as const; +export type WSDKErrorSeverity = (typeof WSDKErrorSeverity)[keyof typeof WSDKErrorSeverity]; export type ErrorsRequestBody = { additionalInformation?: Record; @@ -27,6 +27,6 @@ export type ErrorsRequestBody = { integration?: string; reporter?: string; url?: string; - }; +}; export type LogRequestBody = ErrorsRequestBody; diff --git a/src/mp-instance.ts b/src/mp-instance.ts index e4db9c40b..86029e26b 100644 --- a/src/mp-instance.ts +++ b/src/mp-instance.ts @@ -1375,7 +1375,7 @@ export default function mParticleInstance(this: IMParticleWebSDKInstance, instan }; } }; - + const launcherInstanceGuidKey = Constants.Rokt.LauncherInstanceGuidKey; this.setLauncherInstanceGuid = function() { if (!window[launcherInstanceGuidKey] @@ -1383,7 +1383,7 @@ export default function mParticleInstance(this: IMParticleWebSDKInstance, instan window[launcherInstanceGuidKey] = self._Helpers.generateUniqueId(); } }; - + this.getLauncherInstanceGuid = function() { return window[launcherInstanceGuidKey]; }; diff --git a/src/roktManager.ts b/src/roktManager.ts index e8ddd7367..f871a646e 100644 --- a/src/roktManager.ts +++ b/src/roktManager.ts @@ -195,7 +195,7 @@ export default class RoktManager { } if (kit.integrationName) { - this.store.setIntegrationName(kit.integrationName); + this.store?.setIntegrationName(kit.integrationName); } this.processMessageQueue(); diff --git a/src/sdkRuntimeModels.ts b/src/sdkRuntimeModels.ts index 90a30634c..9406cb5fc 100644 --- a/src/sdkRuntimeModels.ts +++ b/src/sdkRuntimeModels.ts @@ -7,18 +7,24 @@ import { SDKEventAttrs, Callback, } from '@mparticle/web-sdk'; -import { IntegrationAttribute, IntegrationAttributes, IStore, WrapperSDKTypes } from './store'; +import { + IntegrationAttribute, + IntegrationAttributes, + IStore, + WrapperSDKTypes, +} from './store'; import Validators from './validators'; import { Dictionary, valueof } from './utils'; import { IKitConfigs } from './configAPIClient'; import { SDKConsentApi, SDKConsentState } from './consent'; import MPSideloadedKit from './sideloadedKit'; import { ISessionManager } from './sessionManager'; -import { ConfiguredKit, MPForwarder, UnregisteredKit } from './forwarders.interfaces'; import { - SDKIdentityApi, - IAliasCallback, -} from './identity.interfaces'; + ConfiguredKit, + MPForwarder, + UnregisteredKit, +} from './forwarders.interfaces'; +import { SDKIdentityApi, IAliasCallback } from './identity.interfaces'; import { ISDKUserAttributeChangeData, ISDKUserIdentityChanges, @@ -36,8 +42,12 @@ import { } from './types'; import { IPixelConfiguration } from './cookieSyncManager'; import _BatchValidator from './mockBatchCreator'; -import { SDKECommerceAPI } from './ecommerce.interfaces'; -import { IErrorLogMessage, IMParticleWebSDKInstance, IntegrationDelays } from './mp-instance'; +import { SDKECommerceAPI } from './ecommerce.interfaces'; +import { + IErrorLogMessage, + IMParticleWebSDKInstance, + IntegrationDelays, +} from './mp-instance'; import Constants from './constants'; import RoktManager, { IRoktLauncherOptions } from './roktManager'; import { IConsoleLogger } from './logger'; @@ -179,7 +189,7 @@ export interface MParticleWebSDK { _resetForTests( MPConfig?: SDKInitConfig, keepPersistence?: boolean, - instance?: IMParticleWebSDKInstance, + instance?: IMParticleWebSDKInstance ): void; configurePixel(config: IPixelConfiguration): void; endSession(): void; @@ -208,9 +218,24 @@ export interface MParticleWebSDK { ): void; logBaseEvent(event: BaseEvent, eventOptions?: SDKEventOptions): void; logError(error: IErrorLogMessage, attrs?: SDKEventAttrs): void; - logLink(selector: string, eventName: string, eventType: valueof, eventInfo: SDKEventAttrs): void; - logForm(selector: string, eventName: string, eventType: valueof, eventInfo: SDKEventAttrs): void; - logPageView(eventName?: string, attrs?: SDKEventAttrs, customFlags?: SDKEventCustomFlags, eventOptions?: SDKEventOptions): void; + logLink( + selector: string, + eventName: string, + eventType: valueof, + eventInfo: SDKEventAttrs + ): void; + logForm( + selector: string, + eventName: string, + eventType: valueof, + eventInfo: SDKEventAttrs + ): void; + logPageView( + eventName?: string, + attrs?: SDKEventAttrs, + customFlags?: SDKEventCustomFlags, + eventOptions?: SDKEventOptions + ): void; setOptOut(isOptingOut: boolean): void; eCommerce: SDKECommerceAPI; isInitialized(): boolean; @@ -228,7 +253,10 @@ export interface MParticleWebSDK { stopTrackingLocation(): void; generateHash(value: string): string; - setIntegrationAttribute(integrationModuleId: number, attrs: IntegrationAttribute): void; + setIntegrationAttribute( + integrationModuleId: number, + attrs: IntegrationAttribute + ): void; getIntegrationAttributes(integrationModuleId: number): IntegrationAttribute; captureTiming(metricName: string): void; } @@ -249,14 +277,13 @@ export interface IMParticleInstanceManager extends MParticleWebSDK { MPSideloadedKit: typeof MPSideloadedKit; Rokt: RoktManager; // https://go.mparticle.com/work/SQDSDKS-7060 - sessionManager: Pick; + sessionManager: Pick; Store: IStore; // Public Methods getInstance(instanceName?: string): IMParticleWebSDKInstance; } - // Used in cases where server requires booleans as strings export type BooleanStringLowerCase = 'false' | 'true'; export type BooleanStringTitleCase = 'False' | 'True'; @@ -365,7 +392,7 @@ export interface SDKHelpersApi { export interface SDKLoggerApi { error(msg: string, code?: ErrorCodes): void; verbose(msg: string): void; - warning(msg: string, code?: ErrorCodes): void; + warning(msg: string): void; setLogLevel(logLevel: LogLevelType): void; } diff --git a/src/store.ts b/src/store.ts index 2961b41a2..f76c1cfdf 100644 --- a/src/store.ts +++ b/src/store.ts @@ -208,8 +208,8 @@ export interface IStore { integrationDelayTimeoutStart: number; // UNIX Timestamp webviewBridgeEnabled?: boolean; wrapperSDKInfo: WrapperSDKInfo; - roktAccountId: string; - integrationName: string; + roktAccountId: string | null; + integrationName: string | null; persistenceData?: IPersistenceMinified; From 1d1ebfa357e90fe3f6c39764ff33db62eb5f48fe Mon Sep 17 00:00:00 2001 From: Alexander Sapountzis Date: Tue, 17 Feb 2026 15:58:46 -0500 Subject: [PATCH 11/11] exclude urls from silo routing --- src/store.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/store.ts b/src/store.ts index f76c1cfdf..3f0a63cc1 100644 --- a/src/store.ts +++ b/src/store.ts @@ -861,7 +861,7 @@ function processDirectBaseUrls( 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') { + if (baseUrlKey === 'configUrl' || baseUrlKey === 'loggingUrl' || baseUrlKey === 'errorUrl') { directBaseUrls[baseUrlKey] = config[baseUrlKey] || defaultBaseUrls[baseUrlKey]; continue;