trace

Create a custom trace span with OpenTelemetry-compatible timing, attributes, and status tracking. When telemetry is disabled, this is a zero-cost passthrough that calls the function with a no-op span.

Import

import { trace } from 'catmint/telemetry'

Signature

async function trace<T>(
  name: string,
  fn: (span: Span) => Promise<T> | T,
  options?: SpanOptions,
): Promise<T>

Parameters

ParameterTypeRequiredDescription
namestringYesThe span name, typically a dotted identifier like 'checkout.calculate'.
fn(span: Span) => Promise<T> | TYesThe function to execute within the span. Receives the active Span object.
optionsSpanOptionsNoOptional span configuration.

SpanOptions

PropertyTypeRequiredDescription
attributesRecord<string, string | number | boolean>NoKey-value attributes attached to the span.
kind'internal' | 'server' | 'client' | 'producer' | 'consumer'NoThe span kind. Defaults to 'internal'.

Span

The Span interface passed to the callback:

interface Span {
  setAttribute(key: string, value: string | number | boolean): void
  addEvent(name: string, attributes?: Record<string, unknown>): void
  setStatus(status: { code: number; message?: string }): void
  end(): void
}

Use SpanStatusCode constants for the code field:

import { SpanStatusCode } from 'catmint/telemetry'

SpanStatusCode.OK    // 1
SpanStatusCode.ERROR // 2
SpanStatusCode.UNSET // 0

Return Value

Returns the value returned by fn. If fn throws, the span is automatically marked with ERROR status and an exception event before re-throwing.

Behavior

  • Telemetry disabled: fn is called directly with a no-op span. Zero overhead.
  • Telemetry enabled: A real span is created with trace/span IDs, timing, and parent correlation.
  • Sampling: If sampleRate < 1 in the telemetry config, some traces are skipped (using a no-op span).
  • Nesting: Spans automatically inherit the traceId from the parent span. A span stack tracks the active parent.

Examples

import { trace } from 'catmint/telemetry'

const result = await trace('checkout.calculate', async (span) => {
  span.setAttribute('cart.items', cart.items.length)
  const total = await calculateTotal(cart)
  span.setAttribute('cart.total', total)
  return total
})
// Nested spans share the same traceId
await trace('order.process', async (outerSpan) => {
  outerSpan.setAttribute('order.id', orderId)

  await trace('order.validate', async (innerSpan) => {
    // innerSpan.traceId === outerSpan.traceId
    await validateOrder(order)
  })

  await trace('order.charge', async (innerSpan) => {
    innerSpan.setAttribute('payment.method', 'card')
    await chargePayment(order)
  })
})
// With span kind and initial attributes
await trace(
  'api.fetchUser',
  async (span) => {
    const user = await fetch('/api/user')
    return user.json()
  },
  {
    kind: 'client',
    attributes: { 'http.method': 'GET', 'http.url': '/api/user' },
  },
)

See Also