Skip to content

Commit 307f752

Browse files
gtamanahaalexs-mparticle
authored andcommitted
feat: Create Logging Service
1 parent 933bb77 commit 307f752

File tree

8 files changed

+305
-8
lines changed

8 files changed

+305
-8
lines changed

src/apiClient.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { IMParticleUser, ISDKUserAttributes } from './identity-user-interfaces';
1010
import { AsyncUploader, FetchUploader, XHRUploader } from './uploaders';
1111
import { IMParticleWebSDKInstance } from './mp-instance';
1212
import { appendUserInfo } from './user-utils';
13+
import { LogRequest } from './logging/logRequest';
1314

1415
export interface IAPIClient {
1516
uploader: BatchUploader | null;
@@ -27,6 +28,7 @@ export interface IAPIClient {
2728
forwarder: MPForwarder,
2829
event: IUploadObject
2930
) => void;
31+
sendLogToServer: (log: LogRequest) => void;
3032
}
3133

3234
export interface IForwardingStatsData {
@@ -231,4 +233,26 @@ export default function APIClient(
231233
}
232234
}
233235
};
236+
237+
this.sendLogToServer = function(logRequest: LogRequest) {
238+
const baseUrl = mpInstance._Helpers.createServiceUrl(
239+
mpInstance._Store.SDKConfig.v2SecureServiceUrl,
240+
mpInstance._Store.devToken
241+
);
242+
const uploadUrl = `apps.stage.rokt.com/v1/log/v1/log`;
243+
// const uploadUrl = `${baseUrl}/v1/log`;
244+
245+
const uploader = window.fetch
246+
? new FetchUploader(uploadUrl)
247+
: new XHRUploader(uploadUrl);
248+
249+
uploader.upload({
250+
method: 'POST',
251+
headers: {
252+
Accept: 'text/plain;charset=UTF-8',
253+
'Content-Type': 'text/plain;charset=UTF-8',
254+
},
255+
body: JSON.stringify(logRequest),
256+
});
257+
};
234258
}

src/logging/errorCodes.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export type ErrorCodes = (typeof ErrorCodes)[keyof typeof ErrorCodes];
2+
3+
export const ErrorCodes = {
4+
UNHANDLED_EXCEPTION: 'UNHANDLED_EXCEPTION',
5+
} as const;

src/logging/logMessage.ts

Whitespace-only changes.

src/logging/logRequest.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { ErrorCodes } from "./errorCodes";
2+
3+
export enum LogRequestSeverity {
4+
Error = 'error',
5+
Warning = 'warning',
6+
Info = 'info',
7+
}
8+
9+
export interface LogRequest {
10+
additionalInformation: {
11+
message: string;
12+
version: string;
13+
};
14+
severity: LogRequestSeverity;
15+
code: ErrorCodes;
16+
url: string;
17+
deviceInfo: string;
18+
stackTrace: string;
19+
reporter: string;
20+
integration: string;
21+
}

src/logging/reportingLogger.ts

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
import { LogLevelType, SDKLoggerApi } from "../sdkRuntimeModels";
2+
import { IAPIClient } from "../apiClient";
3+
import { ErrorCodes } from "./errorCodes";
4+
import { LogRequest, LogRequestSeverity } from "./logRequest";
5+
6+
export interface IReportingLogger {
7+
error(msg: string, code?: ErrorCodes, stackTrace?: string): void;
8+
warning(msg: string, code?: ErrorCodes): void;
9+
}
10+
11+
export class ReportingLogger implements IReportingLogger {
12+
private readonly isEnabled: boolean;
13+
private readonly apiClient: IAPIClient;
14+
private readonly reporter: string = 'mp-wsdk';
15+
private readonly integration: string = 'mp-wsdk';
16+
private readonly rateLimiter: RateLimiter = new RateLimiter();
17+
18+
constructor(
19+
apiClient: IAPIClient,
20+
private readonly sdkVersion: string,
21+
) {
22+
this.isEnabled = this.isReportingEnabled();
23+
this.apiClient = apiClient;
24+
this.rateLimiter = new RateLimiter();
25+
}
26+
27+
public error(msg: string, code?: ErrorCodes, stackTrace?: string) {
28+
this.sendLog(LogRequestSeverity.Error, msg, code ?? ErrorCodes.UNHANDLED_EXCEPTION, stackTrace);
29+
};
30+
31+
public warning(msg: string, code?: ErrorCodes) {
32+
this.sendLog(LogRequestSeverity.Warning, msg, code ?? ErrorCodes.UNHANDLED_EXCEPTION);
33+
};
34+
35+
private sendLog(
36+
severity: LogRequestSeverity,
37+
msg: string,
38+
code: ErrorCodes,
39+
stackTrace?: string
40+
): void {
41+
if(!this.canSendLog(severity))
42+
return;
43+
44+
const logRequest: LogRequest = {
45+
additionalInformation: {
46+
message: msg,
47+
version: this.sdkVersion,
48+
},
49+
severity: severity,
50+
code: code,
51+
url: this.getUrl(),
52+
deviceInfo: this.getUserAgent(),
53+
stackTrace: stackTrace ?? '',
54+
reporter: this.reporter,
55+
integration: this.integration,
56+
};
57+
58+
this.apiClient.sendLogToServer(logRequest);
59+
}
60+
61+
private isReportingEnabled() {
62+
return (
63+
this.isRoktDomainPresent() &&
64+
(this.isFeatureFlagEnabled() ||
65+
this.isDebugModeEnabled())
66+
);
67+
}
68+
69+
private isRoktDomainPresent() {
70+
return window['ROKT_DOMAIN'];
71+
}
72+
73+
private isFeatureFlagEnabled() {
74+
return window.
75+
mParticle?.
76+
config?.
77+
isWebSdkLoggingEnabled ?? false;
78+
}
79+
80+
private isDebugModeEnabled() {
81+
return (
82+
window.
83+
location?.
84+
search?.
85+
toLowerCase()?.
86+
includes('mp_enable_logging=true') ?? false
87+
);
88+
}
89+
90+
private canSendLog(severity: LogRequestSeverity): boolean {
91+
return this.isEnabled && !this.isRateLimited(severity);
92+
}
93+
94+
private isRateLimited(severity: LogRequestSeverity): boolean {
95+
return this.rateLimiter.incrementAndCheck(severity);
96+
}
97+
98+
private getUrl(): string {
99+
return window.location.href;
100+
}
101+
102+
private getUserAgent(): string {
103+
return window.navigator.userAgent;
104+
}
105+
}
106+
107+
export interface IRateLimiter {
108+
incrementAndCheck(severity: LogRequestSeverity): boolean;
109+
}
110+
111+
export class RateLimiter implements IRateLimiter {
112+
private readonly rateLimits: Map<LogRequestSeverity, number> = new Map([
113+
[LogRequestSeverity.Error, 10],
114+
[LogRequestSeverity.Warning, 10],
115+
[LogRequestSeverity.Info, 10],
116+
]);
117+
private logCount: Map<LogRequestSeverity, number> = new Map();
118+
119+
public incrementAndCheck(severity: LogRequestSeverity): boolean {
120+
const count = this.logCount.get(severity) || 0;
121+
const limit = this.rateLimits.get(severity) || 10;
122+
123+
const newCount = count + 1;
124+
this.logCount.set(severity, newCount);
125+
126+
return newCount > limit;
127+
}
128+
}

src/mp-instance.ts

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,10 @@ export default function mParticleInstance(this: IMParticleWebSDKInstance, instan
162162
);
163163
}
164164

165-
runPreConfigFetchInitialization(this, apiKey, config);
165+
const kitBlocker = createKitBlocker(config, this);
166+
runPreConfigFetchInitialization(this, apiKey, config, kitBlocker);
167+
debugger;
168+
this.Logger.error('gt error test');
166169

167170
// config code - Fetch config when requestConfig = true, otherwise, proceed with SDKInitialization
168171
// Since fetching the configuration is asynchronous, we must pass completeSDKInitialization
@@ -185,10 +188,10 @@ export default function mParticleInstance(this: IMParticleWebSDKInstance, instan
185188
result
186189
);
187190

188-
completeSDKInitialization(apiKey, mergedConfig, this);
191+
completeSDKInitialization(apiKey, mergedConfig, this, kitBlocker);
189192
});
190193
} else {
191-
completeSDKInitialization(apiKey, config, this);
194+
completeSDKInitialization(apiKey, config, this, kitBlocker);
192195
}
193196
} else {
194197
console.error(
@@ -1361,11 +1364,9 @@ export default function mParticleInstance(this: IMParticleWebSDKInstance, instan
13611364
}
13621365

13631366
// Some (server) config settings need to be returned before they are set on SDKConfig in a self hosted environment
1364-
function completeSDKInitialization(apiKey, config, mpInstance) {
1365-
const kitBlocker = createKitBlocker(config, mpInstance);
1367+
function completeSDKInitialization(apiKey, config, mpInstance, kitBlocker: KitBlocker) {
13661368
const { getFeatureFlag } = mpInstance._Helpers;
13671369

1368-
mpInstance._APIClient = new APIClient(mpInstance, kitBlocker);
13691370
mpInstance._Forwarders = new Forwarders(mpInstance, kitBlocker);
13701371
mpInstance._Store.processConfig(config);
13711372

@@ -1549,8 +1550,9 @@ function createIdentityCache(mpInstance) {
15491550
});
15501551
}
15511552

1552-
function runPreConfigFetchInitialization(mpInstance, apiKey, config) {
1553-
mpInstance.Logger = new Logger(config);
1553+
function runPreConfigFetchInitialization(mpInstance, apiKey, config, kitBlocker: KitBlocker) {
1554+
mpInstance._APIClient = new APIClient(mpInstance, kitBlocker);
1555+
mpInstance.Logger = new Logger(config, mpInstance._APIClient);
15541556
mpInstance._Store = new Store(config, mpInstance, apiKey);
15551557
window.mParticle.Store = mpInstance._Store;
15561558
mpInstance.Logger.verbose(StartingInitialization);

src/sdkRuntimeModels.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -311,6 +311,7 @@ export interface SDKInitConfig
311311
identityCallback?: IdentityCallback;
312312

313313
launcherOptions?: IRoktLauncherOptions;
314+
isWebSdkLoggingEnabled?: boolean;
314315

315316
rq?: Function[] | any[];
316317
logger?: IConsoleLogger;

test/jest/reportingLogger.spec.ts

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
import { RateLimiter, ReportingLogger } from '../../src/logging/reportingLogger';
2+
import { LogRequestSeverity } from '../../src/logging/logRequest';
3+
import { ErrorCodes } from '../../src/logging/errorCodes';
4+
5+
describe('ReportingLogger', () => {
6+
let apiClient: any;
7+
let logger: ReportingLogger;
8+
const sdkVersion = '1.2.3';
9+
10+
beforeEach(() => {
11+
apiClient = { sendLogToServer: jest.fn() };
12+
13+
// Mock location object to allow modifying search property
14+
delete (window as any).location;
15+
(window as any).location = {
16+
href: 'https://e.com',
17+
search: ''
18+
};
19+
20+
Object.assign(window, {
21+
navigator: { userAgent: 'ua' },
22+
mParticle: { config: { isWebSdkLoggingEnabled: true } },
23+
ROKT_DOMAIN: 'set'
24+
});
25+
logger = new ReportingLogger(apiClient, sdkVersion);
26+
});
27+
28+
afterEach(() => {
29+
jest.clearAllMocks();
30+
delete (window as any).ROKT_DOMAIN;
31+
delete (window as any).mParticle;
32+
});
33+
34+
it('sends error logs with correct params', () => {
35+
logger.error('msg', ErrorCodes.UNHANDLED_EXCEPTION, 'stack');
36+
expect(apiClient.sendLogToServer).toHaveBeenCalledWith(expect.objectContaining({
37+
severity: LogRequestSeverity.Error,
38+
code: ErrorCodes.UNHANDLED_EXCEPTION,
39+
stackTrace: 'stack'
40+
}));
41+
});
42+
43+
it('sends warning logs with correct params', () => {
44+
logger.warning('warn');
45+
expect(apiClient.sendLogToServer).toHaveBeenCalledWith(expect.objectContaining({
46+
severity: LogRequestSeverity.Warning
47+
}));
48+
});
49+
50+
it('does not log if ROKT_DOMAIN missing', () => {
51+
delete (window as any).ROKT_DOMAIN;
52+
logger = new ReportingLogger(apiClient, sdkVersion);
53+
logger.error('x');
54+
expect(apiClient.sendLogToServer).not.toHaveBeenCalled();
55+
});
56+
57+
it('does not log if feature flag and debug mode off', () => {
58+
window.mParticle.config.isWebSdkLoggingEnabled = false;
59+
window.location.search = '';
60+
logger = new ReportingLogger(apiClient, sdkVersion);
61+
logger.error('x');
62+
expect(apiClient.sendLogToServer).not.toHaveBeenCalled();
63+
});
64+
65+
it('logs if debug mode on even if feature flag off', () => {
66+
window.mParticle.config.isWebSdkLoggingEnabled = false;
67+
window.location.search = '?mp_enable_logging=true';
68+
logger = new ReportingLogger(apiClient, sdkVersion);
69+
logger.error('x');
70+
expect(apiClient.sendLogToServer).toHaveBeenCalled();
71+
});
72+
73+
it('rate limits after 10 errors', () => {
74+
for (let i = 0; i < 12; i++) logger.error('err');
75+
expect(apiClient.sendLogToServer).toHaveBeenCalledTimes(10);
76+
});
77+
});
78+
79+
describe('RateLimiter', () => {
80+
let rateLimiter: RateLimiter;
81+
beforeEach(() => {
82+
rateLimiter = new RateLimiter();
83+
});
84+
85+
it('allows up to 10 error logs then rate limits', () => {
86+
for (let i = 0; i < 10; i++) {
87+
expect(rateLimiter.incrementAndCheck(LogRequestSeverity.Error)).toBe(false);
88+
}
89+
expect(rateLimiter.incrementAndCheck(LogRequestSeverity.Error)).toBe(true);
90+
expect(rateLimiter.incrementAndCheck(LogRequestSeverity.Error)).toBe(true);
91+
});
92+
93+
it('allows up to 10 warning logs then rate limits', () => {
94+
for (let i = 0; i < 10; i++) {
95+
expect(rateLimiter.incrementAndCheck(LogRequestSeverity.Warning)).toBe(false);
96+
}
97+
expect(rateLimiter.incrementAndCheck(LogRequestSeverity.Warning)).toBe(true);
98+
expect(rateLimiter.incrementAndCheck(LogRequestSeverity.Warning)).toBe(true);
99+
});
100+
101+
it('allows up to 10 info logs then rate limits', () => {
102+
for (let i = 0; i < 10; i++) {
103+
expect(rateLimiter.incrementAndCheck(LogRequestSeverity.Info)).toBe(false);
104+
}
105+
expect(rateLimiter.incrementAndCheck(LogRequestSeverity.Info)).toBe(true);
106+
expect(rateLimiter.incrementAndCheck(LogRequestSeverity.Info)).toBe(true);
107+
});
108+
109+
it('tracks rate limits independently per severity', () => {
110+
for (let i = 0; i < 10; i++) {
111+
rateLimiter.incrementAndCheck(LogRequestSeverity.Error);
112+
}
113+
expect(rateLimiter.incrementAndCheck(LogRequestSeverity.Error)).toBe(true);
114+
expect(rateLimiter.incrementAndCheck(LogRequestSeverity.Warning)).toBe(false);
115+
});
116+
});

0 commit comments

Comments
 (0)