Skip to content

Commit 064d058

Browse files
mattbodleclaude
andcommitted
feat: implement Rokt reporting service in kit
Port of efb8905 from main into TypeScript. Adds ErrorReportingService, LoggingService, ReportingTransport, and RateLimiter classes with full test coverage (42 new tests). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 4035c80 commit 064d058

2 files changed

Lines changed: 702 additions & 1 deletion

File tree

src/Rokt-Kit.ts

Lines changed: 258 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ interface RoktKitSettings {
2323
placementEventAttributeMapping?: string;
2424
hashedEmailUserIdentityType?: string;
2525
onboardingExpProvider?: string;
26+
loggingUrl?: string;
27+
errorUrl?: string;
28+
isLoggingEnabled?: string | boolean;
2629
}
2730

2831
interface EventAttributeCondition {
@@ -123,6 +126,8 @@ interface MParticleExtended {
123126
captureTiming?(metricName: string): void;
124127
forwarder?: RoktKit;
125128
loggedEvents?: Array<Record<string, unknown>>;
129+
_registerErrorReportingService?(service: ErrorReportingService): void;
130+
_registerLoggingService?(service: LoggingService): void;
126131
}
127132

128133
interface TestHelpers {
@@ -136,6 +141,12 @@ interface TestHelpers {
136141
createAutoRemovedIframe: (src: string) => void;
137142
djb2: (str: string) => number;
138143
setAllowedOriginHashes: (hashes: number[]) => void;
144+
ReportingTransport: typeof ReportingTransport;
145+
ErrorReportingService: typeof ErrorReportingService;
146+
LoggingService: typeof LoggingService;
147+
RateLimiter: typeof RateLimiter;
148+
ErrorCodes: typeof ErrorCodes;
149+
WSDKErrorSeverity: typeof WSDKErrorSeverity;
139150
}
140151

141152
interface ForwarderRegistration {
@@ -152,11 +163,30 @@ interface MParticleEvent {
152163
[key: string]: unknown;
153164
}
154165

166+
interface ReportingConfig {
167+
loggingUrl?: string;
168+
errorUrl?: string;
169+
isLoggingEnabled?: boolean | string;
170+
}
171+
172+
interface ErrorReport {
173+
message: string;
174+
code?: string;
175+
severity?: string;
176+
stackTrace?: string;
177+
}
178+
179+
interface LogEntry {
180+
message: string;
181+
code?: string;
182+
}
183+
155184
declare global {
156185
interface Window {
157186
Rokt?: RoktGlobal;
158187
__rokt_li_guid__?: string;
159188
optimizely?: OptimizelyGlobal;
189+
ROKT_DOMAIN?: string;
160190
// mParticle is declared as any to avoid conflicts with @mparticle/web-sdk type declarations.
161191
// We use the typed mp() accessor for all internal accesses.
162192
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -175,6 +205,26 @@ const ADBLOCK_CONTROL_DOMAIN = 'apps.roktecommerce.com';
175205
const INIT_LOG_SAMPLING_RATE = 0.1;
176206
const MESSAGE_TYPE_PROFILE = 14; // mParticle MessageType.Profile
177207

208+
// ============================================================
209+
// Reporting service constants
210+
// ============================================================
211+
212+
const ErrorCodes = {
213+
UNKNOWN_ERROR: 'UNKNOWN_ERROR',
214+
UNHANDLED_EXCEPTION: 'UNHANDLED_EXCEPTION',
215+
IDENTITY_REQUEST: 'IDENTITY_REQUEST',
216+
} as const;
217+
218+
const WSDKErrorSeverity = {
219+
ERROR: 'ERROR',
220+
INFO: 'INFO',
221+
WARNING: 'WARNING',
222+
} as const;
223+
224+
const DEFAULT_LOGGING_URL = 'apps.rokt-api.com/v1/log';
225+
const DEFAULT_ERROR_URL = 'apps.rokt-api.com/v1/errors';
226+
const RATE_LIMIT_PER_SEVERITY = 10;
227+
178228
// ============================================================
179229
// Helper: typed accessor for window.mParticle
180230
// We use an explicit cast here to avoid conflicts with @mparticle/web-sdk
@@ -359,6 +409,177 @@ function sendAdBlockMeasurementSignals(domain: string | undefined, version: stri
359409
);
360410
}
361411

412+
// ============================================================
413+
// Reporting helpers
414+
// ============================================================
415+
416+
function _isRoktDomainPresent(): boolean {
417+
return typeof window !== 'undefined' && Boolean(window.ROKT_DOMAIN);
418+
}
419+
420+
function _isDebugModeEnabled(): boolean {
421+
return typeof window !== 'undefined' && !!window.location?.search?.toLowerCase().includes('mp_enable_logging=true');
422+
}
423+
424+
function _getReportingUrl(): string | undefined {
425+
return typeof window !== 'undefined' ? window.location?.href : undefined;
426+
}
427+
428+
function _getUserAgent(): string | undefined {
429+
return typeof window !== 'undefined' ? window.navigator?.userAgent : undefined;
430+
}
431+
432+
class RateLimiter {
433+
private _logCount: Record<string, number> = {};
434+
435+
incrementAndCheck(severity: string): boolean {
436+
const count = this._logCount[severity] || 0;
437+
const newCount = count + 1;
438+
this._logCount[severity] = newCount;
439+
return newCount > RATE_LIMIT_PER_SEVERITY;
440+
}
441+
}
442+
443+
class ReportingTransport {
444+
private _isEnabled: boolean;
445+
private _integrationName: string;
446+
private _launcherInstanceGuid: string | undefined;
447+
private _accountId: string | null;
448+
private _rateLimiter: RateLimiter;
449+
private readonly _reporter = 'mp-wsdk';
450+
451+
constructor(
452+
config: ReportingConfig,
453+
integrationName: string | null | undefined,
454+
launcherInstanceGuid: string | undefined,
455+
accountId: string | null | undefined,
456+
rateLimiter?: RateLimiter,
457+
) {
458+
const isLoggingEnabled = config?.isLoggingEnabled === true || config?.isLoggingEnabled === 'true';
459+
this._integrationName = integrationName || '';
460+
this._launcherInstanceGuid = launcherInstanceGuid;
461+
this._accountId = accountId || null;
462+
this._rateLimiter = rateLimiter || new RateLimiter();
463+
this._isEnabled = _isDebugModeEnabled() || (_isRoktDomainPresent() && isLoggingEnabled);
464+
}
465+
466+
send(
467+
url: string,
468+
severity: string,
469+
msg: string,
470+
code?: string,
471+
stackTrace?: string,
472+
onError?: (error: Error) => void,
473+
): void {
474+
if (!this._isEnabled || this._rateLimiter.incrementAndCheck(severity)) {
475+
return;
476+
}
477+
478+
try {
479+
const logRequest = {
480+
additionalInformation: {
481+
message: msg,
482+
version: this._integrationName,
483+
},
484+
severity,
485+
code: code || ErrorCodes.UNKNOWN_ERROR,
486+
url: _getReportingUrl(),
487+
deviceInfo: _getUserAgent(),
488+
stackTrace,
489+
reporter: this._reporter,
490+
integration: this._integrationName,
491+
};
492+
493+
const headers: Record<string, string> = {
494+
Accept: 'text/plain;charset=UTF-8',
495+
'Content-Type': 'application/json',
496+
'rokt-launcher-version': this._integrationName,
497+
'rokt-wsdk-version': 'joint',
498+
};
499+
500+
if (this._launcherInstanceGuid) {
501+
headers['rokt-launcher-instance-guid'] = this._launcherInstanceGuid;
502+
}
503+
if (this._accountId) {
504+
headers['rokt-account-id'] = this._accountId;
505+
}
506+
507+
fetch(url, {
508+
method: 'POST',
509+
headers,
510+
body: JSON.stringify(logRequest),
511+
}).catch((error: Error) => {
512+
console.error('ReportingTransport: Failed to send log', error);
513+
if (onError) onError(error);
514+
});
515+
} catch (error) {
516+
console.error('ReportingTransport: Failed to send log', error);
517+
if (onError) onError(error as Error);
518+
}
519+
}
520+
}
521+
522+
class ErrorReportingService {
523+
private _transport: ReportingTransport;
524+
private _errorUrl: string;
525+
526+
constructor(
527+
config: ReportingConfig,
528+
integrationName: string | null | undefined,
529+
launcherInstanceGuid?: string,
530+
accountId?: string | null,
531+
rateLimiter?: RateLimiter,
532+
) {
533+
this._transport = new ReportingTransport(config, integrationName, launcherInstanceGuid, accountId, rateLimiter);
534+
this._errorUrl = 'https://' + (config?.errorUrl || DEFAULT_ERROR_URL);
535+
}
536+
537+
report(error: ErrorReport | null | undefined): void {
538+
if (!error) return;
539+
const severity = error.severity || WSDKErrorSeverity.ERROR;
540+
this._transport.send(this._errorUrl, severity, error.message, error.code, error.stackTrace);
541+
}
542+
}
543+
544+
class LoggingService {
545+
private _transport: ReportingTransport;
546+
private _loggingUrl: string;
547+
private _errorReportingService: { report: (e: ErrorReport) => void };
548+
549+
constructor(
550+
config: ReportingConfig,
551+
errorReportingService: { report: (e: ErrorReport) => void },
552+
integrationName: string | null | undefined,
553+
launcherInstanceGuid?: string,
554+
accountId?: string | null,
555+
rateLimiter?: RateLimiter,
556+
) {
557+
this._transport = new ReportingTransport(config, integrationName, launcherInstanceGuid, accountId, rateLimiter);
558+
this._loggingUrl = 'https://' + (config?.loggingUrl || DEFAULT_LOGGING_URL);
559+
this._errorReportingService = errorReportingService;
560+
}
561+
562+
log(entry: LogEntry | null | undefined): void {
563+
if (!entry) return;
564+
this._transport.send(
565+
this._loggingUrl,
566+
WSDKErrorSeverity.INFO,
567+
entry.message,
568+
entry.code,
569+
undefined,
570+
(error: Error) => {
571+
if (this._errorReportingService) {
572+
this._errorReportingService.report({
573+
message: 'LoggingService: Failed to send log: ' + error.message,
574+
code: ErrorCodes.UNKNOWN_ERROR,
575+
severity: WSDKErrorSeverity.ERROR,
576+
});
577+
}
578+
},
579+
);
580+
}
581+
}
582+
362583
// ============================================================
363584
// RoktKit class
364585
// ============================================================
@@ -387,6 +608,8 @@ class RoktKit {
387608
public eventStreamQueue: MParticleEvent[] = [];
388609
public integrationName: string | null = null;
389610
public domain?: string;
611+
public errorReportingService: ErrorReportingService | null = null;
612+
public loggingService: LoggingService | null = null;
390613

391614
// Private fields
392615
private _mappedEmailSha256Key?: string;
@@ -738,6 +961,35 @@ class RoktKit {
738961

739962
this.domain = domain;
740963

964+
const reportingConfig: ReportingConfig = {
965+
loggingUrl: settings.loggingUrl,
966+
errorUrl: settings.errorUrl,
967+
isLoggingEnabled: settings.isLoggingEnabled === 'true' || settings.isLoggingEnabled === true,
968+
};
969+
const errorReportingService = new ErrorReportingService(
970+
reportingConfig,
971+
this.integrationName,
972+
window.__rokt_li_guid__,
973+
settings.accountId,
974+
);
975+
const loggingService = new LoggingService(
976+
reportingConfig,
977+
errorReportingService,
978+
this.integrationName,
979+
window.__rokt_li_guid__,
980+
settings.accountId,
981+
);
982+
983+
this.errorReportingService = errorReportingService;
984+
this.loggingService = loggingService;
985+
986+
if (mp()._registerErrorReportingService) {
987+
mp()._registerErrorReportingService!(errorReportingService);
988+
}
989+
if (mp()._registerLoggingService) {
990+
mp()._registerLoggingService!(loggingService);
991+
}
992+
741993
if (testMode) {
742994
this.testHelpers = {
743995
generateLauncherScript: generateLauncherScript,
@@ -752,6 +1004,12 @@ class RoktKit {
7521004
setAllowedOriginHashes: (hashes: number[]) => {
7531005
RoktKit._allowedOriginHashes = hashes;
7541006
},
1007+
ReportingTransport: ReportingTransport,
1008+
ErrorReportingService: ErrorReportingService,
1009+
LoggingService: LoggingService,
1010+
RateLimiter: RateLimiter,
1011+
ErrorCodes: ErrorCodes,
1012+
WSDKErrorSeverity: WSDKErrorSeverity,
7551013
};
7561014
this.attachLauncher(accountId, launcherOptions);
7571015
return;

0 commit comments

Comments
 (0)