diff --git a/src/Rokt-Kit.ts b/src/Rokt-Kit.ts index 5f91127..c161e71 100644 --- a/src/Rokt-Kit.ts +++ b/src/Rokt-Kit.ts @@ -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; @@ -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 = { @@ -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; @@ -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); } } } @@ -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, }); } }, diff --git a/test/src/tests.spec.ts b/test/src/tests.spec.ts index 8fc54b8..8406143 100644 --- a/test/src/tests.spec.ts +++ b/test/src/tests.spec.ts @@ -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); @@ -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) => { @@ -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');