Skip to content
Merged
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
40 changes: 31 additions & 9 deletions src/Rokt-Kit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,13 @@ interface ErrorReport {
stackTrace?: string;
}

// A log-delivery failure. statusCode is set when the request reached the server
// and returned a non-2xx status (server-side); it is absent for network-level
// failures such as ad-blockers, offline, or CORS rejections (client-side).
interface DeliveryError extends Error {
statusCode?: number;
}

interface LogEntry {
message: string;
code?: string;
Expand Down Expand Up @@ -250,6 +257,7 @@ const ErrorCodes = {
UNKNOWN_ERROR: 'UNKNOWN_ERROR',
UNHANDLED_EXCEPTION: 'UNHANDLED_EXCEPTION',
IDENTITY_REQUEST: 'IDENTITY_REQUEST',
LOG_DELIVERY_FAILURE: 'LOG_DELIVERY_FAILURE',
} as const;

const WSDKErrorSeverity = {
Expand Down Expand Up @@ -555,7 +563,7 @@ class ReportingTransport {
msg: string,
code?: string,
stackTrace?: string,
onError?: (error: Error) => void,
onError?: (error: DeliveryError) => void,
): void {
if (!this._isEnabled || this._rateLimiter.incrementAndCheck(severity)) {
return;
Expand Down Expand Up @@ -594,13 +602,23 @@ class ReportingTransport {
method: 'POST',
headers,
body: JSON.stringify(logRequest),
}).catch((error: Error) => {
console.error('ReportingTransport: Failed to send log', error);
if (onError) onError(error);
});
})
.then((response: Response) => {
// fetch only rejects on network failures; an HTTP 5xx resolves with
// ok === false. Surface server-side failures so they are not swallowed.
if (!response.ok) {
const serverError: DeliveryError = new Error('HTTP ' + response.status + ' from log endpoint');
serverError.statusCode = response.status;
throw serverError;
}
})
.catch((error: DeliveryError) => {
console.error('ReportingTransport: Failed to send log', error);
if (onError) onError(error);
});
} catch (error) {
console.error('ReportingTransport: Failed to send log', error);
if (onError) onError(error as Error);
if (onError) onError(error as DeliveryError);
}
}
}
Expand Down Expand Up @@ -653,12 +671,16 @@ class LoggingService {
entry.message,
entry.code,
undefined,
(error: Error) => {
(error: DeliveryError) => {
if (this._errorReportingService) {
// A failed log POST is not itself an SDK error. Network-level failures
// (ad-blockers, offline, CORS) are client-side noise and reported as a
// WARNING; only a server-side non-2xx response stays at ERROR severity.
const isServerSide = typeof error.statusCode === 'number';
this._errorReportingService.report({
message: 'LoggingService: Failed to send log: ' + error.message,
code: ErrorCodes.UNKNOWN_ERROR,
severity: WSDKErrorSeverity.ERROR,
code: ErrorCodes.LOG_DELIVERY_FAILURE,
severity: isServerSide ? WSDKErrorSeverity.ERROR : WSDKErrorSeverity.WARNING,
});
}
},
Expand Down
44 changes: 42 additions & 2 deletions test/src/tests.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6298,6 +6298,23 @@ describe('Rokt Forwarder', () => {
console.error = originalConsoleError;
});

it('should surface a server-side 5xx response that fetch does not reject', async () => {
(window as any).fetch = () => Promise.resolve({ ok: false, status: 500 });
const consoleErrors: any[][] = [];
const originalConsoleError = console.error;
console.error = (...args: any[]) => {
consoleErrors.push(args);
};

const service = new ErrorReportingServiceClass({ isLoggingEnabled: true }, '1.0.0', 'test-guid');
service.report({ message: 'test', severity: WSDKErrorSeverityConst.ERROR });

await new Promise((resolve) => setTimeout(resolve, 50));
expect(consoleErrors.length).toBeGreaterThan(0);
expect(consoleErrors[0][0]).toBe('ReportingTransport: Failed to send log');
console.error = originalConsoleError;
});

it('should not send when report is called with null', () => {
const service = new ErrorReportingServiceClass({ isLoggingEnabled: true }, '1.0.0', 'test-guid');
service.report(null);
Expand Down Expand Up @@ -6338,7 +6355,7 @@ describe('Rokt Forwarder', () => {
expect(body.additionalInformation.message).toBe('log entry');
});

it('should report failure through ErrorReportingService on fetch error', async () => {
it('should report a network failure as a WARNING-level LOG_DELIVERY_FAILURE', async () => {
const errorReports: any[] = [];
const errorService = {
report: (error: any) => {
Expand All @@ -6354,11 +6371,34 @@ describe('Rokt Forwarder', () => {

await new Promise((resolve) => setTimeout(resolve, 50));
expect(errorReports.length).toBeGreaterThan(0);
expect(errorReports[0].severity).toBe('ERROR');
expect(errorReports[0].severity).toBe('WARNING');
expect(errorReports[0].code).toBe('LOG_DELIVERY_FAILURE');
expect(errorReports[0].message).toContain('Failed to send log');
console.error = originalConsoleError;
});

it('should report a server-side 5xx as an ERROR-level LOG_DELIVERY_FAILURE', async () => {
const errorReports: any[] = [];
const errorService = {
report: (error: any) => {
errorReports.push(error);
},
};
(window as any).fetch = () => Promise.resolve({ ok: false, status: 503 });
const originalConsoleError = console.error;
console.error = () => {};

const service = new LoggingServiceClass({ isLoggingEnabled: true }, errorService, '1.0.0', 'test-guid');
service.log({ message: 'test' });

await new Promise((resolve) => setTimeout(resolve, 50));
expect(errorReports.length).toBeGreaterThan(0);
expect(errorReports[0].severity).toBe('ERROR');
expect(errorReports[0].code).toBe('LOG_DELIVERY_FAILURE');
expect(errorReports[0].message).toContain('503');
console.error = originalConsoleError;
});

it('should not send when log is called with null', () => {
const errorService = new ErrorReportingServiceClass({ isLoggingEnabled: true }, '1.0.0', 'test-guid');
const service = new LoggingServiceClass({ isLoggingEnabled: true }, errorService, '1.0.0', 'test-guid');
Expand Down
Loading