Recount
Node.js

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

OptionTypeDescription
apiKeystringYour Recount API key
apiUrlstringOverride the default ingest URL
environmentstringTag all traces with this environment (e.g. 'production'). Reads RECOUNT_ENVIRONMENT env var
debugbooleanLog 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

OptionTypeDescription
metadataRecord<string, string | number | boolean | null>Filterable key-value metadata
environmentstringOverride the environment for this trace
retentionPeriodRetentionPeriodHow 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.

ParameterTypeDescription
namestringSpan 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 value

Pass 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 typeDrop condition
json, table, chart, http, llm, image, dbNo input and no output
groupAll child spans were dropped
messageNever dropped

Enable debug: true in recount.init() to log dropped spans to stdout.

Overloads

SignatureDescription
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

TypeUse for
messagePlain text output
jsonStructured JSON data
tableTabular data (use recount.table())
chartCharts (use recount.chart())
httpHTTP requests (usually via recount.fetch())
llmLLM calls, see LLM spans
imageImages rendered inline, see Image spans
dbDatabase queries, see Database spans
groupGroup child spans together, see Groups

Options

OptionTypeDescription
descriptionstringShort description shown below the span header in the dashboard (max 1,000 chars)
inputJsonObjectInput to this step
outputJsonObjectOutput from this step
durationMsnumberHow long this step took
metadataDbSpanMetadataWhen type: 'db', set collection, table, or queryType
redactstring[]Fields to redact from input/output. See Redaction
publicbooleanMark 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

OptionTypeDescription
descriptionstringShort description shown below the span header in the dashboard (max 1,000 chars)
level'info' | 'warn' | 'error'Severity. Default: 'info'
publicbooleanMark 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.

ParameterTypeDescription
dataRecord<string, string | number | boolean | null>Key-value pairs to merge into trace metadata

On this page