HTTP tracing
Trace outbound HTTP requests in Node.js with recount.fetch, a drop-in replacement for fetch that records the request and response.
recount.fetch(name, input, init?, opts?)
A drop-in replacement for fetch that also records the request and response as an http span on the active trace. It returns the Response untouched, so you check the status and read the body exactly as you would with fetch:
await recount.startTrace('Load policy', async () => {
const res = await recount.fetch(
'Fetch policy',
'https://api.example.com/policies/POL-8821-AU'
);
if (!res.ok) throw new Error(`Policy lookup failed: ${res.status}`);
const policy = (await res.json()) as Policy;
});The span captures:
- Request method, URL, headers, and body
- Response status, headers, and body
- Duration in milliseconds (until the response headers arrive)
The span reads its copy of the response body from a clone, so your code can still call .json(), .text(), .blob() or read .body as a stream. Bodies are captured as JSON when they parse as JSON (whatever the content-type says), and as text otherwise. Event streams and binary responses (images, PDFs, archives, application/octet-stream) aren't captured, a body still arriving one second after the headers isn't captured (so slow or never-ending streams return straight away), and captured bodies, URLs and headers are trimmed to fit the ingest limits.
If the request fails without a response (DNS failure, refused connection, abort), the span is still recorded with the request and the error as its description, and the error is rethrown.
Parameters
| Parameter | Type | Description |
|---|---|---|
name | string | Label for the span in the dashboard |
input | string | URL | Request | Request URL or Request, as for fetch |
init | RequestInit | Standard fetch options (method, headers, body, etc.) |
opts | { redact?: string[] } | Fields to redact from request/response body. See Redaction |
Returns Promise<Response>.
POST example
const res = await recount.fetch(
'create-record',
'https://api.example.com/records',
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: 'Acme Corp' })
}
);
const created = (await res.json()) as CreateResponse;Redacting body fields
Pass opts.redact to strip sensitive fields from the captured request or response body. Redaction only affects the span; the Response you get back is unchanged:
const res = await recount.fetch(
'get-user',
'https://api.example.com/users/1',
undefined,
{ redact: ['user.ssn', 'user.dateOfBirth'] }
);Authorization headers are always redacted automatically - see Redaction.
Limits
To keep a trace within what ingest accepts, the captured span (never the request or response itself) is trimmed:
- Text bodies longer than 65,536 characters are truncated.
- JSON bodies longer than 65,536 characters are replaced with a short
[JSON body omitted: ...]note. Truncated JSON can't be parsed, soredactpaths couldn't be applied to it. - URLs are truncated to 2,048 characters.
- At most 50 headers are kept, each value truncated to 1,024 characters.
- Methods other than
GET,POST,PUT,PATCHandDELETEare recorded asGET.
Migrating from 0.7
recount.fetch used to return the parsed body (T | null). It now returns the Response:
// before
const policy = await recount.fetch<Policy>('Fetch policy', url);
// after
const res = await recount.fetch('Fetch policy', url);
const policy = (await res.json()) as Policy;Manual HTTP spans
If you need to capture HTTP details from a request you made yourself, use addSpan with type 'http':
import type {
HttpRequestInput,
HttpResponseOutput
} from '@refactorlabs/recount';
recount.addSpan('http', 'Fraud check', {
input: {
method: 'POST',
url: 'https://api.fraudservice.com/v1/check',
content_type: 'json',
body: { claimId: 'CLM-2024-03941', amount: 6840 }
} satisfies HttpRequestInput,
output: {
status: 200,
content_type: 'json',
body: { score: 0.03, verdict: 'low_risk' }
} satisfies HttpResponseOutput,
durationMs: 340
});