Skip to content

Commit ae5b87f

Browse files
feat: Create logging service (#1137)
1 parent 6ecd6e0 commit ae5b87f

14 files changed

Lines changed: 1050 additions & 41 deletions

src/constants.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,8 @@ const Constants = {
139139
identityUrl: 'identity.mparticle.com/v1/',
140140
aliasUrl: 'jssdks.mparticle.com/v1/identity/',
141141
userAudienceUrl: 'nativesdks.mparticle.com/v1/',
142+
loggingUrl: 'apps.rokt-api.com/v1/log',
143+
errorUrl: 'apps.rokt-api.com/v1/errors',
142144
},
143145
// These are the paths that are used to construct the CNAME urls
144146
CNAMEUrlPaths: {
@@ -148,6 +150,8 @@ const Constants = {
148150
configUrl: '/tags/JS/v2/',
149151
identityUrl: '/identity/v1/',
150152
aliasUrl: '/webevents/v1/identity/',
153+
loggingUrl: '/v1/log',
154+
errorUrl: '/v1/errors',
151155
},
152156
Base64CookieKeys: {
153157
csm: 1,

src/identityApiClient.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import {
2525
IIdentityResponse,
2626
} from './identity-user-interfaces';
2727
import { IMParticleWebSDKInstance } from './mp-instance';
28+
import { ErrorCodes } from './logging/types';
2829

2930
const { HTTPCodes, Messages, IdentityMethods } = Constants;
3031

@@ -326,10 +327,13 @@ export default function IdentityAPIClient(
326327
const requestCount = mpInstance._Store.identifyRequestCount;
327328
mpInstance.captureTiming(`${requestCount}-identityRequestEnd`);
328329
}
329-
330+
330331
const errorMessage = (err as Error).message || err.toString();
331-
Logger.error('Error sending identity request to servers - ' + errorMessage);
332-
332+
Logger.error(
333+
'Error sending identity request to servers' + ' - ' + errorMessage,
334+
ErrorCodes.IDENTITY_REQUEST
335+
);
336+
333337
mpInstance.processQueueOnIdentityFailure?.();
334338
invokeCallback(callback, HTTPCodes.noHttpCoverage, errorMessage);
335339
}

src/logger.ts

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,21 @@
11
import { LogLevelType, SDKInitConfig, SDKLoggerApi } from './sdkRuntimeModels';
2+
import { ReportingLogger } from './logging/reportingLogger';
3+
import { ErrorCodes } from './logging/types';
24

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

68
export class Logger {
79
private logLevel: LogLevelType;
810
private logger: IConsoleLogger;
11+
private reportingLogger: ReportingLogger;
912

10-
constructor(config: ILoggerConfig) {
13+
constructor(config: ILoggerConfig,
14+
reportingLogger?: ReportingLogger,
15+
) {
1116
this.logLevel = config.logLevel ?? LogLevelType.Warning;
1217
this.logger = config.logger ?? new ConsoleLogger();
18+
this.reportingLogger = reportingLogger;
1319
}
1420

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

2430
public warning(msg: string): void {
25-
if(this.logLevel === LogLevelType.None)
31+
if(this.logLevel === LogLevelType.None)
2632
return;
2733

28-
if (this.logger.warning &&
34+
if (this.logger.warning &&
2935
(this.logLevel === LogLevelType.Verbose || this.logLevel === LogLevelType.Warning)) {
3036
this.logger.warning(msg);
3137
}
3238
}
3339

34-
public error(msg: string): void {
35-
if(this.logLevel === LogLevelType.None)
40+
public error(msg: string, codeForReporting?: ErrorCodes): void {
41+
if(this.logLevel === LogLevelType.None)
3642
return;
3743

3844
if (this.logger.error) {
3945
this.logger.error(msg);
46+
if (codeForReporting) {
47+
this.reportingLogger?.error(msg, codeForReporting);
48+
}
4049
}
4150
}
4251

src/logging/reportingLogger.ts

Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
import { ErrorCodes, LogRequestBody, WSDKErrorSeverity } from "./types";
2+
import { FetchUploader, IFetchPayload } from "../uploaders";
3+
import { IStore, SDKConfig } from "../store";
4+
import { SDKInitConfig } from "../sdkRuntimeModels";
5+
import Constants from "../constants";
6+
7+
// Header key constants
8+
const HEADER_ACCEPT = 'Accept' as const;
9+
const HEADER_CONTENT_TYPE = 'Content-Type' as const;
10+
const HEADER_ROKT_LAUNCHER_VERSION = 'rokt-launcher-version' as const;
11+
const HEADER_ROKT_LAUNCHER_INSTANCE_GUID = 'rokt-launcher-instance-guid' as const;
12+
const HEADER_ROKT_WSDK_VERSION = 'rokt-wsdk-version' as const;
13+
14+
interface IReportingLoggerPayload extends IFetchPayload {
15+
headers: IFetchPayload['headers'] & {
16+
[HEADER_ROKT_LAUNCHER_INSTANCE_GUID]?: string;
17+
[HEADER_ROKT_LAUNCHER_VERSION]: string;
18+
[HEADER_ROKT_WSDK_VERSION]: string;
19+
};
20+
body: string;
21+
}
22+
23+
export class ReportingLogger {
24+
private readonly isEnabled: boolean;
25+
private readonly reporter: string = 'mp-wsdk';
26+
private readonly rateLimiter: IRateLimiter;
27+
private store: IStore | null;
28+
private readonly loggingUrl: string;
29+
private readonly errorUrl: string;
30+
private readonly isLoggingEnabled: boolean;
31+
32+
constructor(
33+
config: SDKConfig | SDKInitConfig | any,
34+
private readonly sdkVersion: string,
35+
store?: IStore,
36+
private readonly launcherInstanceGuid?: string,
37+
rateLimiter?: IRateLimiter,
38+
) {
39+
this.loggingUrl = `https://${config.loggingUrl || Constants.DefaultBaseUrls.loggingUrl}`;
40+
this.errorUrl = `https://${config.errorUrl || Constants.DefaultBaseUrls.errorUrl}`;
41+
this.isLoggingEnabled = config.isLoggingEnabled || false;
42+
this.store = store ?? null;
43+
this.isEnabled = this.isReportingEnabled();
44+
this.rateLimiter = rateLimiter ?? new RateLimiter();
45+
}
46+
47+
public setStore(store: IStore): void {
48+
this.store = store;
49+
}
50+
51+
public info(msg: string, code?: ErrorCodes) {
52+
this.sendLog(WSDKErrorSeverity.INFO, msg, code);
53+
}
54+
55+
public error(msg: string, code?: ErrorCodes, stackTrace?: string) {
56+
this.sendError(WSDKErrorSeverity.ERROR, msg, code, stackTrace);
57+
}
58+
59+
public warning(msg: string, code?: ErrorCodes) {
60+
this.sendError(WSDKErrorSeverity.WARNING, msg, code);
61+
}
62+
63+
private sendToServer(url: string, severity: WSDKErrorSeverity, msg: string, code?: ErrorCodes, stackTrace?: string): void {
64+
if (!this.canSendLog(severity))
65+
return;
66+
67+
try {
68+
const logRequest = this.buildLogRequest(severity, msg, code, stackTrace);
69+
const uploader = new FetchUploader(url);
70+
const payload: IReportingLoggerPayload = {
71+
method: 'POST',
72+
headers: this.getHeaders(),
73+
body: JSON.stringify(logRequest),
74+
};
75+
uploader.upload(payload).catch((error) => {
76+
console.error('ReportingLogger: Failed to send log', error);
77+
});
78+
} catch (error) {
79+
console.error('ReportingLogger: Failed to send log', error);
80+
}
81+
}
82+
83+
private sendLog(severity: WSDKErrorSeverity, msg: string, code?: ErrorCodes, stackTrace?: string): void {
84+
this.sendToServer(this.loggingUrl, severity, msg, code, stackTrace);
85+
}
86+
87+
private sendError(severity: WSDKErrorSeverity, msg: string, code?: ErrorCodes, stackTrace?: string): void {
88+
this.sendToServer(this.errorUrl, severity, msg, code, stackTrace);
89+
}
90+
91+
private buildLogRequest(severity: WSDKErrorSeverity, msg: string, code?: ErrorCodes, stackTrace?: string): LogRequestBody {
92+
return {
93+
additionalInformation: {
94+
message: msg,
95+
version: this.getVersion(),
96+
},
97+
severity: severity,
98+
code: code ?? ErrorCodes.UNKNOWN_ERROR,
99+
url: this.getUrl(),
100+
deviceInfo: this.getUserAgent(),
101+
stackTrace: stackTrace,
102+
reporter: this.reporter,
103+
// Integration will be set to integrationName once the kit connects via RoktManager.attachKit()
104+
integration: this.store?.getIntegrationName() ?? 'mp-wsdk'
105+
};
106+
}
107+
108+
private getVersion(): string {
109+
return this.store?.getIntegrationName?.() ?? `mParticle_wsdkv_${this.sdkVersion}`;
110+
}
111+
112+
private isReportingEnabled(): boolean {
113+
return this.isDebugModeEnabled() ||
114+
(this.isRoktDomainPresent() && this.isFeatureFlagEnabled());
115+
}
116+
117+
private isRoktDomainPresent(): boolean {
118+
return typeof window !== 'undefined' && Boolean(window['ROKT_DOMAIN']);
119+
}
120+
121+
private isFeatureFlagEnabled = (): boolean => this.isLoggingEnabled;
122+
123+
private isDebugModeEnabled(): boolean {
124+
return (
125+
typeof window !== 'undefined' &&
126+
(window.
127+
location?.
128+
search?.
129+
toLowerCase()?.
130+
includes('mp_enable_logging=true') ?? false)
131+
);
132+
}
133+
134+
private canSendLog(severity: WSDKErrorSeverity): boolean {
135+
return this.isEnabled && !this.isRateLimited(severity);
136+
}
137+
138+
private isRateLimited(severity: WSDKErrorSeverity): boolean {
139+
return this.rateLimiter.incrementAndCheck(severity);
140+
}
141+
142+
private getUrl(): string | undefined {
143+
return typeof window !== 'undefined' ? window.location?.href : undefined;
144+
}
145+
146+
private getUserAgent(): string | undefined {
147+
return typeof window !== 'undefined' ? window.navigator?.userAgent : undefined;
148+
}
149+
150+
private getHeaders(): IReportingLoggerPayload['headers'] {
151+
const headers: IReportingLoggerPayload['headers'] = {
152+
[HEADER_ACCEPT]: 'text/plain;charset=UTF-8',
153+
[HEADER_CONTENT_TYPE]: 'application/json',
154+
[HEADER_ROKT_LAUNCHER_VERSION]: this.getVersion(),
155+
[HEADER_ROKT_WSDK_VERSION]: 'joint',
156+
};
157+
158+
if (this.launcherInstanceGuid) {
159+
headers[HEADER_ROKT_LAUNCHER_INSTANCE_GUID] = this.launcherInstanceGuid;
160+
}
161+
162+
const accountId = this.store?.getRoktAccountId?.();
163+
if (accountId) {
164+
headers['rokt-account-id'] = accountId;
165+
}
166+
167+
return headers;
168+
}
169+
}
170+
171+
export interface IRateLimiter {
172+
incrementAndCheck(severity: WSDKErrorSeverity): boolean;
173+
}
174+
175+
export class RateLimiter implements IRateLimiter {
176+
private readonly rateLimits: Map<WSDKErrorSeverity, number> = new Map([
177+
[WSDKErrorSeverity.ERROR, 10],
178+
[WSDKErrorSeverity.WARNING, 10],
179+
[WSDKErrorSeverity.INFO, 10],
180+
]);
181+
private logCount: Map<WSDKErrorSeverity, number> = new Map();
182+
183+
public incrementAndCheck(severity: WSDKErrorSeverity): boolean {
184+
const count = this.logCount.get(severity) || 0;
185+
const limit = this.rateLimits.get(severity) || 10;
186+
187+
const newCount = count + 1;
188+
this.logCount.set(severity, newCount);
189+
190+
return newCount > limit;
191+
}
192+
}

src/logging/types.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { valueof } from '../utils';
2+
3+
export const ErrorCodes = {
4+
UNKNOWN_ERROR: 'UNKNOWN_ERROR',
5+
UNHANDLED_EXCEPTION: 'UNHANDLED_EXCEPTION',
6+
IDENTITY_REQUEST: 'IDENTITY_REQUEST',
7+
} as const;
8+
9+
export type ErrorCodes = valueof<typeof ErrorCodes>;
10+
11+
export type ErrorCode = ErrorCodes | string;
12+
13+
export const WSDKErrorSeverity = {
14+
ERROR: 'ERROR',
15+
INFO: 'INFO',
16+
WARNING: 'WARNING',
17+
} as const;
18+
19+
export type WSDKErrorSeverity = (typeof WSDKErrorSeverity)[keyof typeof WSDKErrorSeverity];
20+
21+
export type ErrorsRequestBody = {
22+
additionalInformation?: Record<string, string>;
23+
code: ErrorCode;
24+
severity: WSDKErrorSeverity;
25+
stackTrace?: string;
26+
deviceInfo?: string;
27+
integration?: string;
28+
reporter?: string;
29+
url?: string;
30+
};
31+
32+
export type LogRequestBody = ErrorsRequestBody;

0 commit comments

Comments
 (0)