/
githubmirror
/
ydb-embedded-ui
Обзор
Документация
Войти
/
githubmirror
/
ydb-embedded-ui
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
src/utils/errors/__test__/extractErrorDetails.test.ts
899 строк
32 KB
Anton Standrik
fix: monitoring error message looks really bad (#3850)
29 апр 2026, 17:46
Не верифицирован
29 апр 2026, 17:46
dac73d5
Код
Авторство
О чём код?
import {extractErrorDetails} from '../extractErrorDetails'; describe('extractErrorDetails', () => { test('should return null for null/undefined/string/number', () => { expect(extractErrorDetails(null)).toBeNull(); expect(extractErrorDetails(undefined)).toBeNull(); expect(extractErrorDetails('some error')).toBeNull(); expect(extractErrorDetails(42)).toBeNull(); }); test('should return null for cancelled requests', () => { const error = {isCancelled: true}; expect(extractErrorDetails(error)).toBeNull(); }); test('should return null for objects without useful metadata', () => { const error = {message: 'Network Error'}; expect(extractErrorDetails(error)).toBeNull(); }); test('should extract status and statusText from HTTP error', () => { const error = {status: 500, statusText: 'Internal Server Error', data: 'error'}; const details = extractErrorDetails(error); expect(details).toEqual( expect.objectContaining({ status: 500, statusText: 'Internal Server Error', }), ); }); test('should extract traceId from traceresponse header', () => { const error = { status: 400, statusText: 'Bad Request', data: 'Cluster not found', headers: { traceresponse: '00-b3d9fbfed1452b574dc654b004ed5b6f-474a6a958ff472fb-00', 'x-request-id': 'b401739a-019b-41a8-8eaf-e168faa8b13b', }, config: { url: '/api/meta3/proxy/cluster/ydb-meta/viewer/capabilities', method: 'get', }, }; const details = extractErrorDetails(error); expect(details).toEqual( expect.objectContaining({ status: 400, statusText: 'Bad Request', traceId: 'b3d9fbfed1452b574dc654b004ed5b6f', requestId: 'b401739a-019b-41a8-8eaf-e168faa8b13b', requestUrl: '/api/meta3/proxy/cluster/ydb-meta/viewer/capabilities', method: 'GET', }), ); }); test('should extract x-trace-id when traceresponse is absent', () => { const error = { status: 401, statusText: 'Unauthorized', headers: { 'x-trace-id': '26d0e0da1cafea7ff0bf3395cffcf647', 'x-request-id': '7e8c3947-0b84-46b9-9052-0a7d7047f5af', }, }; const details = extractErrorDetails(error); expect(details).toEqual( expect.objectContaining({ traceId: '26d0e0da1cafea7ff0bf3395cffcf647', requestId: '7e8c3947-0b84-46b9-9052-0a7d7047f5af', }), ); }); test('should prefer traceresponse over x-trace-id', () => { const error = { status: 400, headers: { traceresponse: '00-aaaa-bbbb-00', 'x-trace-id': 'cccc', }, }; const details = extractErrorDetails(error); expect(details?.traceId).toBe('aaaa'); }); test('should fall back to x-trace-id when traceresponse is malformed', () => { const error = { status: 400, headers: { traceresponse: '00-', 'x-trace-id': 'fallback-trace-id', }, }; const details = extractErrorDetails(error); expect(details?.traceId).toBe('fallback-trace-id'); }); test('should extract x-proxy-name and x-worker-name', () => { const error = { status: 429, statusText: 'Too Many Requests', headers: { 'x-proxy-name': 'https://proxy-preprod.example.test:443', 'x-worker-name': 'proxy-worker-preprod.example.test:8765', 'x-request-id': 'ba767b14-d97b-45e6-889b-4bcccf5c9e46', }, }; const details = extractErrorDetails(error); expect(details).toEqual( expect.objectContaining({ proxyName: 'https://proxy-preprod.example.test:443', workerName: 'proxy-worker-preprod.example.test:8765', requestId: 'ba767b14-d97b-45e6-889b-4bcccf5c9e46', }), ); }); test('should extract proxy diagnostics from headers', () => { const error = { status: 503, statusText: 'Service Unavailable', headers: { 'x-ydb-ui-proxy-trace-id': 'proxy-trace-id-123', 'x-ydb-ui-proxy-request-id': 'proxy-request-id-456', 'x-ydb-ui-proxy-rewritten-path': '/meta/cp_databases?cluster_name=test', 'x-ydb-ui-proxy-target': 'http://meta-upstream.example.test:8780/', }, }; const details = extractErrorDetails(error); expect(details).toEqual( expect.objectContaining({ proxyTraceId: 'proxy-trace-id-123', proxyRequestId: 'proxy-request-id-456', proxyRewrittenPath: '/meta/cp_databases?cluster_name=test', proxyTarget: 'http://meta-upstream.example.test:8780/', }), ); }); test('should extract requestUrl and method from config', () => { const error = { status: 503, statusText: 'Service Unavailable', data: '', config: { url: '/node/559390/viewer/json/whoami', method: 'options', }, }; const details = extractErrorDetails(error); expect(details).toEqual( expect.objectContaining({ requestUrl: '/node/559390/viewer/json/whoami', method: 'OPTIONS', }), ); }); test('should extract errorCode and config from network error', () => { const error = { message: 'Network Error', name: 'AxiosError', code: 'ERR_NETWORK', config: { url: '/viewer/json/cluster?tablets=true', method: 'get', }, status: null, }; const details = extractErrorDetails(error); expect(details).toEqual( expect.objectContaining({ errorCode: 'ERR_NETWORK', requestUrl: '/viewer/json/cluster?tablets=true', method: 'GET', }), ); expect(details?.status).toBeUndefined(); }); test('should extract nested response details from hybrid network error', () => { const error = Object.assign(new Error('Network Error'), { name: 'AxiosError', code: 'ERR_NETWORK', config: { url: '/api/meta3/proxy/cluster/test-cluster/viewer/json/whoami?database=test-db', method: 'get', }, response: { status: 404, statusText: 'Not Found', data: '', headers: { traceresponse: '00-11112222333344445555666677778888-9999aaaabbbbcccc-00', 'x-request-id': 'test-request-id-404-hybrid', 'x-proxy-name': 'https://test-proxy-node.example.test:443', 'x-trace-id': '11112222333344445555666677778888', }, }, }); const details = extractErrorDetails(error); expect(details).toEqual( expect.objectContaining({ status: 404, statusText: 'Not Found', title: '404 Not Found', errorCode: 'ERR_NETWORK', traceId: '11112222333344445555666677778888', requestId: 'test-request-id-404-hybrid', proxyName: 'https://test-proxy-node.example.test:443', requestUrl: '/api/meta3/proxy/cluster/test-cluster/viewer/json/whoami?database=test-db', method: 'GET', }), ); }); test('should extract errorCode for timeout error', () => { const error = { message: 'timeout of 600000ms exceeded', code: 'ETIMEDOUT', config: { url: '/viewer/json/query', method: 'post', }, }; const details = extractErrorDetails(error); expect(details).toEqual( expect.objectContaining({ errorCode: 'ETIMEDOUT', requestUrl: '/viewer/json/query', method: 'POST', }), ); }); test('should detect hasIssues for HTTP 429 with issues', () => { const error = { status: 429, statusText: 'Too Many Requests', data: { issues: [{issue_code: 200803, severity: 1, message: 'Throughput limit exceeded'}], error: {issue_code: 200803, severity: 1, message: 'Throughput limit exceeded'}, status: 'OVERLOADED', }, }; const details = extractErrorDetails(error); expect(details?.hasIssues).toBe(true); expect(details?.issues).toHaveLength(1); expect(details?.issues?.[0].message).toBe('Throughput limit exceeded'); }); test('should not set hasIssues for responses without issues', () => { const error = {status: 500, statusText: 'Server Error', data: 'Internal error'}; const details = extractErrorDetails(error); expect(details?.hasIssues).toBeUndefined(); }); test('should extract responseBody from string data', () => { const error = {status: 400, data: 'Cluster not found'}; const details = extractErrorDetails(error); expect(details?.responseBody).toBe('Cluster not found'); }); test('should extract responseBody from object data as JSON', () => { const error = {status: 500, data: {error: 'Internal server error'}}; const details = extractErrorDetails(error); expect(details?.responseBody).toBe('{\n "error": "Internal server error"\n}'); }); test('should extract gateway-shaped top-level error details', () => { const error = { status: 404, message: 'dashboard ewfewfwefewfewff not found', code: 'GATEWAY_REQUEST_ERROR', details: { title: 'Error', description: 'dashboard wefewfewfewfewf not found', grpcCode: 5, }, }; const details = extractErrorDetails(error); expect(details).toEqual( expect.objectContaining({ status: 404, title: '404', dataMessage: 'dashboard ewfewfwefewfewff not found', errorCode: 'GATEWAY_REQUEST_ERROR', grpcCode: 5, responseBody: '{\n' + ' "status": 404,\n' + ' "message": "dashboard ewfewfwefewfewff not found",\n' + ' "code": "GATEWAY_REQUEST_ERROR",\n' + ' "details": {\n' + ' "title": "Error",\n' + ' "description": "dashboard wefewfewfewfewf not found",\n' + ' "grpcCode": 5\n' + ' }\n' + '}', }), ); }); test('should not treat array details as gateway-shaped error', () => { const error = { status: 404, message: 'Not found', details: [{description: 'Unexpected shape'}], }; const details = extractErrorDetails(error); expect(details).toEqual( expect.objectContaining({ status: 404, title: '404', message: 'Not found', }), ); expect(details?.dataMessage).toBeUndefined(); expect(details?.responseBody).toBeUndefined(); expect(details?.grpcCode).toBeUndefined(); }); test('should extract gateway-shaped response data details', () => { const error = { status: 404, data: { status: 404, code: 'GATEWAY_REQUEST_ERROR', details: { title: 'Error', description: 'dashboard wefewfewfewfewf not found', grpcCode: 5, }, }, }; const details = extractErrorDetails(error); expect(details).toEqual( expect.objectContaining({ status: 404, title: '404', dataMessage: 'dashboard wefewfewfewfewf not found', errorCode: 'GATEWAY_REQUEST_ERROR', grpcCode: 5, }), ); expect(details?.responseBody).toContain('"code": "GATEWAY_REQUEST_ERROR"'); expect(details?.responseBody).toContain('"grpcCode": 5'); }); test('should not extract responseBody from empty string data', () => { const error = {status: 503, statusText: 'Service Unavailable', data: ''}; const details = extractErrorDetails(error); expect(details?.responseBody).toBeUndefined(); }); test('should not extract responseBody from empty object data', () => { const error = {status: 502, statusText: 'Bad Gateway', data: {}}; const details = extractErrorDetails(error); expect(details?.responseBody).toBeUndefined(); }); test('should truncate long responseBody', () => { const longBody = 'x'.repeat(600); const error = {status: 500, data: longBody}; const details = extractErrorDetails(error); expect(details?.responseBody?.length).toBeLessThanOrEqual(501); expect(details?.responseBody).toMatch(/…$/); }); test('should handle empty headers object', () => { const error = {status: 503, headers: {}}; const details = extractErrorDetails(error); expect(details?.traceId).toBeUndefined(); expect(details?.requestId).toBeUndefined(); expect(details?.proxyName).toBeUndefined(); expect(details?.workerName).toBeUndefined(); }); test('should set title from status and statusText for HTTP errors', () => { const error = {status: 500, statusText: 'Internal Server Error', data: 'error'}; const details = extractErrorDetails(error); expect(details?.title).toBe('500 Internal Server Error'); }); test('should set title from status alone when statusText is missing', () => { const error = {status: 403}; const details = extractErrorDetails(error); expect(details?.title).toBe('403'); }); test('should set title from message for network errors with errorCode', () => { const error = { message: 'Network Error', code: 'ERR_NETWORK', config: {url: '/viewer/json/cluster', method: 'get'}, }; const details = extractErrorDetails(error); expect(details?.title).toBe('Network Error'); }); test('should not set title for objects with only message (no errorCode)', () => { const error = {message: 'Network Error'}; expect(extractErrorDetails(error)).toBeNull(); }); test('should extract dataMessage from data.error string', () => { const error = {status: 500, data: {error: 'Internal server error'}}; const details = extractErrorDetails(error); expect(details?.dataMessage).toBe('Internal server error'); }); test('should extract dataMessage from data.error.message', () => { const error = { status: 429, statusText: 'Too Many Requests', data: { error: {issue_code: 200803, severity: 1, message: 'Throughput limit exceeded'}, issues: [{issue_code: 200803, severity: 1, message: 'Throughput limit exceeded'}], status: 'OVERLOADED', }, }; const details = extractErrorDetails(error); expect(details?.dataMessage).toBe('Throughput limit exceeded'); expect(details?.title).toBe('429 Too Many Requests'); }); test('should extract dataMessage from data.message', () => { const error = { status: 429, statusText: 'Too Many Requests', data: {message: 'Rate limit exceeded'}, }; const details = extractErrorDetails(error); expect(details?.dataMessage).toBe('Rate limit exceeded'); }); test('should extract proxy diagnostics from body', () => { const error = { status: 503, statusText: 'Service Unavailable', data: { message: 'Meta upstream unavailable', proxyDiagnostics: { traceId: 'proxy-trace-id-body', requestId: 'proxy-request-id-body', rewrittenPath: '/meta/cp_databases?cluster_name=body', target: 'http://meta-body.example.test:8780/', }, }, }; const details = extractErrorDetails(error); expect(details).toEqual( expect.objectContaining({ proxyTraceId: 'proxy-trace-id-body', proxyRequestId: 'proxy-request-id-body', proxyRewrittenPath: '/meta/cp_databases?cluster_name=body', proxyTarget: 'http://meta-body.example.test:8780/', }), ); }); test('should merge proxy diagnostics from headers and body with per-field fallback', () => { const error = { status: 503, headers: { 'x-ydb-ui-proxy-trace-id': 'proxy-trace-id-header', 'x-ydb-ui-proxy-rewritten-path': '/meta/from-header', }, data: { proxyDiagnostics: { traceId: 'proxy-trace-id-body', requestId: 'proxy-request-id-body', rewrittenPath: '/meta/from-body', target: 'http://meta-body.example.test:8780/', }, }, }; const details = extractErrorDetails(error); expect(details).toEqual( expect.objectContaining({ proxyTraceId: 'proxy-trace-id-header', proxyRequestId: 'proxy-request-id-body', proxyRewrittenPath: '/meta/from-header', proxyTarget: 'http://meta-body.example.test:8780/', }), ); }); test('should prefer proxy headers over body for the same field', () => { const error = { status: 503, headers: { 'x-ydb-ui-proxy-request-id': 'proxy-request-id-header', 'x-ydb-ui-proxy-target': 'http://meta-header.example.test:8780/', }, data: { proxyDiagnostics: { requestId: 'proxy-request-id-body', target: 'http://meta-body.example.test:8780/', }, }, }; const details = extractErrorDetails(error); expect(details?.proxyRequestId).toBe('proxy-request-id-header'); expect(details?.proxyTarget).toBe('http://meta-header.example.test:8780/'); }); test('should extract error origin and stage from headers', () => { const error = { status: 503, headers: { 'x-ydb-ui-error-origin': 'upstream', 'x-ydb-ui-error-stage': 'upstream-response', }, }; const details = extractErrorDetails(error); expect(details?.errorOrigin).toBe('upstream'); expect(details?.errorStage).toBe('upstream-response'); }); test('should extract error origin and stage from top-level body', () => { const error = { status: 503, data: { errorOrigin: 'proxy', errorStage: 'connect', }, }; const details = extractErrorDetails(error); expect(details?.errorOrigin).toBe('proxy'); expect(details?.errorStage).toBe('connect'); }); test('should extract error origin and stage from proxyDiagnostics body', () => { const error = { status: 503, data: { proxyDiagnostics: { origin: 'app', stage: 'before-proxy', }, }, }; const details = extractErrorDetails(error); expect(details?.errorOrigin).toBe('app'); expect(details?.errorStage).toBe('before-proxy'); }); test('should prefer error marker headers over body values', () => { const error = { status: 503, headers: { 'x-ydb-ui-error-origin': 'upstream', 'x-ydb-ui-error-stage': 'upstream-response', }, data: { errorOrigin: 'proxy', errorStage: 'connect', }, }; const details = extractErrorDetails(error); expect(details?.errorOrigin).toBe('upstream'); expect(details?.errorStage).toBe('upstream-response'); }); test('should ignore unknown error origin values', () => { const error = { status: 503, headers: { 'x-ydb-ui-error-origin': 'unknown-origin', 'x-ydb-ui-error-stage': 'dns', }, }; const details = extractErrorDetails(error); expect(details?.errorOrigin).toBeUndefined(); expect(details?.errorStage).toBe('dns'); }); test('should keep error origin independent from proxy diagnostics fields', () => { const error = { status: 503, headers: { 'x-ydb-ui-error-origin': 'proxy', 'x-ydb-ui-proxy-request-id': 'proxy-request-id-header', 'x-ydb-ui-proxy-target': 'http://meta-header.example.test:8780/', }, data: { proxyDiagnostics: { requestId: 'proxy-request-id-body', target: 'http://meta-body.example.test:8780/', }, }, }; const details = extractErrorDetails(error); expect(details?.errorOrigin).toBe('proxy'); expect(details?.proxyRequestId).toBe('proxy-request-id-header'); expect(details?.proxyTarget).toBe('http://meta-header.example.test:8780/'); }); test('should keep generic Request-ID and Proxy Request-ID separately', () => { const error = { status: 503, headers: { 'x-request-id': 'generic-request-id', 'x-ydb-ui-proxy-request-id': 'proxy-request-id', }, }; const details = extractErrorDetails(error); expect(details?.requestId).toBe('generic-request-id'); expect(details?.proxyRequestId).toBe('proxy-request-id'); }); test('should ignore invalid proxy diagnostics fields independently', () => { const error = { status: 503, data: { proxyDiagnostics: { traceId: 123, requestId: 'proxy-request-id-body', rewrittenPath: '/meta/cp_databases?cluster_name=body', target: false, }, }, }; const details = extractErrorDetails(error); expect(details?.proxyTraceId).toBeUndefined(); expect(details?.proxyRequestId).toBe('proxy-request-id-body'); expect(details?.proxyRewrittenPath).toBe('/meta/cp_databases?cluster_name=body'); expect(details?.proxyTarget).toBeUndefined(); }); test('should extract dataMessage from short string data', () => { const error = {status: 400, data: 'Cluster not found'}; const details = extractErrorDetails(error); expect(details?.dataMessage).toBe('Cluster not found'); expect(details?.responseBody).toBe('Cluster not found'); }); test('should not extract dataMessage from long string data', () => { const longString = 'x'.repeat(300); const error = {status: 500, data: longString}; const details = extractErrorDetails(error); expect(details?.dataMessage).toBeUndefined(); }); test('should not extract dataMessage from HTML string data', () => { const html = '<html><body><h1>429 Too Many Requests</h1></body></html>'; const error = {status: 429, statusText: 'Too Many Requests', data: html}; const details = extractErrorDetails(error); expect(details?.dataMessage).toBeUndefined(); expect(details?.title).toBe('429 Too Many Requests'); }); test('should not extract dataMessage from empty data', () => { const error = {status: 503, statusText: 'Service Unavailable', data: ''}; const details = extractErrorDetails(error); expect(details?.dataMessage).toBeUndefined(); }); test('should extract dataMessage and issues from ErrorResponse (top-level)', () => { const error = { error: {message: 'Member not found: CPUCores', severity: 1, issue_code: 2001}, issues: [ {message: 'Member not found: CPUCores', severity: 1, issue_code: 2001}, {message: 'Type annotation', severity: 2, issue_code: 1030}, ], }; const details = extractErrorDetails(error); expect(details?.dataMessage).toBe('Member not found: CPUCores'); expect(details?.hasIssues).toBe(true); expect(details?.issues).toHaveLength(2); expect(details?.issues?.[0].message).toBe('Member not found: CPUCores'); expect(details?.issues?.[1].message).toBe('Type annotation'); }); test('should handle 503 with x-worker-name from nginx', () => { const error = { status: 503, statusText: 'Service Unavailable', data: '', headers: { 'x-worker-name': 'worker-nginx.example.test:8765', server: 'nginx/1.18.0', }, config: { url: '/node/559390/viewer/json/whoami', method: 'options', }, }; const details = extractErrorDetails(error); expect(details).toEqual( expect.objectContaining({ status: 503, statusText: 'Service Unavailable', workerName: 'worker-nginx.example.test:8765', requestUrl: '/node/559390/viewer/json/whoami', method: 'OPTIONS', }), ); }); test('should extract HTTP details from recovered ERR_NETWORK response', () => { const error = { status: 504, statusText: 'Deadline Exceeded', code: 'ERR_NETWORK', message: 'Network Error', data: '', headers: { 'x-worker-name': 'oidc-proxy-1-vm-preprod.example.net', }, config: { url: 'https://oidc-proxy-preprod.example.net/viewer/json/whoami?database=%2Fdb', method: 'get', }, }; const details = extractErrorDetails(error); expect(details).toEqual( expect.objectContaining({ status: 504, statusText: 'Deadline Exceeded', title: '504 Deadline Exceeded', errorCode: 'ERR_NETWORK', workerName: 'oidc-proxy-1-vm-preprod.example.net', requestUrl: 'https://oidc-proxy-preprod.example.net/viewer/json/whoami?database=%2Fdb', method: 'GET', }), ); expect(details?.dataMessage).toBeUndefined(); expect(details?.message).toBe('Network Error'); }); test('should extract config from enriched fetch error without title', () => { const error = Object.assign(new TypeError('Failed to fetch'), { config: {url: '/viewer/query?timeout=60000', method: 'POST'}, }); const details = extractErrorDetails(error); // title is not set - no status/statusText/errorCode // useErrorInfo uses fallback from prepareCommonErrorMessage expect(details?.title).toBeUndefined(); expect(details?.requestUrl).toBe('/viewer/query?timeout=60000'); expect(details?.method).toBe('POST'); expect(details?.message).toBe('Failed to fetch'); }); test('should extract message from enriched fetch TypeError', () => { const error = Object.assign(new TypeError('Failed to fetch'), { config: {url: '/viewer/query', method: 'POST'}, }); const details = extractErrorDetails(error); expect(details?.message).toBe('Failed to fetch'); }); test('should extract message from Axios ERR_NETWORK error', () => { const error = { message: 'Network Error', code: 'ERR_NETWORK', config: {url: '/viewer/json/cluster', method: 'get'}, }; const details = extractErrorDetails(error); expect(details?.message).toBe('Network Error'); expect(details?.title).toBe('Network Error'); }); test('should not extract message when no other useful fields exist', () => { const error = {message: 'Network Error'}; expect(extractErrorDetails(error)).toBeNull(); }); test('should extract message from mid-stream parsing error', () => { const error = Object.assign(new Error('Error parsing chunk: SyntaxError: Unexpected end'), { config: {url: '/viewer/query', method: 'POST'}, headers: {'x-trace-id': 'abc123'}, errorPhase: 'stream', }); const details = extractErrorDetails(error); expect(details?.message).toBe('Error parsing chunk: SyntaxError: Unexpected end'); expect(details?.errorPhase).toBe('stream'); }); test('should extract errorPhase connection from enriched fetch error', () => { const error = Object.assign(new TypeError('Failed to fetch'), { config: {url: '/viewer/query', method: 'POST'}, errorPhase: 'connection', }); const details = extractErrorDetails(error); expect(details?.errorPhase).toBe('connection'); }); test('should extract errorPhase stream from enriched stream error', () => { const error = Object.assign(new Error('ERR_INCOMPLETE_CHUNKED_ENCODING'), { config: {url: '/viewer/query', method: 'POST'}, headers: {'x-trace-id': 'abc123'}, errorPhase: 'stream', }); const details = extractErrorDetails(error); expect(details?.errorPhase).toBe('stream'); }); test('should not set errorPhase when not present on error', () => { const error = { status: 500, statusText: 'Internal Server Error', data: 'error', }; const details = extractErrorDetails(error); expect(details?.errorPhase).toBeUndefined(); }); test('should extract networkOnline false from enriched connection error', () => { const error = Object.assign(new TypeError('Failed to fetch'), { config: {url: '/viewer/query', method: 'POST'}, errorPhase: 'connection', networkOnline: false, }); const details = extractErrorDetails(error); expect(details?.networkOnline).toBe(false); expect(details?.errorPhase).toBe('connection'); }); test('should extract networkOnline true when online but server unreachable', () => { const error = Object.assign(new TypeError('Failed to fetch'), { config: {url: '/viewer/query', method: 'POST'}, errorPhase: 'connection', networkOnline: true, }); const details = extractErrorDetails(error); expect(details?.networkOnline).toBe(true); }); test('should not set networkOnline when not present on error', () => { const error = Object.assign(new TypeError('Failed to fetch'), { config: {url: '/viewer/query', method: 'POST'}, errorPhase: 'connection', }); const details = extractErrorDetails(error); expect(details?.networkOnline).toBeUndefined(); }); });