Skip to main content

TypeScript SDK

Use @latitude-data/telemetry to send LLM traces from TypeScript and JavaScript applications to Latitude. The SDK is built on OpenTelemetry and can attach to an existing tracing setup when your app already uses one. No Latitude account yet? Your agent can create a temporary one and do this whole setup with the latitude-setup skill, no signup.
Using an agent? Install the Latitude skills and let it handle the setup below. latitude-setup instruments your app or agent harness, verifies traces arrive, creates a temporary account if you don’t have one yet (no signup), and ends by building your first Artifact.

Installation

You need a Latitude API key and a project slug. No account yet? Your agent can create a temporary one with the latitude-setup skill, no signup.
Provider instrumentation implementations are included with @latitude-data/telemetry. Importing a factory from an opt-in subpath keeps unused instrumentations out of your application bundle.

Bootstrap

Initialize Latitude once before your LLM calls run. Import an instrumentation factory from its opt-in subpath and pass the created instance through instrumentations.
new Latitude() returns immediately. Await latitude.ready before creating LLM clients or making calls.

See what was captured

Once a real run has landed, your agent builds your first Artifact: a single HTML page, in the Latitude look, with everything the telemetry captured from that session: model calls, tool calls, tokens, cost, timing, and the conversation as the model saw it. It is the fastest way to check the integration end to end and to see what Latitude will have to work with. The latitude-setup skill does this as its last step from its bundled first-artifact.html template, filling the page with the values the latitude CLI returns for the trace, and adds a Claim your workspace button when the account is temporary. If you set things up by hand, the same template and instructions live in the skills repo. Prompt, if you need to ask for it:

Add context with capture()

Auto-instrumentation creates spans for supported LLM calls. Use capture() to attach Latitude context to the spans created inside a request, conversation turn, or agent run. You can use capture() to:
  • group traces by user
  • group traces into a session
  • route traces to a specific project
  • add tags and metadata for filtering
  • mark the boundary of an agent run
capture() does not create spans by itself. It only adds context to spans created by auto-instrumentation inside the callback. In most apps, wrap the outer request handler, conversation turn, or agent entrypoint once. If callback wrapping does not fit your control flow, use lifecycle mode:
Nested capture() calls inherit parent context and can override local values. Metadata is shallow-merged, and tags are appended and deduplicated.

Bring your own cost

By default Latitude prices each LLM call from its token counts using public model prices. If you pay a negotiated rate, use a fine-tuned or self-hosted model, or already know what each call cost, tell the SDK and Latitude uses your figure instead. All amounts are in USD. There are four ways to set it. When more than one applies to the same span, the first in this list wins:
  1. setLlmCost(span, cost): the cost of one specific span you hold.
  2. capture(name, fn, { cost }): a cost per LLM call, applied to every LLM call inside that capture.
  3. costResolver: a function that prices each LLM call.
  4. pricing: a per-model price table.
If none of them sets a cost, Latitude prices the span itself, as before.
A cost is either { input, output }, { total }, or all three. When you leave out total, the SDK sets it to input + output. An explicit 0 is a real cost of zero, not “unset”. Negative or non-numeric amounts are ignored with a warning.
capture(name, fn, { cost }) is a cost per LLM call. It is applied to every LLM call inside the capture, not split across them, so a capture that makes 3 LLM calls records 3× the cost. If the calls inside a capture cost different amounts, use pricing or costResolver to price each call, or setLlmCost() for one specific span.
The capture’s own wrapper span never gets a cost. Nested captures inherit the cost unless they set their own. Cost does not travel through injectTraceContext() carriers, so set it on each side. setLlmCost() writes onto a live span, typically one you created yourself:

What the SDK reads and writes

The SDK only prices LLM-call spans: spans whose gen_ai.operation.name is chat, text_completion, generate_content, embeddings or rerank/reranker, or the equivalent OpenInference (openinference.span.kind LLM/EMBEDDING/RERANKER), OpenLLMetry (llm.request.type) or Vercel AI SDK leaf (ai.*.doGenerate/doStream/doEmbed) spans, plus CrewAI’s AGENT span, which carries its LLM usage. setLlmCost() applies to whatever span you pass it. For costResolver and pricing it reads the fields below. costResolver receives them as an LlmUsage with provider, model, inputTokens, outputTokens, operation, spanName and attributes; any of the first four can be undefined. pricing tries the response model first, then the requested model. It needs a provider, a matching model and at least one token count, and treats a missing token count or rate as 0. Cache and reasoning tokens are not priced separately. Use costResolver if you need that; it also receives the span’s raw attributes. If costResolver throws, the SDK logs a warning and falls back to pricing. Cost is resolved when each span ends, before redaction, so costResolver runs on your application’s thread; keep it fast and side-effect free. Where the SDK sets a cost it writes the standard gen_ai.usage.input_cost, gen_ai.usage.output_cost and gen_ai.usage.total_cost attributes plus latitude.cost.source = "user" (exported as ATTRIBUTES.costInput, costOutput, costTotal, costSource and COST_SOURCE_USER). It replaces any cost your instrumentation already wrote on that span, including a total-only cost removing the instrumentation’s input and output costs so the numbers stay consistent. Spans the SDK doesn’t price keep whatever cost the instrumentation wrote. The cost is written as the span is exported to Latitude, so other exporters on the same OpenTelemetry provider see the span unchanged.

Existing Sentry or OpenTelemetry setup

If your app already uses Sentry, Datadog, New Relic, Honeycomb, or another OpenTelemetry-compatible SDK, initialize that SDK first and construct Latitude second. Latitude will attach its span processor to the existing provider when possible.
If Sentry’s automatic OpenTelemetry setup conflicts with Latitude tracing, setting skipOpenTelemetrySetup: true in Sentry.init() can help by preventing Sentry from configuring OpenTelemetry itself. This disables Sentry’s automatic tracing and span emission; error reporting remains available, but sending traces to Sentry requires manually wiring Sentry’s OpenTelemetry components. latitude.shutdown() only shuts down Latitude-owned processing. It does not shut down your existing observability SDK. If you need lower-level OpenTelemetry wiring or a non-TypeScript runtime, see the OpenTelemetry Exporter guide.

Supported integrations

Import each factory from its opt-in subpath under @latitude-data/telemetry/instrumentations/ and pass the SDK module your app imports. Frameworks that run their own OpenTelemetry (Vercel AI SDK, Cloudflare Think, Flue, LiveKit, Mastra, Eve) and providers without a TypeScript factory are covered by their own pages in the Getting Started sidebar, or by the OpenTelemetry exporter.

Troubleshooting

Spans are not appearing in Latitude

Start with the most common setup issues.

Check the API key and project slug

Make sure both values are present in the runtime where your app is executing:
If either value is missing or points to the wrong organization/project, Latitude cannot route the spans to your project.

Pass the same SDK module your app uses

The module passed to instrumentations should be the same package import used for the actual LLM call.
Avoid importing one SDK module for instrumentation and using a different wrapper or separately loaded copy for the LLM call.

Flush before short-lived processes exit

Servers can usually export spans in the background. Scripts, CLIs, tests, and jobs that exit immediately should flush before shutdown:

Wrap the actual LLM call with capture()

If you use capture(), the instrumented operation must happen inside the callback:
This will not attach context to the LLM call, because the call happens before capture() starts:

Consume streaming responses inside capture()

For streaming responses, create and consume the stream inside the capture() callback. This keeps the full streamed operation inside the active OpenTelemetry context.
Avoid returning the stream from capture() and consuming it later. Once the callback has finished, the Latitude context is no longer active for the remaining stream consumption.

No spans are created inside capture()

capture() only attaches context. You still need a supported instrumentation, and the code inside the callback must make an instrumented LLM call.

Context is not propagating

new Latitude() registers the OpenTelemetry context manager automatically. If you provide your own OpenTelemetry setup, make sure it has a working context manager before Latitude attaches to it.