From a6ee497836e7b6420758413343a6e37339700e14 Mon Sep 17 00:00:00 2001 From: jdalton Date: Fri, 17 Apr 2026 20:37:48 -0400 Subject: [PATCH 1/6] fix(debug): log structured HTTP error details instead of raw response MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When an HTTP request fails, `debugApiResponse` now logs: * endpoint (description) — already was * status — already was * method, url, durationMs — already was * sanitized request headers (Authorization/api-keys redacted) — already was And new fields that make support tickets actionable: * requestedAt — ISO-8601 timestamp of request start, for correlating with server-side logs. * cfRay — Cloudflare trace id extracted as a top-level field from response headers (accepts `cf-ray` or `CF-Ray` casing). * responseHeaders — sanitized headers returned by the server. * responseBody — string response body, truncated at 2_000 bytes so megabyte payloads don't balloon debug logs. Wired into both the `queryApiSafeText` and `sendApiRequest` !ok branches, which are the primary points where a non-thrown HTTP error reaches a user. The success-path log includes the new `requestedAt` timestamp too. Added 5 new tests in `test/unit/utils/debug.test.mts` covering requestedAt, cfRay (both casings), body passthrough, and body truncation. Existing debug-output shape preserved: callers that don't pass the new fields (`responseHeaders`, `responseBody`, `requestedAt`) see no change. --- packages/cli/src/utils/debug.mts | 118 ++++++++++++++------ packages/cli/src/utils/socket/api.mts | 20 +++- packages/cli/test/unit/utils/debug.test.mts | 75 +++++++++++++ 3 files changed, 176 insertions(+), 37 deletions(-) diff --git a/packages/cli/src/utils/debug.mts b/packages/cli/src/utils/debug.mts index 4687e6c17..24023f0b0 100644 --- a/packages/cli/src/utils/debug.mts +++ b/packages/cli/src/utils/debug.mts @@ -33,8 +33,20 @@ export type ApiRequestDebugInfo = { url?: string | undefined headers?: Record | undefined durationMs?: number | undefined + // ISO-8601 timestamp of when the request was initiated. Useful when + // correlating failures with server-side logs. + requestedAt?: string | undefined + // Response headers from the failed request. The helper extracts the + // cf-ray trace id as a first-class field so support can look it up in + // the Cloudflare dashboard without eyeballing the whole header dump. + responseHeaders?: Record | undefined + // Response body string; truncated by the helper to a safe length so + // logs don't balloon on megabyte payloads. + responseBody?: string | undefined } +const RESPONSE_BODY_TRUNCATE_LENGTH = 2_000 + /** * Sanitize headers to remove sensitive information. * Redacts Authorization and API key headers. @@ -76,16 +88,70 @@ export function debugApiRequest( } } +/** + * Build the structured debug payload shared by the error + failure-status + * branches of `debugApiResponse`. Extracted so both paths log the same + * shape. + */ +function buildApiDebugDetails( + base: Record, + requestInfo?: ApiRequestDebugInfo | undefined, +): Record { + if (!requestInfo) { + return base + } + const details: Record = { ...base } + if (requestInfo.requestedAt) { + details['requestedAt'] = requestInfo.requestedAt + } + if (requestInfo.method) { + details['method'] = requestInfo.method + } + if (requestInfo.url) { + details['url'] = requestInfo.url + } + if (requestInfo.durationMs !== undefined) { + details['durationMs'] = requestInfo.durationMs + } + if (requestInfo.headers) { + details['headers'] = sanitizeHeaders(requestInfo.headers) + } + if (requestInfo.responseHeaders) { + const cfRay = + requestInfo.responseHeaders['cf-ray'] ?? + requestInfo.responseHeaders['CF-Ray'] + if (cfRay) { + // First-class field so it's obvious when filing a support ticket + // that points at a Cloudflare trace. + details['cfRay'] = cfRay + } + details['responseHeaders'] = sanitizeHeaders(requestInfo.responseHeaders) + } + if (requestInfo.responseBody !== undefined) { + const body = requestInfo.responseBody + details['responseBody'] = + body.length > RESPONSE_BODY_TRUNCATE_LENGTH + ? `${body.slice(0, RESPONSE_BODY_TRUNCATE_LENGTH)}… (truncated, ${body.length} bytes)` + : body + } + return details +} + /** * Debug an API response with detailed request information. - * Logs essential info without exposing sensitive data. * - * For failed requests (status >= 400 or error), logs: - * - HTTP method (GET, POST, etc.) - * - Full URL - * - Response status code - * - Sanitized headers (Authorization redacted) - * - Request duration in milliseconds + * For failed requests (status >= 400 or error), logs a structured + * object with: + * - endpoint (human-readable description) + * - requestedAt (ISO timestamp, if passed) + * - method, url, durationMs + * - sanitized request headers (Authorization redacted) + * - cfRay (extracted from response headers if present) + * - sanitized response headers + * - responseBody (truncated) + * + * All request-headers are sanitized to redact Authorization and + * `*api-key*` values. */ export function debugApiResponse( endpoint: string, @@ -94,37 +160,19 @@ export function debugApiResponse( requestInfo?: ApiRequestDebugInfo | undefined, ): void { if (error) { - const errorDetails = { - __proto__: null, - endpoint, - error: error instanceof Error ? error.message : UNKNOWN_ERROR, - ...(requestInfo?.method ? { method: requestInfo.method } : {}), - ...(requestInfo?.url ? { url: requestInfo.url } : {}), - ...(requestInfo?.durationMs !== undefined - ? { durationMs: requestInfo.durationMs } - : {}), - ...(requestInfo?.headers - ? { headers: sanitizeHeaders(requestInfo.headers) } - : {}), - } - debugDir(errorDetails) + debugDir( + buildApiDebugDetails( + { + endpoint, + error: error instanceof Error ? error.message : UNKNOWN_ERROR, + }, + requestInfo, + ), + ) } else if (status && status >= 400) { // For failed requests, log detailed information. if (requestInfo) { - const failureDetails = { - __proto__: null, - endpoint, - status, - ...(requestInfo.method ? { method: requestInfo.method } : {}), - ...(requestInfo.url ? { url: requestInfo.url } : {}), - ...(requestInfo.durationMs !== undefined - ? { durationMs: requestInfo.durationMs } - : {}), - ...(requestInfo.headers - ? { headers: sanitizeHeaders(requestInfo.headers) } - : {}), - } - debugDir(failureDetails) + debugDir(buildApiDebugDetails({ endpoint, status }, requestInfo)) } else { debug(`API ${endpoint}: HTTP ${status}`) } diff --git a/packages/cli/src/utils/socket/api.mts b/packages/cli/src/utils/socket/api.mts index 15b8a0ed4..16fc881d6 100644 --- a/packages/cli/src/utils/socket/api.mts +++ b/packages/cli/src/utils/socket/api.mts @@ -425,6 +425,7 @@ export async function queryApiSafeText( const baseUrl = getDefaultApiBaseUrl() const fullUrl = `${baseUrl}${baseUrl?.endsWith('/') ? '' : '/'}${path}` const startTime = Date.now() + const requestedAt = new Date(startTime).toISOString() let result: any try { @@ -440,6 +441,7 @@ export async function queryApiSafeText( method: 'GET', url: fullUrl, durationMs, + requestedAt, headers: { Authorization: '[REDACTED]' }, }) } catch (e) { @@ -455,6 +457,7 @@ export async function queryApiSafeText( method: 'GET', url: fullUrl, durationMs, + requestedAt, headers: { Authorization: '[REDACTED]' }, }) @@ -472,12 +475,17 @@ export async function queryApiSafeText( if (!result.ok) { const { status } = result const durationMs = Date.now() - startTime - // Log detailed error information. + // Log detailed error information — include response headers (for + // cf-ray) and a truncated body so support tickets have everything + // needed to file against Cloudflare or backend teams. debugApiResponse(description || 'Query API', status, undefined, { method: 'GET', url: fullUrl, durationMs, + requestedAt, headers: { Authorization: '[REDACTED]' }, + responseHeaders: result.headers, + responseBody: result.text?.(), }) // Log required permissions for 403 errors when in a command context. if (commandPath && status === 403) { @@ -584,6 +592,7 @@ export async function sendApiRequest( const fullUrl = `${baseUrl}${baseUrl.endsWith('/') ? '' : '/'}${path}` const startTime = Date.now() + const requestedAt = new Date(startTime).toISOString() let result: any try { @@ -611,6 +620,7 @@ export async function sendApiRequest( method, url: fullUrl, durationMs, + requestedAt, headers: { Authorization: '[REDACTED]', 'Content-Type': 'application/json', @@ -630,6 +640,7 @@ export async function sendApiRequest( method, url: fullUrl, durationMs, + requestedAt, headers: { Authorization: '[REDACTED]', 'Content-Type': 'application/json', @@ -650,15 +661,20 @@ export async function sendApiRequest( if (!result.ok) { const { status } = result const durationMs = Date.now() - startTime - // Log detailed error information. + // Log detailed error information — include response headers (for + // cf-ray) and a truncated body so support tickets have everything + // needed to file against Cloudflare or backend teams. debugApiResponse(description || 'Send API Request', status, undefined, { method, url: fullUrl, durationMs, + requestedAt, headers: { Authorization: '[REDACTED]', 'Content-Type': 'application/json', }, + responseHeaders: result.headers, + responseBody: result.text?.(), }) // Log required permissions for 403 errors when in a command context. if (commandPath && status === 403) { diff --git a/packages/cli/test/unit/utils/debug.test.mts b/packages/cli/test/unit/utils/debug.test.mts index 0e01d86ca..905e7513a 100644 --- a/packages/cli/test/unit/utils/debug.test.mts +++ b/packages/cli/test/unit/utils/debug.test.mts @@ -188,6 +188,81 @@ describe('debug utilities', () => { expect(calledWith.url).toBeUndefined() expect(calledWith.headers).toBeUndefined() }) + + it('includes requestedAt timestamp when provided', () => { + const requestInfo = { + method: 'POST', + url: 'https://api.socket.dev/x', + requestedAt: '2026-04-18T00:00:00.000Z', + } + + debugApiResponse('/api/x', 500, undefined, requestInfo) + + const calledWith = mockDebugDir.mock.calls[0]?.[0] + expect(calledWith.requestedAt).toBe('2026-04-18T00:00:00.000Z') + }) + + it('extracts cf-ray as a top-level field and keeps responseHeaders', () => { + const requestInfo = { + method: 'GET', + url: 'https://api.socket.dev/y', + responseHeaders: { + 'cf-ray': 'abc123-IAD', + 'content-type': 'application/json', + }, + } + + debugApiResponse('/api/y', 500, undefined, requestInfo) + + const calledWith = mockDebugDir.mock.calls[0]?.[0] + expect(calledWith.cfRay).toBe('abc123-IAD') + expect(calledWith.responseHeaders?.['cf-ray']).toBe('abc123-IAD') + }) + + it('tolerates CF-Ray header casing', () => { + const requestInfo = { + method: 'GET', + url: 'https://api.socket.dev/z', + responseHeaders: { + 'CF-Ray': 'xyz789-SJC', + }, + } + + debugApiResponse('/api/z', 500, undefined, requestInfo) + + const calledWith = mockDebugDir.mock.calls[0]?.[0] + expect(calledWith.cfRay).toBe('xyz789-SJC') + }) + + it('includes response body on error', () => { + const requestInfo = { + method: 'GET', + url: 'https://api.socket.dev/body', + responseBody: '{"error":"bad"}', + } + + debugApiResponse('/api/body', 400, undefined, requestInfo) + + const calledWith = mockDebugDir.mock.calls[0]?.[0] + expect(calledWith.responseBody).toBe('{"error":"bad"}') + }) + + it('truncates oversized response bodies', () => { + const bigBody = 'x'.repeat(5000) + const requestInfo = { + method: 'GET', + url: 'https://api.socket.dev/big', + responseBody: bigBody, + } + + debugApiResponse('/api/big', 500, undefined, requestInfo) + + const calledWith = mockDebugDir.mock.calls[0]?.[0] + expect(calledWith.responseBody).toMatch(/… \(truncated, 5000 bytes\)$/) + expect((calledWith.responseBody as string).length).toBeLessThan( + bigBody.length, + ) + }) }) describe('debugFileOp', () => { From 9dffe43da64e8e521498d607e517d6fb4403accb Mon Sep 17 00:00:00 2001 From: jdalton Date: Fri, 17 Apr 2026 22:08:25 -0400 Subject: [PATCH 2/6] fix(api): guard response.text() in error paths and label chars Addresses Cursor bugbot feedback on PR #1233. 1. `result.text?.()` in the `!result.ok` branches of `queryApiSafeText` and `sendApiRequest` was unguarded. If `text()` threw, the exception propagated past the clean `{ ok: false, ... }` return and broke the error-handling contract. Wrap both call sites in a shared `tryReadResponseText` helper that swallows the failure and returns `undefined`. 2. The truncation suffix reported `body.length` as "bytes", but `String.prototype.length` / `String.prototype.slice` count UTF-16 code units, not bytes. For non-ASCII response bodies the label was misleading. Rename to "chars" so the counter matches the actual measurement. --- packages/cli/src/utils/debug.mts | 5 ++++- packages/cli/src/utils/socket/api.mts | 16 ++++++++++++++-- packages/cli/test/unit/utils/debug.test.mts | 2 +- 3 files changed, 19 insertions(+), 4 deletions(-) diff --git a/packages/cli/src/utils/debug.mts b/packages/cli/src/utils/debug.mts index 24023f0b0..4237bf686 100644 --- a/packages/cli/src/utils/debug.mts +++ b/packages/cli/src/utils/debug.mts @@ -129,9 +129,12 @@ function buildApiDebugDetails( } if (requestInfo.responseBody !== undefined) { const body = requestInfo.responseBody + // `.length` / `.slice` operate on UTF-16 code units, not bytes, so + // the counter and truncation are both reported in "chars" to stay + // consistent with what we actually measured. details['responseBody'] = body.length > RESPONSE_BODY_TRUNCATE_LENGTH - ? `${body.slice(0, RESPONSE_BODY_TRUNCATE_LENGTH)}… (truncated, ${body.length} bytes)` + ? `${body.slice(0, RESPONSE_BODY_TRUNCATE_LENGTH)}… (truncated, ${body.length} chars)` : body } return details diff --git a/packages/cli/src/utils/socket/api.mts b/packages/cli/src/utils/socket/api.mts index 16fc881d6..d5a1c50a3 100644 --- a/packages/cli/src/utils/socket/api.mts +++ b/packages/cli/src/utils/socket/api.mts @@ -82,6 +82,18 @@ export async function socketHttpRequest( return await httpRequest(url, options) } +// Safe wrapper for `response.text()` in error-handling code paths. +// `text()` can throw (e.g. already consumed, malformed body), which +// would blow past the `ok: false` CResult return and break the +// error-handling contract of callers like `queryApiSafeText`. +function tryReadResponseText(result: HttpResponse): string | undefined { + try { + return result.text?.() + } catch { + return undefined + } +} + export type CommandRequirements = { permissions?: string[] | undefined quota?: number | undefined @@ -485,7 +497,7 @@ export async function queryApiSafeText( requestedAt, headers: { Authorization: '[REDACTED]' }, responseHeaders: result.headers, - responseBody: result.text?.(), + responseBody: tryReadResponseText(result), }) // Log required permissions for 403 errors when in a command context. if (commandPath && status === 403) { @@ -674,7 +686,7 @@ export async function sendApiRequest( 'Content-Type': 'application/json', }, responseHeaders: result.headers, - responseBody: result.text?.(), + responseBody: tryReadResponseText(result), }) // Log required permissions for 403 errors when in a command context. if (commandPath && status === 403) { diff --git a/packages/cli/test/unit/utils/debug.test.mts b/packages/cli/test/unit/utils/debug.test.mts index 905e7513a..ab1efc776 100644 --- a/packages/cli/test/unit/utils/debug.test.mts +++ b/packages/cli/test/unit/utils/debug.test.mts @@ -258,7 +258,7 @@ describe('debug utilities', () => { debugApiResponse('/api/big', 500, undefined, requestInfo) const calledWith = mockDebugDir.mock.calls[0]?.[0] - expect(calledWith.responseBody).toMatch(/… \(truncated, 5000 bytes\)$/) + expect(calledWith.responseBody).toMatch(/… \(truncated, 5000 chars\)$/) expect((calledWith.responseBody as string).length).toBeLessThan( bigBody.length, ) From 7b0c42918d985cc72ff439e2d33bd92822f1a9ff Mon Sep 17 00:00:00 2001 From: jdalton Date: Mon, 20 Apr 2026 14:23:37 -0400 Subject: [PATCH 3/6] chore(debug): trim redundant comments in API debug logging --- packages/cli/src/utils/debug.mts | 19 +++++-------------- packages/cli/src/utils/socket/api.mts | 12 ++++++------ 2 files changed, 11 insertions(+), 20 deletions(-) diff --git a/packages/cli/src/utils/debug.mts b/packages/cli/src/utils/debug.mts index 4237bf686..1fa844469 100644 --- a/packages/cli/src/utils/debug.mts +++ b/packages/cli/src/utils/debug.mts @@ -141,20 +141,12 @@ function buildApiDebugDetails( } /** - * Debug an API response with detailed request information. + * Debug an API response. Only failed requests (error or status >= 400) + * produce structured output; successful responses optionally log a + * one-liner at the `notice` debug namespace. * - * For failed requests (status >= 400 or error), logs a structured - * object with: - * - endpoint (human-readable description) - * - requestedAt (ISO timestamp, if passed) - * - method, url, durationMs - * - sanitized request headers (Authorization redacted) - * - cfRay (extracted from response headers if present) - * - sanitized response headers - * - responseBody (truncated) - * - * All request-headers are sanitized to redact Authorization and - * `*api-key*` values. + * Request and response headers are sanitized via `sanitizeHeaders` so + * Authorization and `*api-key*` values are redacted. */ export function debugApiResponse( endpoint: string, @@ -173,7 +165,6 @@ export function debugApiResponse( ), ) } else if (status && status >= 400) { - // For failed requests, log detailed information. if (requestInfo) { debugDir(buildApiDebugDetails({ endpoint, status }, requestInfo)) } else { diff --git a/packages/cli/src/utils/socket/api.mts b/packages/cli/src/utils/socket/api.mts index d5a1c50a3..9b9e3a754 100644 --- a/packages/cli/src/utils/socket/api.mts +++ b/packages/cli/src/utils/socket/api.mts @@ -487,9 +487,9 @@ export async function queryApiSafeText( if (!result.ok) { const { status } = result const durationMs = Date.now() - startTime - // Log detailed error information — include response headers (for - // cf-ray) and a truncated body so support tickets have everything - // needed to file against Cloudflare or backend teams. + // Include response headers (for cf-ray) and a truncated body so + // support tickets have everything needed to file against Cloudflare + // or backend teams. debugApiResponse(description || 'Query API', status, undefined, { method: 'GET', url: fullUrl, @@ -673,9 +673,9 @@ export async function sendApiRequest( if (!result.ok) { const { status } = result const durationMs = Date.now() - startTime - // Log detailed error information — include response headers (for - // cf-ray) and a truncated body so support tickets have everything - // needed to file against Cloudflare or backend teams. + // Include response headers (for cf-ray) and a truncated body so + // support tickets have everything needed to file against Cloudflare + // or backend teams. debugApiResponse(description || 'Send API Request', status, undefined, { method, url: fullUrl, From 07162408b3d3450c93ec78403df08ba69e88d9aa Mon Sep 17 00:00:00 2001 From: jdalton Date: Mon, 20 Apr 2026 15:00:59 -0400 Subject: [PATCH 4/6] chore(debug): restore __proto__: null on API debug payload --- packages/cli/src/utils/debug.mts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/cli/src/utils/debug.mts b/packages/cli/src/utils/debug.mts index 1fa844469..0e3c64d36 100644 --- a/packages/cli/src/utils/debug.mts +++ b/packages/cli/src/utils/debug.mts @@ -97,10 +97,12 @@ function buildApiDebugDetails( base: Record, requestInfo?: ApiRequestDebugInfo | undefined, ): Record { + // `__proto__: null` keeps the payload free of prototype-chain keys + // when callers iterate over `debugDir`'s output. + const details: Record = { __proto__: null, ...base } as Record if (!requestInfo) { - return base + return details } - const details: Record = { ...base } if (requestInfo.requestedAt) { details['requestedAt'] = requestInfo.requestedAt } From ea881e63f8dcddea009b57049f8180d8afa7c1df Mon Sep 17 00:00:00 2001 From: jdalton Date: Mon, 20 Apr 2026 15:06:23 -0400 Subject: [PATCH 5/6] chore(debug): sort ApiRequestDebugInfo properties alphabetically --- packages/cli/src/utils/debug.mts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/cli/src/utils/debug.mts b/packages/cli/src/utils/debug.mts index 0e3c64d36..e18aca84b 100644 --- a/packages/cli/src/utils/debug.mts +++ b/packages/cli/src/utils/debug.mts @@ -29,20 +29,20 @@ import { } from '@socketsecurity/lib/debug' export type ApiRequestDebugInfo = { - method?: string | undefined - url?: string | undefined - headers?: Record | undefined durationMs?: number | undefined + headers?: Record | undefined + method?: string | undefined // ISO-8601 timestamp of when the request was initiated. Useful when // correlating failures with server-side logs. requestedAt?: string | undefined + // Response body string; truncated by the helper to a safe length so + // logs don't balloon on megabyte payloads. + responseBody?: string | undefined // Response headers from the failed request. The helper extracts the // cf-ray trace id as a first-class field so support can look it up in // the Cloudflare dashboard without eyeballing the whole header dump. responseHeaders?: Record | undefined - // Response body string; truncated by the helper to a safe length so - // logs don't balloon on megabyte payloads. - responseBody?: string | undefined + url?: string | undefined } const RESPONSE_BODY_TRUNCATE_LENGTH = 2_000 From a0e7c42f7f6cc80080dbb145424ecf71ba0c2d32 Mon Sep 17 00:00:00 2001 From: jdalton Date: Mon, 20 Apr 2026 16:08:50 -0400 Subject: [PATCH 6/6] refactor(debug): route API failure logs through the error namespace --- packages/cli/src/utils/debug.mts | 15 +++++++------ packages/cli/test/unit/utils/debug.test.mts | 24 ++++++++++----------- 2 files changed, 20 insertions(+), 19 deletions(-) diff --git a/packages/cli/src/utils/debug.mts b/packages/cli/src/utils/debug.mts index e18aca84b..9a2f9a917 100644 --- a/packages/cli/src/utils/debug.mts +++ b/packages/cli/src/utils/debug.mts @@ -98,7 +98,7 @@ function buildApiDebugDetails( requestInfo?: ApiRequestDebugInfo | undefined, ): Record { // `__proto__: null` keeps the payload free of prototype-chain keys - // when callers iterate over `debugDir`'s output. + // when callers iterate over the debug output. const details: Record = { __proto__: null, ...base } as Record if (!requestInfo) { return details @@ -143,9 +143,9 @@ function buildApiDebugDetails( } /** - * Debug an API response. Only failed requests (error or status >= 400) - * produce structured output; successful responses optionally log a - * one-liner at the `notice` debug namespace. + * Debug an API response. Failed requests (error or status >= 400) log + * under the `error` namespace; successful responses optionally log a + * one-liner under `notice`. * * Request and response headers are sanitized via `sanitizeHeaders` so * Authorization and `*api-key*` values are redacted. @@ -157,7 +157,8 @@ export function debugApiResponse( requestInfo?: ApiRequestDebugInfo | undefined, ): void { if (error) { - debugDir( + debugDirNs( + 'error', buildApiDebugDetails( { endpoint, @@ -168,9 +169,9 @@ export function debugApiResponse( ) } else if (status && status >= 400) { if (requestInfo) { - debugDir(buildApiDebugDetails({ endpoint, status }, requestInfo)) + debugDirNs('error', buildApiDebugDetails({ endpoint, status }, requestInfo)) } else { - debug(`API ${endpoint}: HTTP ${status}`) + debugNs('error', `API ${endpoint}: HTTP ${status}`) } /* c8 ignore next 3 */ } else if (isDebugNs('notice')) { diff --git a/packages/cli/test/unit/utils/debug.test.mts b/packages/cli/test/unit/utils/debug.test.mts index ab1efc776..f5d711062 100644 --- a/packages/cli/test/unit/utils/debug.test.mts +++ b/packages/cli/test/unit/utils/debug.test.mts @@ -97,16 +97,16 @@ describe('debug utilities', () => { debugApiResponse('/api/test', undefined, error) - expect(debugDir).toHaveBeenCalledWith({ + expect(mockDebugDirNs).toHaveBeenCalledWith('error', { endpoint: '/api/test', error: 'API failed', }) }) - it('logs warning for HTTP error status codes', () => { + it('logs under error namespace for HTTP error status codes', () => { debugApiResponse('/api/test', 404) - expect(debug).toHaveBeenCalledWith('API /api/test: HTTP 404') + expect(debugNs).toHaveBeenCalledWith('error', 'API /api/test: HTTP 404') }) it('logs notice for successful responses when debug is enabled', () => { @@ -128,7 +128,7 @@ describe('debug utilities', () => { it('handles non-Error objects in error parameter', () => { debugApiResponse('/api/test', undefined, 'String error') - expect(debugDir).toHaveBeenCalledWith({ + expect(mockDebugDirNs).toHaveBeenCalledWith('error', { endpoint: '/api/test', error: 'Unknown error', }) @@ -148,7 +148,7 @@ describe('debug utilities', () => { debugApiResponse('/api/test', undefined, error, requestInfo) - const calledWith = mockDebugDir.mock.calls[0]?.[0] + const calledWith = mockDebugDirNs.mock.calls[0]?.[1] as any expect(calledWith.method).toBe('POST') expect(calledWith.url).toBe('https://api.socket.dev/test') expect(calledWith.durationMs).toBe(1500) @@ -169,7 +169,7 @@ describe('debug utilities', () => { debugApiResponse('/api/resource', 500, undefined, requestInfo) - const calledWith = mockDebugDir.mock.calls[0]?.[0] + const calledWith = mockDebugDirNs.mock.calls[0]?.[1] as any expect(calledWith.status).toBe(500) expect(calledWith.method).toBe('GET') // API key should be redacted. @@ -183,7 +183,7 @@ describe('debug utilities', () => { debugApiResponse('/api/update', 400, undefined, requestInfo) - const calledWith = mockDebugDir.mock.calls[0]?.[0] + const calledWith = mockDebugDirNs.mock.calls[0]?.[1] as any expect(calledWith.method).toBe('PUT') expect(calledWith.url).toBeUndefined() expect(calledWith.headers).toBeUndefined() @@ -198,7 +198,7 @@ describe('debug utilities', () => { debugApiResponse('/api/x', 500, undefined, requestInfo) - const calledWith = mockDebugDir.mock.calls[0]?.[0] + const calledWith = mockDebugDirNs.mock.calls[0]?.[1] as any expect(calledWith.requestedAt).toBe('2026-04-18T00:00:00.000Z') }) @@ -214,7 +214,7 @@ describe('debug utilities', () => { debugApiResponse('/api/y', 500, undefined, requestInfo) - const calledWith = mockDebugDir.mock.calls[0]?.[0] + const calledWith = mockDebugDirNs.mock.calls[0]?.[1] as any expect(calledWith.cfRay).toBe('abc123-IAD') expect(calledWith.responseHeaders?.['cf-ray']).toBe('abc123-IAD') }) @@ -230,7 +230,7 @@ describe('debug utilities', () => { debugApiResponse('/api/z', 500, undefined, requestInfo) - const calledWith = mockDebugDir.mock.calls[0]?.[0] + const calledWith = mockDebugDirNs.mock.calls[0]?.[1] as any expect(calledWith.cfRay).toBe('xyz789-SJC') }) @@ -243,7 +243,7 @@ describe('debug utilities', () => { debugApiResponse('/api/body', 400, undefined, requestInfo) - const calledWith = mockDebugDir.mock.calls[0]?.[0] + const calledWith = mockDebugDirNs.mock.calls[0]?.[1] as any expect(calledWith.responseBody).toBe('{"error":"bad"}') }) @@ -257,7 +257,7 @@ describe('debug utilities', () => { debugApiResponse('/api/big', 500, undefined, requestInfo) - const calledWith = mockDebugDir.mock.calls[0]?.[0] + const calledWith = mockDebugDirNs.mock.calls[0]?.[1] as any expect(calledWith.responseBody).toMatch(/… \(truncated, 5000 chars\)$/) expect((calledWith.responseBody as string).length).toBeLessThan( bigBody.length,