Skip to content
Draft
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
23 changes: 23 additions & 0 deletions src/apiClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import { AsyncUploader, FetchUploader, XHRUploader } from './uploaders';
import { IMParticleWebSDKInstance } from './mp-instance';
import { appendUserInfo } from './user-utils';
import { LogRequest } from './logging/logRequest';

export interface IAPIClient {
uploader: BatchUploader | null;
Expand All @@ -27,6 +28,7 @@
forwarder: MPForwarder,
event: IUploadObject
) => void;
sendLogToServer: (log: LogRequest) => void;
}

export interface IForwardingStatsData {
Expand Down Expand Up @@ -231,4 +233,25 @@
}
}
};

this.sendLogToServer = function(logRequest: LogRequest) {
const baseUrl = mpInstance._Helpers.createServiceUrl(
mpInstance._Store.SDKConfig.v2SecureServiceUrl,
mpInstance._Store.devToken
);

const uploadUrl = `${baseUrl}/v1/log`;
const uploader = window.fetch

Check warning on line 244 in src/apiClient.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `globalThis` over `window`.

See more on https://sonarcloud.io/project/issues?id=mParticle_mparticle-web-sdk&issues=AZq71T31rBKUHbxz8nXu&open=AZq71T31rBKUHbxz8nXu&pullRequest=1123
? new FetchUploader(uploadUrl)
: new XHRUploader(uploadUrl);

uploader.upload({
method: 'POST',
headers: {
Accept: 'text/plain;charset=UTF-8',
'Content-Type': 'text/plain;charset=UTF-8',
},
body: JSON.stringify(logRequest),
});
};
}
5 changes: 5 additions & 0 deletions src/logging/errorCodes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export type ErrorCodes = (typeof ErrorCodes)[keyof typeof ErrorCodes];

export const ErrorCodes = {
UNHANDLED_EXCEPTION: 'UNHANDLED_EXCEPTION',
} as const;
Empty file added src/logging/logMessage.ts
Empty file.
21 changes: 21 additions & 0 deletions src/logging/logRequest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { ErrorCodes } from "./errorCodes";

export enum LogRequestSeverity {
Error = 'error',
Warning = 'warning',
Info = 'info',
}

export interface LogRequest {
additionalInformation: {
message: string;
version: string;
};
severity: LogRequestSeverity;
code: ErrorCodes;
url: string;
deviceInfo: string;
stackTrace: string;
reporter: string;
integration: string;
}
128 changes: 128 additions & 0 deletions src/logging/reportingLogger.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import { IAPIClient } from "../apiClient";
import { ErrorCodes } from "./errorCodes";
import { LogRequest, LogRequestSeverity } from "./logRequest";

export interface IReportingLogger {
error(msg: string, code?: ErrorCodes, stackTrace?: string): void;
warning(msg: string, code?: ErrorCodes): void;
}

export class ReportingLogger implements IReportingLogger {
private readonly isEnabled: boolean;
private readonly apiClient: IAPIClient;
private readonly reporter: string = 'mp-wsdk';
private readonly integration: string = 'mp-wsdk';
private readonly rateLimiter: IRateLimiter;

constructor(
apiClient: IAPIClient,
private readonly sdkVersion: string,
rateLimiter?: IRateLimiter,
) {
this.isEnabled = this.isReportingEnabled();
this.apiClient = apiClient;
this.rateLimiter = rateLimiter ?? new RateLimiter();
}

public error(msg: string, code?: ErrorCodes, stackTrace?: string) {
this.sendLog(LogRequestSeverity.Error, msg, code ?? ErrorCodes.UNHANDLED_EXCEPTION, stackTrace);
};

public warning(msg: string, code?: ErrorCodes) {
this.sendLog(LogRequestSeverity.Warning, msg, code ?? ErrorCodes.UNHANDLED_EXCEPTION);
};

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

const logRequest: LogRequest = {
additionalInformation: {
message: msg,
version: this.sdkVersion,
},
severity: severity,
code: code,
url: this.getUrl(),
deviceInfo: this.getUserAgent(),
stackTrace: stackTrace ?? '',
reporter: this.reporter,
integration: this.integration,
};

this.apiClient.sendLogToServer(logRequest);
}

private isReportingEnabled(): boolean {
return (
this.isRoktDomainPresent() &&
(this.isFeatureFlagEnabled() ||
this.isDebugModeEnabled())
);
}

private isRoktDomainPresent(): boolean {
return Boolean(window['ROKT_DOMAIN']);
}

private isFeatureFlagEnabled(): boolean {
return window.

Check warning on line 74 in src/logging/reportingLogger.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `globalThis` over `window`.

See more on https://sonarcloud.io/project/issues?id=mParticle_mparticle-web-sdk&issues=AZq71T1IrBKUHbxz8nXn&open=AZq71T1IrBKUHbxz8nXn&pullRequest=1123
mParticle?.
config?.
isWebSdkLoggingEnabled ?? false;
}

private isDebugModeEnabled(): boolean {
return (
window.

Check warning on line 82 in src/logging/reportingLogger.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `globalThis` over `window`.

See more on https://sonarcloud.io/project/issues?id=mParticle_mparticle-web-sdk&issues=AZq71T1IrBKUHbxz8nXo&open=AZq71T1IrBKUHbxz8nXo&pullRequest=1123
location?.
search?.
toLowerCase()?.
includes('mp_enable_logging=true') ?? false
);
}

private canSendLog(severity: LogRequestSeverity): boolean {
return this.isEnabled && !this.isRateLimited(severity);
}

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

private getUrl(): string {
return window.location.href;

Check warning on line 99 in src/logging/reportingLogger.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `globalThis` over `window`.

See more on https://sonarcloud.io/project/issues?id=mParticle_mparticle-web-sdk&issues=AZq71T1IrBKUHbxz8nXp&open=AZq71T1IrBKUHbxz8nXp&pullRequest=1123
}

private getUserAgent(): string {
return window.navigator.userAgent;

Check warning on line 103 in src/logging/reportingLogger.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `globalThis` over `window`.

See more on https://sonarcloud.io/project/issues?id=mParticle_mparticle-web-sdk&issues=AZq71T1IrBKUHbxz8nXq&open=AZq71T1IrBKUHbxz8nXq&pullRequest=1123
}
}

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

export class RateLimiter implements IRateLimiter {
private readonly rateLimits: Map<LogRequestSeverity, number> = new Map([
[LogRequestSeverity.Error, 10],
[LogRequestSeverity.Warning, 10],
[LogRequestSeverity.Info, 10],
]);
private logCount: Map<LogRequestSeverity, number> = new Map();

public incrementAndCheck(severity: LogRequestSeverity): boolean {
const count = this.logCount.get(severity) || 0;
const limit = this.rateLimits.get(severity) || 10;

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

return newCount > limit;
}
}
1 change: 1 addition & 0 deletions src/sdkRuntimeModels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,7 @@ export interface SDKInitConfig
identityCallback?: IdentityCallback;

launcherOptions?: IRoktLauncherOptions;
isWebSdkLoggingEnabled?: boolean;

rq?: Function[] | any[];
logger?: IConsoleLogger;
Expand Down
129 changes: 129 additions & 0 deletions test/jest/apiClient.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import { describe, test, expect, beforeEach, afterEach, jest } from '@jest/globals';
import { LogRequest, LogRequestSeverity } from '../../src/logging/logRequest';
import { ErrorCodes } from '../../src/logging/errorCodes';
import APIClient from '../../src/apiClient';

jest.mock('../../src/uploaders', () => {
const fetchUploadMock = jest.fn(() => Promise.resolve({} as Response));
const xhrUploadMock = jest.fn(() => Promise.resolve({} as Response));

class MockFetchUploader {
constructor(public url: string) {}
upload = fetchUploadMock;
}
class MockXHRUploader {
constructor(public url: string) {}
upload = xhrUploadMock;
}

(globalThis as any).__fetchUploadSpy = fetchUploadMock;
(globalThis as any).__xhrUploadSpy = xhrUploadMock;

return {
AsyncUploader: class {},

Check warning on line 23 in test/jest/apiClient.spec.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Unexpected empty class.

See more on https://sonarcloud.io/project/issues?id=mParticle_mparticle-web-sdk&issues=AZrFqq57E1EndTSChu6f&open=AZrFqq57E1EndTSChu6f&pullRequest=1123
FetchUploader: MockFetchUploader,
XHRUploader: MockXHRUploader,
};
});

describe('apiClient.sendLogToServer', () => {
let mpInstance: any;
let logRequest: LogRequest;
let originalWindow: any;
let originalFetch: any;
let kitBlocker: any = {
kitBlockingEnabled: false,
dataPlanMatchLookups: {},
};

beforeEach(() => {
jest.resetModules();

originalWindow = (global as any).window;

Check warning on line 42 in test/jest/apiClient.spec.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `globalThis` over `global`.

See more on https://sonarcloud.io/project/issues?id=mParticle_mparticle-web-sdk&issues=AZrFqq57E1EndTSChu6g&open=AZrFqq57E1EndTSChu6g&pullRequest=1123
originalFetch = (global as any).window?.fetch;

Check warning on line 43 in test/jest/apiClient.spec.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `globalThis` over `global`.

See more on https://sonarcloud.io/project/issues?id=mParticle_mparticle-web-sdk&issues=AZrFqq57E1EndTSChu6h&open=AZrFqq57E1EndTSChu6h&pullRequest=1123

mpInstance = {
_Helpers: {
createServiceUrl: jest.fn((url: string, token: string) => `https://api.fake.com/${token}`)
},
_Store: {
SDKConfig: { v2SecureServiceUrl: 'someUrl' },
devToken: 'testToken123'
}
};

logRequest = {
additionalInformation: {
message: 'test',
version: '1.0.0'
},
severity: LogRequestSeverity.Error,
code: ErrorCodes.UNHANDLED_EXCEPTION,
url: 'https://example.com',
deviceInfo: 'test',
stackTrace: 'test',
reporter: 'test',
integration: 'test'
};

// @ts-ignore
(global as any).window = {};

Check warning on line 70 in test/jest/apiClient.spec.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `globalThis` over `global`.

See more on https://sonarcloud.io/project/issues?id=mParticle_mparticle-web-sdk&issues=AZrFqq57E1EndTSChu6i&open=AZrFqq57E1EndTSChu6i&pullRequest=1123

const fetchSpy = (globalThis as any).__fetchUploadSpy as jest.Mock;
const xhrSpy = (globalThis as any).__xhrUploadSpy as jest.Mock;
if (fetchSpy) fetchSpy.mockClear();
if (xhrSpy) xhrSpy.mockClear();
});

afterEach(() => {
(globalThis as any).window = originalWindow;
if (originalFetch !== undefined) {
(globalThis as any).window.fetch = originalFetch;
}
jest.clearAllMocks();
});

test('should use FetchUploader if window.fetch is available', () => {
(globalThis as any).window.fetch = jest.fn();

const uploadSpy = (global as any).__fetchUploadSpy as jest.Mock;

Check warning on line 89 in test/jest/apiClient.spec.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `globalThis` over `global`.

See more on https://sonarcloud.io/project/issues?id=mParticle_mparticle-web-sdk&issues=AZrFqq57E1EndTSChu6o&open=AZrFqq57E1EndTSChu6o&pullRequest=1123
const client = new APIClient(mpInstance, kitBlocker);

client.sendLogToServer(logRequest);

validateUploadCall(uploadSpy, logRequest, mpInstance);
});

test('should use XHRUploader if window.fetch is not available', () => {
delete (global as any).window.fetch;

Check warning on line 98 in test/jest/apiClient.spec.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `globalThis` over `global`.

See more on https://sonarcloud.io/project/issues?id=mParticle_mparticle-web-sdk&issues=AZrFqq57E1EndTSChu6p&open=AZrFqq57E1EndTSChu6p&pullRequest=1123

const uploadSpy = (global as any).__xhrUploadSpy as jest.Mock;

Check warning on line 100 in test/jest/apiClient.spec.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `globalThis` over `global`.

See more on https://sonarcloud.io/project/issues?id=mParticle_mparticle-web-sdk&issues=AZrFqq57E1EndTSChu6q&open=AZrFqq57E1EndTSChu6q&pullRequest=1123
const client = new APIClient(mpInstance, kitBlocker);

client.sendLogToServer(logRequest);

validateUploadCall(uploadSpy, logRequest, mpInstance);
});

function validateUploadCall(uploadSpy: jest.Mock, expectedLogRequest: LogRequest, mpInstance: any) {
expect(uploadSpy).toHaveBeenCalledTimes(1);
expect(uploadSpy.mock.calls.length).toBeGreaterThan(0);
const firstCall = uploadSpy.mock.calls[0] as any[];
expect(firstCall).toBeDefined();
const call = firstCall[0];
expect(call).toBeDefined();
expect((call as any).method).toBe('POST');
expect((call as any).body).toBe(JSON.stringify(expectedLogRequest));
expect((call as any).headers).toMatchObject({
Accept: 'text/plain;charset=UTF-8',
'Content-Type': 'text/plain;charset=UTF-8'
});
expect(mpInstance._Helpers.createServiceUrl).toHaveBeenCalledWith(
mpInstance._Store.SDKConfig.v2SecureServiceUrl,
mpInstance._Store.devToken
);
}
});



Loading
Loading