Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
388b1ac
feat: Create Logging Service
gtamanaha Nov 24, 2025
dc68dd3
fix(apiClient): Update upload URL construction in APIClient and clean…
gtamanaha Nov 25, 2025
8a2e1bc
fix(tests): Update reportingLogger tests to use globalThis instead of…
gtamanaha Nov 25, 2025
d100879
refactor(mParticleInstance): Simplify initialization by removing kitB…
gtamanaha Nov 27, 2025
f0182bc
test(apiClient): Add unit tests for sendLogToServer method with Fetch…
gtamanaha Nov 27, 2025
4c5d663
refactor(reportingLogger): Add return types to private methods for im…
gtamanaha Nov 27, 2025
36e2a4f
refactor(reportingLogger): Allow optional rate limiter in constructor…
gtamanaha Nov 27, 2025
6a94aeb
refactor(reportingLogger): Change rateLimiter type to IRateLimiter an…
gtamanaha Nov 27, 2025
c5085ea
fix(tests): Replace global references from 'global' to 'globalThis' i…
gtamanaha Nov 27, 2025
db1d40c
refactor(logging): Move sendLogToServer method from APIClient to Repo…
gtamanaha Dec 2, 2025
d078785
refactor(apiClient): Remove unused LogRequest import to clean up code
gtamanaha Dec 2, 2025
a88b8df
feat(reportingLogger): Add 'rokt-account-id' header and update constr…
gtamanaha Dec 5, 2025
2db99dd
refactor(errorCodes, reportingLogger): Replace ErrorCodes type defini…
gtamanaha Dec 9, 2025
f7db566
feat(reportingLogger): Introduce default user agent and URL handling …
gtamanaha Dec 10, 2025
f3e25fe
fix(reportingLogger): Update default account ID to '0' and adjust cor…
gtamanaha Dec 10, 2025
b972c57
Revise implementation of ReportingLogger
alexs-mparticle Dec 17, 2025
b3525cd
Revised implementation
alexs-mparticle Dec 18, 2025
e0d5885
Introduce IDENTITY_REQUEST error code and update error logging for id…
alexs-mparticle Dec 18, 2025
73ec2ee
Remove unnecessary semicolons
alexs-mparticle Dec 19, 2025
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.com/v1/log/',
errorUrl: 'apps.rokt.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
8 changes: 6 additions & 2 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/errorCodes';

const { HTTPCodes, Messages, IdentityMethods } = Constants;

Expand Down Expand Up @@ -300,10 +301,13 @@ export default function IdentityAPIClient(
);
} catch (err) {
mpInstance._Store.identityCallInFlight = false;

const errorMessage = (err as Error).message || err.toString();

Logger.error('Error sending identity request to servers' + ' - ' + errorMessage);
Logger.error(
'Error sending identity request to servers' + ' - ' + errorMessage,
ErrorCodes.IDENTITY_REQUEST
);
invokeCallback(
callback,
HTTPCodes.noHttpCoverage,
Expand Down
16 changes: 12 additions & 4 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 { IReportingLogger } from './logging/reportingLogger';
import { ErrorCodes } from './logging/errorCodes';

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: IReportingLogger;

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

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

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

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

if (this.logger.error) {
this.logger.error(msg);
this.reportingLogger.error(msg, code);
Copy link

Copilot AI Dec 17, 2025

Choose a reason for hiding this comment

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

The code attempts to call this.reportingLogger.error() without checking if reportingLogger is defined. Since reportingLogger is optional in the constructor, this will throw a TypeError when reportingLogger is undefined. Add a null check before calling reportingLogger methods.

Copilot uses AI. Check for mistakes.
}
}

Expand All @@ -63,4 +71,4 @@ export class ConsoleLogger implements IConsoleLogger {
console.warn(msg);
}
}
}
}
9 changes: 9 additions & 0 deletions src/logging/errorCodes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { valueof } from '../utils';

export type ErrorCodes = valueof<typeof ErrorCodes>;

export const ErrorCodes = {
UNKNOWN_ERROR: 'UNKNOWN_ERROR',
UNHANDLED_EXCEPTION: 'UNHANDLED_EXCEPTION',
IDENTITY_REQUEST: 'IDENTITY_REQUEST',
} as const;
23 changes: 23 additions & 0 deletions src/logging/logRequest.ts
Original file line number Diff line number Diff line change
@@ -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<string, string>;
code: ErrorCode;
severity: WSDKErrorSeverity;
stackTrace?: string;
deviceInfo?: string;
integration?: string;
reporter?: string;
url?: string;
};

export type LogRequestBody = ErrorsRequestBody;
178 changes: 178 additions & 0 deletions src/logging/reportingLogger.ts
Original file line number Diff line number Diff line change
@@ -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 {
Copy link

Copilot AI Dec 17, 2025

Choose a reason for hiding this comment

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

The getUrl() method accesses window.location.href without checking if it exists, which could throw an error in non-browser environments. Add a fallback similar to getUserAgent() which returns 'no-url-set' when the location is undefined.

Suggested change
private getUrl(): string {
private getUrl(): string {
if (typeof window === 'undefined' || !window.location || !window.location.href) {
return 'no-url-set';
}

Copilot uses AI. Check for mistakes.
return window.location.href;
}

private getUserAgent(): string {
Copy link

Copilot AI Dec 17, 2025

Choose a reason for hiding this comment

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

The getUserAgent() method accesses window.navigator.userAgent without checking if it exists, which could throw an error in non-browser environments. Add a fallback to return 'no-user-agent-set' when navigator is undefined, similar to what the test expects.

Suggested change
private getUserAgent(): string {
private getUserAgent(): string {
if (typeof window === 'undefined' || !window.navigator || !window.navigator.userAgent) {
return 'no-user-agent-set';
}

Copilot uses AI. Check for mistakes.
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<string, string> = {
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',
};
Comment on lines +144 to +147
Copy link

Copilot AI Dec 17, 2025

Choose a reason for hiding this comment

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

The 'rokt-launcher-instance-guid' header is set to this.launcherInstanceGuid which may be undefined. This will result in the header being set to the string 'undefined'. Add a check to either omit the header when undefined or ensure it always has a valid value.

Suggested change
'rokt-launcher-instance-guid': this.launcherInstanceGuid,
'rokt-launcher-version': this.getVersion(),
'rokt-wsdk-version': 'joint',
};
'rokt-launcher-version': this.getVersion(),
'rokt-wsdk-version': 'joint',
};
if (this.launcherInstanceGuid !== undefined) {
headers['rokt-launcher-instance-guid'] = this.launcherInstanceGuid;
}

Copilot uses AI. Check for mistakes.

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<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);

return newCount > limit;
}
}
Loading
Loading