Skip to content
4 changes: 4 additions & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand All @@ -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,
Expand Down
10 changes: 7 additions & 3 deletions src/identityApiClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
IIdentityResponse,
} from './identity-user-interfaces';
import { IMParticleWebSDKInstance } from './mp-instance';
import { ErrorCodes } from './logging/types';

const { HTTPCodes, Messages, IdentityMethods } = Constants;

Expand Down Expand Up @@ -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,
Copy link

Copilot AI Feb 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The string concatenation here uses both the ' + ' operator and string concatenation with quotes. For consistency and readability, consider using template literals instead: Error sending identity request to servers - ${errorMessage}

Suggested change
'Error sending identity request to servers' + ' - ' + errorMessage,
`Error sending identity request to servers - ${errorMessage}`,

Copilot uses AI. Check for mistakes.
ErrorCodes.IDENTITY_REQUEST
);

mpInstance.processQueueOnIdentityFailure?.();
invokeCallback(callback, HTTPCodes.noHttpCoverage, errorMessage);
}
Expand Down
19 changes: 14 additions & 5 deletions src/logger.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,21 @@
import { LogLevelType, SDKInitConfig, SDKLoggerApi } from './sdkRuntimeModels';
import { ReportingLogger } from './logging/reportingLogger';
import { ErrorCodes } from './logging/types';

export type ILoggerConfig = Pick<SDKInitConfig, 'logLevel' | 'logger'>;
export type IConsoleLogger = Partial<Pick<SDKLoggerApi, 'error' | 'warning' | 'verbose'>>;

export class Logger {
private logLevel: LogLevelType;
private logger: IConsoleLogger;
private reportingLogger: ReportingLogger;

constructor(config: ILoggerConfig) {
constructor(config: ILoggerConfig,
reportingLogger?: ReportingLogger,
) {
this.logLevel = config.logLevel ?? LogLevelType.Warning;
this.logger = config.logger ?? new ConsoleLogger();
this.reportingLogger = reportingLogger;
}

public verbose(msg: string): void {
Expand All @@ -22,21 +28,24 @@ export class Logger {
}

public warning(msg: string): void {
if(this.logLevel === LogLevelType.None)
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);
}
}

public error(msg: string): void {
if(this.logLevel === LogLevelType.None)
public error(msg: string, code?: ErrorCodes): void {
if(this.logLevel === LogLevelType.None)
return;

if (this.logger.error) {
this.logger.error(msg);
if (code) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm unsure if this is a good way to determine whether to report the error to our backend.
I would make it more explicit, or at a minimum change this param name to codeForReporting or something to make it obvious it's doing something additional.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 on renaming param name to codeForReporting from code

this.reportingLogger?.error(msg, code);
}
}
}

Expand Down
185 changes: 185 additions & 0 deletions src/logging/reportingLogger.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
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'] & {
'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';
private readonly integration: string = 'mp-wsdk';
Comment on lines +18 to +19
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would these ver differ and if not should we make them one string? or have a constant and set each to the constant?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mattbodle what is the actual difference between these two?

private readonly rateLimiter: IRateLimiter;
private store: IStore | null;
private readonly loggingUrl: string;
private readonly errorUrl: string;
private readonly isWebSdkLoggingEnabled: boolean;

constructor(
config: SDKConfig | SDKInitConfig | any,
Copy link

Copilot AI Feb 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The config parameter is typed as 'SDKConfig | SDKInitConfig | any', where 'any' removes all type safety. This makes it impossible for TypeScript to catch type errors in config usage. Consider creating a union type or interface that includes all valid config properties, or at minimum use 'unknown' instead of 'any' to require explicit type checking.

Copilot uses AI. Check for mistakes.
private readonly sdkVersion: string,
store?: IStore,
private readonly launcherInstanceGuid?: string,
rateLimiter?: IRateLimiter,
) {
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();
Copy link

Copilot AI Feb 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The isEnabled flag is calculated once in the constructor and never updated. If the feature flag (isWebSdkLoggingEnabled) changes after initialization, or if the debug mode query parameter is added/removed during the page lifecycle, the reporting behavior won't reflect these changes. Consider making isEnabled a getter that re-evaluates the condition on each access, or add a method to update the flag when configuration changes.

Copilot uses AI. Check for mistakes.
this.rateLimiter = rateLimiter ?? new RateLimiter();
}

public setStore(store: IStore): void {
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) {
Copy link

Copilot AI Feb 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Trailing whitespace on this line. Remove the trailing whitespace for consistency with code style.

Copilot uses AI. Check for mistakes.
this.sendError(WSDKErrorSeverity.WARNING, msg, code);
}

private sendToServer(url: string, severity: WSDKErrorSeverity, msg: string, code?: ErrorCodes, stackTrace?: string): void {
if (!this.canSendLog(severity))
return;

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);
});
} catch (error) {
console.error('ReportingLogger: Failed to send log', error);
}
Comment on lines +72 to +74
Copy link

Copilot AI Feb 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error handling silently catches and logs all exceptions to console. In production, this could mask issues with the reporting service itself. Consider adding a flag or counter to track repeated failures, or consider not catching certain types of errors (like network timeouts) that might indicate a systemic issue that should be addressed.

Copilot uses AI. Check for mistakes.
}

private sendLog(severity: WSDKErrorSeverity, msg: string, code?: ErrorCodes, stackTrace?: string): void {
this.sendToServer(this.loggingUrl, severity, msg, code, stackTrace);
}

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: {
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.store?.getIntegrationName?.() ?? `mParticle_wsdkv_${this.sdkVersion}`;
Copy link

Copilot AI Feb 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The getVersion() method returns either the integration name from store or a fallback string with SDK version. However, the header is named 'rokt-launcher-version' which suggests it should contain version information, not a name. If store.getIntegrationName() returns a name like 'custom-integration-name' (as shown in test line 197), this won't be a version. Consider either renaming the header to 'rokt-launcher-name' or ensuring the value is actually a version string.

Suggested change
return this.store?.getIntegrationName?.() ?? `mParticle_wsdkv_${this.sdkVersion}`;
return `mParticle_wsdkv_${this.sdkVersion}`;

Copilot uses AI. Check for mistakes.
}

Copy link

Copilot AI Feb 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Trailing whitespace on this line. Remove the trailing whitespace for consistency with code style.

Suggested change

Copilot uses AI. Check for mistakes.
private isReportingEnabled(): boolean {
return this.isDebugModeEnabled() ||
(this.isRoktDomainPresent() && this.isFeatureFlagEnabled());
}

private isRoktDomainPresent(): boolean {
return typeof window !== 'undefined' && Boolean(window['ROKT_DOMAIN']);
}

private isFeatureFlagEnabled = (): boolean => this.isWebSdkLoggingEnabled;

private isDebugModeEnabled(): boolean {
return (
typeof window !== 'undefined' &&
(window.
location?.
search?.
toLowerCase()?.
includes('mp_enable_logging=true') ?? false)
);
}

Comment on lines +117 to +126
Copy link

Copilot AI Feb 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The optional chaining across multiple lines (116-120) is fragile and hard to read. Consider simplifying this check by extracting the query parameter check into a separate helper function, or by checking for window.location first and then checking the search parameter. This would make the code more maintainable and testable.

Suggested change
return (
typeof window !== 'undefined' &&
(window.
location?.
search?.
toLowerCase()?.
includes('mp_enable_logging=true') ?? false)
);
}
return this.isDebugModeEnabledViaQueryParam();
}
private isDebugModeEnabledViaQueryParam(): boolean {
if (typeof window === 'undefined' || !window.location || !window.location.search) {
return false;
}
const search = window.location.search.toLowerCase();
return search.includes('mp_enable_logging=true');
}

Copilot uses AI. Check for mistakes.
private canSendLog(severity: WSDKErrorSeverity): boolean {
return this.isEnabled && !this.isRateLimited(severity);
}

private isRateLimited(severity: WSDKErrorSeverity): boolean {
return this.rateLimiter.incrementAndCheck(severity);
}

private getUrl(): string | undefined {
return typeof window !== 'undefined' ? window.location?.href : undefined;
}

private getUserAgent(): string | undefined {
return typeof window !== 'undefined' ? window.navigator?.userAgent : undefined;
}

private getHeaders(): IReportingLoggerPayload['headers'] {
const headers: IReportingLoggerPayload['headers'] = {
Accept: 'text/plain;charset=UTF-8',
'Content-Type': 'application/json',
'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;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't use a header keys var/enum for these?

}

return headers;
}
}

export interface IRateLimiter {
incrementAndCheck(severity: WSDKErrorSeverity): boolean;
}

export class RateLimiter implements IRateLimiter {
private readonly rateLimits: Map<WSDKErrorSeverity, number> = new Map([
[WSDKErrorSeverity.ERROR, 10],
[WSDKErrorSeverity.WARNING, 10],
[WSDKErrorSeverity.INFO, 10],
]);
private logCount: Map<WSDKErrorSeverity, number> = 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);

Comment on lines +179 to +182
Copy link

Copilot AI Feb 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Trailing whitespace on this line. Remove the trailing whitespace for consistency with code style.

Suggested change
const newCount = count + 1;
this.logCount.set(severity, newCount);
const newCount = count + 1;
this.logCount.set(severity, newCount);

Copilot uses AI. Check for mistakes.
return newCount > limit;
}
}
32 changes: 32 additions & 0 deletions src/logging/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { valueof } from '../utils';

export const ErrorCodes = {
UNKNOWN_ERROR: 'UNKNOWN_ERROR',
UNHANDLED_EXCEPTION: 'UNHANDLED_EXCEPTION',
IDENTITY_REQUEST: 'IDENTITY_REQUEST',
} as const;

export type ErrorCodes = valueof<typeof ErrorCodes>;

export type ErrorCode = ErrorCodes | string;

export const WSDKErrorSeverity = {
ERROR: 'ERROR',
INFO: 'INFO',
WARNING: 'WARNING',
} as const;

export type WSDKErrorSeverity = (typeof WSDKErrorSeverity)[keyof typeof WSDKErrorSeverity];

export type ErrorsRequestBody = {
additionalInformation?: Record<string, string>;
code: ErrorCode;
severity: WSDKErrorSeverity;
stackTrace?: string;
deviceInfo?: string;
integration?: string;
reporter?: string;
url?: string;
};

export type LogRequestBody = ErrorsRequestBody;
Loading
Loading