Traces & spans
Start traces and add spans, alerts, groups, messages and metadata with the Recount Node.js SDK: startTrace, addSpan and init.
recount.init(options?)
Optionally configure the SDK. The SDK reads RECOUNT_API_KEY from the environment automatically, so init is only needed to override defaults.
import recount from '@refactorlabs/recount';
recount.init({
apiKey: 'your-api-key', // overrides RECOUNT_API_KEY env var
apiUrl: 'https://...', // override the ingest endpoint
environment: 'production' // overrides RECOUNT_ENVIRONMENT env var
});If no API key is found, the SDK disables itself. All calls become no-ops with no errors thrown.
Options
| Option | Type | Description |
|---|---|---|
apiKey | string | Your Recount API key |
apiUrl | string | Override the default ingest URL |
environment | string | Tag all traces with this environment (e.g. 'production'). Reads RECOUNT_ENVIRONMENT env var |
debug | boolean | Log SDK operations to stdout |
recount.startTrace(name, fn)
Wraps an async function in a trace. The trace is sent to Recount when fn resolves or throws.
await recount.startTrace('Assess claim', async () => {
// work happens here
});name must be 5–500 characters.
Pass options as the second argument:
await recount.startTrace(
'Insurance claim - CLM-2024-03941',
{
metadata: { claimId: 'CLM-2024-03941', policyNumber: 'POL-8821-AU' },
retentionPeriod: '30d'
},
async () => {
// work happens here
}
);Options
| Option | Type | Description |
|---|---|---|
metadata | Record<string, string | number | boolean | null> | Filterable key-value metadata |
environment | string | Override the environment for this trace |
retentionPeriod | RetentionPeriod | How long to keep the trace. Default: '30d' |
Retention periods
type RetentionPeriod = '1d' | '7d' | '30d' | '90d' | '1y';recount.named(name, fn)
Sets the span name for the next auto-instrumented call inside fn. Integrations like wrapAnthropic, wrapOpenAI, and wrapCollection pick up this name instead of their default (model name, operation name, etc.).
const result = await recount.named('Classify intent', () =>
anthropic.messages.create({
model: 'claude-sonnet-4-6',
max_tokens: 512,
messages: [{ role: 'user', content: prompt }]
})
);
// → llm span named 'Classify intent' instead of 'claude-sonnet-4-6'The name applies to exactly one span - the first auto-instrumented call that runs inside fn. Subsequent calls revert to their default naming.
| Parameter | Type | Description |
|---|---|---|
name | string | Span name to use for the next auto-instrumented call |
fn | () => Promise<T> | Callback containing the instrumented call |
Returns the result of fn, typed to its return value.
recount.addSpan(type, name, ...)
Add a span to the currently active trace. Must be called inside a startTrace callback.
There are two forms: static and callback.
Static form
Pass an options object to set input, output, durationMs, and other fields manually:
recount.addSpan('db', 'Policy lookup', {
metadata: { table: 'policies', queryType: 'sql' },
input: {
query: 'SELECT * FROM policies WHERE number = $1',
params: ['POL-8821-AU']
},
output: { rows: 1 }
});Callback form
Pass an async callback and Recount handles timing and output automatically. The span's durationMs is set from how long the callback takes, and its output is set from the callback's return value.
const policy = await recount.addSpan('json', 'Load policy', async () => {
return await db.policies.findOne({ number: 'POL-8821-AU' });
// ^ becomes the span output automatically
});
// policy is typed to the return valuePass options as the third argument to include input, redact, or metadata:
await recount.addSpan(
'json',
'Load policy',
{ input: { policyNumber: 'POL-8821-AU' }, redact: ['holder.name'] },
async () => {
return await db.policies.findOne({ number: 'POL-8821-AU' });
}
);Empty span dropping
Spans with no input and no output are silently dropped at ingest time (except message spans, which carry their text separately). Groups that produce no surviving children are also dropped. If all spans are dropped, the trace is not sent.
This means you can safely call addSpan inside conditional logic; spans that contribute nothing are discarded automatically.
| Span type | Drop condition |
|---|---|
json, table, chart, http, llm, image, db | No input and no output |
group | All child spans were dropped |
message | Never dropped |
Enable debug: true in recount.init() to log dropped spans to stdout.
Overloads
| Signature | Description |
|---|---|
addSpan(type, name, options?) | Static - set all fields manually |
addSpan(type, name, fn) | Callback - auto-timing, auto-output |
addSpan(type, name, opts, fn) | Callback with options |
Span types
| Type | Use for |
|---|---|
message | Plain text output |
json | Structured JSON data |
table | Tabular data (use recount.table()) |
chart | Charts (use recount.chart()) |
http | HTTP requests (usually via recount.fetch()) |
llm | LLM calls, see LLM spans |
image | Images rendered inline, see Image spans |
db | Database queries, see Database spans |
group | Group child spans together, see Groups |
Options
| Option | Type | Description |
|---|---|---|
description | string | Short description shown below the span header in the dashboard (max 1,000 chars) |
input | JsonObject | Input to this step |
output | JsonObject | Output from this step |
durationMs | number | How long this step took |
metadata | DbSpanMetadata | When type: 'db', set collection, table, or queryType |
redact | string[] | Fields to redact from input/output. See Redaction |
public | boolean | Mark this span as public (visible to end users in shared views) |
recount.addAlert(label, level, options?)
Flag the most recently added span with a warn or error badge. Call it right after the addSpan (or addMessage) it applies to - there's no built-in threshold, so decide with a plain if:
recount.addSpan('json', 'Query time', { output: { durationMs: 5240 } });
if (durationMs > 1000) {
recount.addAlert('Query duration (ms)', 'warn', {
checks: [{ value: durationMs }]
});
}See Alerts for the full API, including multiple checks and usage inside groups.
recount.addGroup(name, builder, options?)
Group related spans together under a named, collapsible section. See Groups for full details.
recount.addGroup('Plan comparison', (g) => {
g.span('table', 'Current plan', {
output: recount.tableFromObject({ data: currentPlan })
});
g.span('table', 'Recommended plan', {
output: recount.tableFromObject({ data: bestPlan })
});
});recount.addMessage(name, text?, opts?)
Shorthand for adding a message span. Useful for log-style checkpoints.
recount.addMessage('Validation passed', 'All policy checks cleared', {
level: 'info'
});
recount.addMessage(
'High-value claim',
'Claim exceeds $10,000 - flagged for review',
{ level: 'warn' }
);
// text is optional - name alone is enough
recount.addMessage('Cache warmed');Options
| Option | Type | Description |
|---|---|---|
description | string | Short description shown below the span header in the dashboard (max 1,000 chars) |
level | 'info' | 'warn' | 'error' | Severity. Default: 'info' |
public | boolean | Mark this span as public |
recount.addMetadata(data)
Attach additional metadata to the active trace. Useful when metadata is only known partway through execution.
await recount.startTrace('Process application', async () => {
const application = await loadApplication(id);
// Attach identifiers once we have them
recount.addMetadata({
applicationId: application.id,
customerId: application.customerId
});
// ... rest of logic
});Metadata values must be strings, numbers, booleans, or null. Metadata is merged with any metadata set on startTrace.
| Parameter | Type | Description |
|---|---|---|
data | Record<string, string | number | boolean | null> | Key-value pairs to merge into trace metadata |