Harbor
Monitoring

TypeScript SDK

Set up Harbor tracing in a Node.js agent and stream its spans over OTLP.

harbor-ai-sdk registers an OpenTelemetry tracer provider, batches the spans your agent produces and exports them to Harbor over OTLP/HTTP. Spans are sent unmodified — Harbor normalizes them server-side — so the SDK stays small and never lags behind the ingestion pipeline.

If you build on the Vercel AI SDK or Mastra, start on those pages. This one covers the client itself, adding context to traces, and tracing code no framework knows about.

Install

npm install harbor-ai-sdk

Node.js 20 or later.

Initialize

Create the client once, as early in the process as you can.

src/telemetry.ts
import { Harbor } from 'harbor-ai-sdk'

export const harbor = new Harbor()

await harbor.ready
src/index.ts
import { harbor } from './telemetry'

new Harbor() registers the global tracer provider, so every span created afterwards — by your code or by any library writing to OpenTelemetry — is exported to Harbor.

apiKey defaults to HARBOR_API_KEY and baseUrl to HARBOR_BASE_URL. Pass either explicitly if you load configuration some other way. With either missing the client warns once, harbor.enabled is false, and nothing is exported.

Setup is synchronous today, so await harbor.ready is a formality — it is there so your code stays correct if that changes.

Group a turn with capture

capture opens one root span for a unit of work and attaches context to every span created inside it, at any depth, including spans your framework created. This is how a trace gets a user, a session and your own metadata.

import { capture } from 'harbor-ai-sdk'

app.post('/chat', async (req, res) => {
  const answer = await capture('chat-turn', () => runAgent(req.body.message), {
    userId: req.user.id,
    sessionId: req.body.conversationId,
    tags: ['production', 'v2-prompt'],
    metadata: { requestId: req.id, plan: req.user.plan },
  })

  res.json({ answer })
})

Prop

Type

Wrap the request handler, the job, or the agent entrypoint once — not every internal step. A nested capture joins the current trace rather than starting a second root: scalars are overridden by the innermost call, metadata is shallow-merged, and tags accumulate.

Pass the same sessionId across turns of one conversation and Harbor stitches them into a session, which is the unit trajectory scoring runs on.

The traced work must happen inside the callback. For a streaming response, create and consume the stream there — once the callback returns, the context is no longer active and later spans lose it.

For control flow a callback cannot wrap — middleware where start and end are separate hooks — use the lifecycle form and end every scope you start. Spans created outside scope.run() are not children of the capture.

const scope = capture.start('request', { userId: user.id })
try {
  await scope.run(() => handle(request))
} catch (error) {
  capture.end(scope, error)
  throw error
}
capture.end(scope)

Trace your own code

harbor.tracer is a plain OpenTelemetry Tracer, so there is no Harbor-specific span API to learn. Spans you start inherit the enclosing capture context like any other.

import { harbor } from './telemetry'

const plan = await harbor.tracer.startActiveSpan('plan-steps', async (span) => {
  const steps = await buildPlan(question)
  span.setAttributes({
    'gen_ai.operation.name': 'invoke_agent',
    'gen_ai.output.messages': JSON.stringify(steps),
  })
  span.end()
  return steps
})

OpenTelemetry attribute values must be scalars or arrays of scalars, so anything structured has to be a JSON string. A span that never ends is never exported.

Auto-instrumentation

Pass any OpenTelemetry instrumentation and the client registers it. Because an instrumentation patches a library when that library is loaded, new Harbor() has to run before the module it wraps — which is why the setup belongs in its own module, imported first.

src/telemetry.ts
import { OpenAIInstrumentation } from '@arizeai/openinference-instrumentation-openai'
import { Harbor } from 'harbor-ai-sdk'

export const harbor = new Harbor({
  instrumentations: [new OpenAIInstrumentation()],
})

Harbor reads whatever conventions the instrumentation emits, so any package following the GenAI, OpenInference or OpenLLMetry attribute names works without an adapter here.

Flush before the process exits

Spans are batched. Long-running servers need nothing extra, but scripts, jobs, tests and serverless handlers must flush or the last batch dies with the process.

try {
  await runAgent()
} finally {
  await harbor.flush()
}

flush() never throws, so it is safe in a finally — a failed export is logged, not raised into the error you were already handling. The client also flushes on beforeExit, which covers a script running out of work but not process.exit() or a signal; call harbor.shutdown() on those paths.

Options

OptionDefaultDescription
apiKeyprocess.env.HARBOR_API_KEYIdentifies your organization and project
baseUrlprocess.env.HARBOR_BASE_URLSpans are posted to <baseUrl>/v1/traces
instrumentations[]OpenTelemetry instrumentations to register
MemberDescription
harbor.readyResolves once tracing is wired up
harbor.enabledfalse when the key or base URL is missing
harbor.tracerOpenTelemetry Tracer for your own spans
harbor.flush()Export everything buffered now
harbor.shutdown()Flush, then tear down. Idempotent

Bring your own OpenTelemetry

If something else already owns the tracer provider — your own NodeSDK, or an APM agent — do not construct Harbor; OpenTelemetry allows one provider and the second registration loses. Add Harbor's span processor to that setup instead, and your spans go to both backends.

src/telemetry.ts
import { NodeSDK } from '@opentelemetry/sdk-node'
import { HarborSpanProcessor } from 'harbor-ai-sdk'

export const harborSpanProcessor = new HarborSpanProcessor()

const sdk = new NodeSDK({
  spanProcessors: [harborSpanProcessor],
})

sdk.start()

HarborSpanProcessor takes the same apiKey and baseUrl options, applies capture context, and exposes forceFlush(). Export it so you can flush where you need to.

Troubleshooting

On this page