Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Comment thread
alexs-mparticle marked this conversation as resolved.
);

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, codeForReporting?: ErrorCodes): void {
if(this.logLevel === LogLevelType.None)
return;

if (this.logger.error) {
this.logger.error(msg);
if (codeForReporting) {
this.reportingLogger?.error(msg, codeForReporting);
}
}
}

Expand Down
192 changes: 192 additions & 0 deletions src/logging/reportingLogger.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
import { ErrorCodes, LogRequestBody, WSDKErrorSeverity } from "./types";
import { FetchUploader, IFetchPayload } from "../uploaders";
import { IStore, SDKConfig } from "../store";
import { SDKInitConfig } from "../sdkRuntimeModels";
import Constants from "../constants";

// Header key constants
const HEADER_ACCEPT = 'Accept' as const;
const HEADER_CONTENT_TYPE = 'Content-Type' as const;
const HEADER_ROKT_LAUNCHER_VERSION = 'rokt-launcher-version' as const;
const HEADER_ROKT_LAUNCHER_INSTANCE_GUID = 'rokt-launcher-instance-guid' as const;
const HEADER_ROKT_WSDK_VERSION = 'rokt-wsdk-version' as const;

interface IReportingLoggerPayload extends IFetchPayload {
headers: IFetchPayload['headers'] & {
[HEADER_ROKT_LAUNCHER_INSTANCE_GUID]?: string;
[HEADER_ROKT_LAUNCHER_VERSION]: string;
[HEADER_ROKT_WSDK_VERSION]: string;
};
body: string;
}

Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

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

The ReportingLogger class lacks JSDoc documentation. Following the codebase convention (seen in BatchUploader, RoktManager, etc.), exported classes should have JSDoc comments describing their purpose, responsibilities, and high-level behavior. Consider adding a JSDoc comment block above the class declaration.

Suggested change
/**
* Logger responsible for reporting Web SDK logs and errors to the configured backend services.
*
* This class builds and sends log payloads (such as info and error events) using the configured
* logging and error endpoints, SDK/store configuration, and an optional launcher instance GUID.
* It also applies rate limiting via an {@link IRateLimiter} implementation to avoid excessive
* reporting and respects feature flags such as `isWebSdkLoggingEnabled`.
*/

Copilot uses AI. Check for mistakes.
export class ReportingLogger {
private readonly isEnabled: boolean;
private readonly reporter: string = 'mp-wsdk';
private readonly rateLimiter: IRateLimiter;
private store: IStore | null;
private readonly loggingUrl: string;
private readonly errorUrl: string;
private readonly isLoggingEnabled: 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.isLoggingEnabled = config.isLoggingEnabled || 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 +78 to +80
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,
Comment thread
alexs-mparticle marked this conversation as resolved.
url: this.getUrl(),
deviceInfo: this.getUserAgent(),
stackTrace: stackTrace,
reporter: this.reporter,
// Integration will be set to integrationName once the kit connects via RoktManager.attachKit()
integration: this.store?.getIntegrationName() ?? 'mp-wsdk'
};
}

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.isLoggingEnabled;

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

Comment on lines +124 to +133
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'] = {
[HEADER_ACCEPT]: 'text/plain;charset=UTF-8',
[HEADER_CONTENT_TYPE]: 'application/json',
[HEADER_ROKT_LAUNCHER_VERSION]: this.getVersion(),
[HEADER_ROKT_WSDK_VERSION]: 'joint',
};

if (this.launcherInstanceGuid) {
headers[HEADER_ROKT_LAUNCHER_INSTANCE_GUID] = this.launcherInstanceGuid;
}

const accountId = this.store?.getRoktAccountId?.();
if (accountId) {
headers['rokt-account-id'] = accountId;
Comment thread
alexs-mparticle marked this conversation as resolved.
}

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 +186 to +189
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.
Comment thread
alexs-mparticle marked this conversation as resolved.
return newCount > limit;
}
Comment thread
alexs-mparticle marked this conversation as resolved.
}
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;
Comment thread
alexs-mparticle marked this conversation as resolved.

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