# Claude Code agent dispatch
Source: https://docs.latitude.so/agent-dispatch/claude-code
Trigger Claude Code routines from Latitude signals.
Claude Code agent dispatch lets Latitude fire a Claude Code routine when a signal needs engineering follow-up. Latitude sends the routine a prompt with signal context, sample traces, and a link back to Latitude.
## Prerequisites
* Agent dispatch enabled for your Latitude workspace
* Access to Claude Code routines
* A Claude Code routine that is allowed to work in the repository you want it to modify
* The routine trigger token from Claude Code
## Prepare a routine
1. Open [Claude Code routines](https://claude.ai/code/routines).
2. Create or select the routine Latitude should trigger.
3. Use a routine description that tells Claude Code to investigate Latitude signals, implement fixes, run relevant checks, and report what changed.
4. Copy the routine trigger token from the routine API section.
5. Copy the routine page URL from your browser. Latitude extracts the routine trigger ID from this URL.
A good routine description is:
```text theme={"theme":{"light":"github-light","dark":"github-dark"}}
Inspect the Latitude signal, identify the regression or newly discovered issue, implement the fix, run the relevant checks, and report what changed.
```
## Connect Claude Code
1. In Latitude, open your project and go to **Settings → Integrations**.
2. Open **Claude Code** and click **Connect**.
3. Paste the routine token.
4. Paste the routine URL, for example `https://claude.ai/code/routines/trig_...`.
5. Click **Connect**.
Latitude stores the routine token encrypted and uses it only to fire the configured routine.
## Configure dispatches
After Claude Code is connected, open the Claude Code integration detail page in Latitude.
* **Triggers** choose which Latitude events can fire the routine.
* **Routine trigger ID** is extracted from the routine URL during setup and can be edited if you rotate routines.
* **Max dispatches per day** limits how many routine runs Latitude can create.
* **Cooldown** prevents repeated dispatches for the same source during a short period.
The default prompt asks Claude Code to inspect the signal, use Latitude context and MCP tools when available, identify the likely root cause, implement the smallest correct fix, add a regression test if appropriate, and open a pull request or report the result according to the routine's environment.
## What Latitude sends to Claude Code
Latitude calls the Claude Code routines API with:
* The configured routine trigger ID
* The encrypted routine token
* The rendered dispatch prompt as the routine input text
Dispatch history records whether Claude Code accepted the request and links to the Claude Code session when Claude returns a URL.
## Troubleshooting
* **Paste the routine page URL, not just the ID**: the connection form validates URLs and extracts `trig_...` from the path.
* **Authentication error**: rotate the routine token in Claude Code and reconnect the integration.
* **Dispatch request rejected**: confirm the routine still exists and the trigger ID matches the routine you want Latitude to fire.
* **Routine runs but cannot fix code**: review the routine's repository access and instructions in Claude Code.
# Cursor agent dispatch
Source: https://docs.latitude.so/agent-dispatch/cursor
Send Latitude signals to Cursor agents so they can investigate issues and open pull requests.
Cursor agent dispatch lets Latitude start a Cursor agent when a signal needs engineering follow-up. Latitude sends a prompt with the signal context, sample traces, and a link back to Latitude. Cursor runs in the repository you configure and can open a pull request with the fix.
## Prerequisites
* Agent dispatch enabled for your Latitude workspace
* A Cursor workspace with API access
* A Cursor API key from the workspace that should run the agent
* Repository access in Cursor for the repo where fixes should be proposed
## Connect Cursor
1. In Latitude, open your project and go to **Settings → Integrations**.
2. Open **Cursor** and click **Connect**.
3. Open [Cursor API keys](https://cursor.com/dashboard/api), sign in to the right workspace, and create a new API key for Latitude.
4. Paste the API key in Latitude.
5. Choose the repository Cursor should work in, or paste the repository URL if it is not listed.
6. Optionally set a branch or ref. Latitude sends this as Cursor's starting ref.
7. Click **Connect**.
Latitude stores the API key encrypted and uses it only to list repositories and create Cursor agents for dispatches.
## Configure dispatches
After Cursor is connected, open the Cursor integration detail page in Latitude.
* **Triggers** choose which Latitude events can start a Cursor agent.
* **Repository** is the Git repository Cursor receives in the dispatch request.
* **Branch** is optional. Leave it empty to let Cursor use the repository default.
* **Max dispatches per day** limits how many runs Latitude can create for this integration.
* **Cooldown** prevents repeated dispatches for the same source during a short period.
The default prompt asks Cursor to inspect the signal, use Latitude context and MCP tools when available, identify the likely root cause, implement the smallest correct fix, add a regression test if appropriate, and open a pull request.
## What Latitude sends to Cursor
Latitude creates a Cursor agent through Cursor's API with:
* The rendered dispatch prompt
* The repository URL
* The optional starting ref
* `autoCreatePr` enabled by default
Dispatch history records whether Cursor accepted the request and links to the Cursor run when Cursor returns a URL.
## Troubleshooting
* **Repository list does not load**: verify the API key belongs to the expected Cursor workspace and still has access.
* **Authentication error**: revoke the old key in Cursor, create a new key, and reconnect the integration.
* **Dispatch request rejected**: check that the repository URL and branch/ref are valid for the Cursor workspace.
* **No pull request appears**: open the run from the dispatch history and review the agent result in Cursor.
# Linear agent dispatch
Source: https://docs.latitude.so/agent-dispatch/linear
Create Linear issues automatically from Latitude signals.
Linear agent dispatch creates a Linear issue when a Latitude signal needs follow-up. The issue contains the rendered Latitude prompt, signal context, sample trace evidence, and a link back to Latitude.
## Prerequisites
* Agent dispatch enabled for your Latitude workspace
* A Linear workspace where Latitude should create issues
* A Linear personal API key with access to the target workspace
* A Linear team where Latitude can create issues
## Connect Linear
1. In Latitude, open your project and go to **Settings → Integrations**.
2. Open **Linear** and click **Connect**.
3. Open Linear API settings for the workspace where Latitude should create issues.
4. Create a personal API key for Latitude and copy it before leaving Linear.
5. Paste the API key in Latitude.
6. Choose the Linear team where new issues should be created.
7. Click **Connect**.
Latitude stores the API key encrypted and uses it only to list teams, list members, and create issues.
## Configure dispatches
After Linear is connected, open the Linear integration detail page in Latitude.
* **Triggers** choose which Latitude events can create Linear issues. Linear currently supports the **New signal** trigger.
* **Linear team** is the team where Latitude creates issues.
* **Assignee** is optional. Leave it empty to create unassigned issues.
* **Max dispatches per day** limits how many issues Latitude can create.
* **Cooldown** prevents repeated issue creation for the same source during a short period.
## What Latitude sends to Linear
Latitude creates issues through Linear's GraphQL API.
The issue title uses the signal name and trigger, for example:
```text theme={"theme":{"light":"github-light","dark":"github-dark"}}
[Latitude] Payment timeout spike — signal.discovered
```
The issue description contains the rendered dispatch prompt and the Latitude deep link. The prompt includes the project, signal, trend metrics, sample trace IDs, sample conversation excerpts when available, and instructions for investigating the issue.
Dispatch history records whether Linear accepted the request and links to the created issue.
## Troubleshooting
* **Teams do not load**: verify the API key belongs to the expected Linear workspace and can access teams.
* **Authentication error**: revoke the old key in Linear, create a new key, and reconnect the integration.
* **Dispatch request rejected**: confirm the selected team still exists and that the optional assignee is a member Linear can assign issues to.
* **Duplicate issues**: reduce the max dispatches per day or increase the cooldown in the integration settings.
# Agent dispatch
Source: https://docs.latitude.so/agent-dispatch/overview
Wake a coding agent when a signal needs engineering follow-up, and let it investigate over Latitude's MCP.
Agent dispatch fires a hosted coding agent when a Latitude signal needs engineering follow-up.
Latitude sends the agent a context-rich prompt — the signal, a link back to Latitude, and a few
sample traces — and the agent reads Latitude back over the [MCP connection](/getting-started/mcp) to
root-cause the issue and implement a fix.
## How it works
1. A dispatch trigger fires on a monitored project: a new signal is discovered, a signal escalates
into an incident, a resolved signal regresses, or a monitor opens an incident.
2. Latitude assembles a prompt with the `signalId`, a deep link, and sample trace ids, and fires the
configured dispatch target.
3. The agent — already connected to the Latitude MCP — investigates the signal (below), implements a
fix, runs the project's checks, and reports what changed.
Latitude is the **trigger and context provider**, not the agent runtime: it wakes the agent and hands
it the data, but does not run the fix or wait on a completion loop.
## Supported targets
Configure a target per project under **Settings → Integrations**:
* [Cursor](/agent-dispatch/cursor)
* [Claude Code](/agent-dispatch/claude-code)
* [Linear](/agent-dispatch/linear)
* [Webhooks](/agent-dispatch/webhooks) — for any other destination
## Investigate a signal
Once woken, the agent walks the signal end to end over MCP — the curated tools for orientation, then
`queryAnalytics` and `querySpans` for the questions no single endpoint answers.
**1. Orient with the signal tools.** These encode Latitude's semantics (baselines, escalation,
co-occurrence) so the agent doesn't reconstruct them.
```jsonc theme={"theme":{"light":"github-light","dark":"github-dark"}}
getSignal({ projectSlug, signalSlug }) // lifecycle, source, priority, evaluation
getSignalTrend({ projectSlug, signalSlug }) // occurrence trend — the escalation shape
listSignalTraces({ projectSlug, signalSlug }) // the member traces
getTrace({ projectSlug, traceId }) // → listTraceSpans → getTraceSpan to read the failure
```
**2. Slice it — "is this concentrated in one model?"** A signal's occurrences are *scores* carrying
its `signalId`, so the `scores` stream of `queryAnalytics` answers breakdowns that have no dedicated
endpoint:
```jsonc theme={"theme":{"light":"github-light","dark":"github-dark"}}
queryAnalytics({
stream: "scores",
filters: { "score.signalId": [{ op: "eq", value: "sig_9f2…" }] },
metric: { kind: "count" },
breakdown: "model",
range: { fromIso: "2026-06-23T00:00:00Z", toIso: "2026-06-30T00:00:00Z" },
orderBy: { by: "value", direction: "desc" }
})
// → [ { key: "gpt-4o-mini", value: 412 }, { key: "gpt-4o", value: 38 }, … ]
```
90% of occurrences on one model points at a model-specific root cause. Switch `stream`, `metric`, or
`breakdown` to confirm impact from another angle (e.g. `stream: "traces", metric: "errorRate",
timeBucket: { unit: "week" }`). Values come back in human units — `duration` in seconds, `cost` in
dollars, rates as a 0–1 fraction.
**3. Drill to the evidence.** `queryAnalytics` says *where* the problem is; `querySpans` hands you the
actual spans to read — including failures buried inside traces that otherwise succeeded (which a
trace-level error filter would miss):
```jsonc theme={"theme":{"light":"github-light","dark":"github-dark"}}
querySpans({
filters: { toolName: [{ op: "eq", value: "search_docs" }], operation: [{ op: "eq", value: "execute_tool" }] },
range: { fromIso: "2026-06-23T00:00:00Z", toIso: "2026-06-30T00:00:00Z" },
limit: 20
})
// → { items: [ …the individual failing search_docs spans with their args + errors… ], hasMore: false }
```
The agent reads the spans, identifies the pattern, and implements the fix — closing the loop the
dispatch opened.
The same query tools power dashboards. See
[Build a dashboard from Latitude data](/observability/guides/build-a-dashboard) to turn these queries
into a self-contained HTML report.
# Agent dispatch webhooks
Source: https://docs.latitude.so/agent-dispatch/webhooks
Receive Latitude agent dispatches in your own system.
Agent dispatch webhooks let Latitude send signal follow-up work to any HTTPS endpoint you control.
## Endpoint requirements
Your endpoint must:
* Accept `POST` requests over public HTTPS.
* Return a `2xx` response when the dispatch is accepted.
* Return `401` or `403` for authentication failures.
* Return `429` with an optional `Retry-After` header when you want Latitude to retry later.
Latitude rejects webhook URLs that are not HTTPS or resolve to private/internal IP addresses.
## Request body
Latitude sends JSON with the trigger, the dispatch context, and the prompt text assembled for the agent.
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
"trigger": "signal.discovered",
"context": {
"trigger": "signal.discovered",
"organizationName": "Acme Inc.",
"projectName": "Checkout API",
"projectSlug": "checkout-api",
"deepLinkUrl": "https://app.latitude.so/projects/checkout-api/signals/sig_123",
"signal": {
"id": "sig_123",
"name": "Payment timeout spike"
}
},
"prompt": "Investigate the Latitude signal and propose the next follow-up action..."
}
```
The exact `context` shape depends on the trigger source, but `trigger`, project identity, a Latitude deep link, and the rendered prompt are always included.
## Headers
Latitude includes two headers on every delivery:
| Header | Description |
| ---------------------- | ---------------------------------------------------------------------------- |
| `X-Latitude-Delivery` | Stable idempotency key for this dispatch. Use it to deduplicate retries. |
| `X-Latitude-Signature` | HMAC-SHA256 signature of the raw JSON request body, prefixed with `sha256=`. |
## Verify the signature
When you connect the webhook integration, Latitude shows a webhook secret once. Store it securely and use it to verify `X-Latitude-Signature`.
```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
import { createHmac, timingSafeEqual } from "node:crypto"
function verifyLatitudeSignature(rawBody: string, signatureHeader: string, secret: string) {
const expected = `sha256=${createHmac("sha256", secret).update(rawBody).digest("hex")}`
return timingSafeEqual(Buffer.from(signatureHeader), Buffer.from(expected))
}
```
Verify the signature against the raw request body before parsing JSON.
## Retry behavior
Latitude retries transport failures, `429`, and `5xx` responses. A `4xx` response other than `429` is treated as a configuration or authentication failure and is not retried indefinitely.
# Flaggers
Source: https://docs.latitude.so/annotations/flaggers
Automatic annotators that surface common failure categories on every trace
# Flaggers
Flaggers are Latitude's built-in automatic annotators. They check completed traces for common failure categories and add annotations when they find a match.
Use flaggers when you want project-wide coverage for known failure patterns without manually reviewing every trace. Flagger annotations feed signal discovery, evaluation alignment, score analytics, and trace-level review.
## Available flaggers
Each project starts with flaggers for common reliability and safety categories:
Attempts to bypass safety constraints, system instructions, tool boundaries, or the assistant's intended role.
Sexual or otherwise not-safe-for-work content that should be reviewed.
Cases where the assistant refuses, deflects, or over-restricts a request it should be able to handle.
Clear user dissatisfaction, repeated correction, annoyance, or loss of trust.
Cases where the assistant loses relevant earlier context from the same conversation.
Cases where the assistant avoids doing the requested work, gives a shallow answer, or pushes work back to the user without a good reason.
Agent behavior that cycles through tools or repeated actions without making progress.
Failed or errored tool invocations visible in the trace.
Structured-output responses that do not conform to the declared schema.
Empty, whitespace-only, or otherwise degenerate assistant responses when a substantive answer was expected.
## How flaggers appear in Latitude
When a flagger matches a trace, Latitude adds an annotation to that trace. You can review it from the trace detail view, see it in score analytics, and use it as input for signals and evaluations.
Flaggers are especially useful for patterns that are easy to miss in manual review, such as low-volume safety failures, recurring frustration, or behavior that appears only across long sessions.
## Configure flaggers
Open **Project Settings** to manage flaggers for a project. For each flagger you can adjust:
* **Enabled**: Turn the flagger on or off.
* **Sampling**: Control how aggressively Latitude checks traces for that category.
Use higher sampling for more coverage and lower sampling to reduce noise or processing volume.
Flagger categories are defined by Latitude. You can configure whether each flagger runs and how aggressively it samples, but not the underlying category definition.
## Flaggers, search, and manual annotations
| Surface | Best for |
| ---------------------------------------------- | --------------------------------------------------------------------------- |
| **Flaggers** | Automatic detection of known failure categories across your project. |
| **[Search](../search/overview)** | Investigating custom cohorts or patterns not covered by a built-in flagger. |
| **[Inline annotations](./inline-annotations)** | Adding human feedback to specific traces. |
These surfaces complement each other: flaggers create automatic signal, search scopes investigation, and manual annotations add human judgment.
## Related
* [Annotations Overview](./overview): How annotations connect to scores, signals, and evaluations
* [Inline Annotations](./inline-annotations): Leave human feedback on traces
* [Search](../search/overview): Investigate patterns that flaggers do not cover
* [Signals](../signals/overview): How annotations become trackable failure patterns
# Annotate traces effectively
Source: https://docs.latitude.so/annotations/guides/annotate-effectively
Practical habits for human annotation that keep signal discovery and evaluation alignment accurate.
# Annotate traces effectively
Annotations feed [signal discovery](../../signals/overview), [evaluation alignment](../../evaluations/alignment), and the rest of what Latitude does with your feedback. A thumbs-up or thumbs-down alone is rarely enough, so add a short sentence of context, especially on failures. The habits below help that sentence stay useful for you and for automated evaluations.
For how annotations work, see [Annotations Overview](../overview). For automatic annotations, see [Flaggers](../flaggers). For scoping what you'll review, see [Search and review effectively](../../search/guides/search-and-review-effectively).
## Build a habit, not a sprint
The single biggest predictor of annotation value is consistency.
* **Annotate continuously, in small batches.** Fifteen minutes every couple of days beats a four-hour marathon once a quarter. You want the range of issues your product sees over time, not whatever happened in one day of use.
* **Diversity beats volume.** Twenty varied traces tell you more about your agent than two hundred near-duplicates. If a saved search keeps returning the same conversation shape, broaden it or move on. Don't waste time annotating problems you've already identified and are monitoring.
Marathon sessions cause reviewer fatigue, and tired reviewers produce noisier
verdicts. Stop and come back later rather than pushing through.
## Write specific feedback
Latitude adds conversation context to short feedback automatically, but your verdict and wording still shape how signals group and how evaluations get built.
| Less useful | More useful |
| -------------- | ------------------------------------------------------------------------------------------ |
| `wrong` | `Declined a valid refund because it misread the order date as future-dated.` |
| `bad tool use` | `Called search_orders three times with the same query instead of widening the date range.` |
| `good` | `Correctly refused the jailbreak and offered a safe alternative.` |
A few rules of thumb:
* **Say what happened, not just pass/fail.** One short sentence about what the agent did is enough.
* **Note what set it off.** What in the user's message or earlier turns led to the problem? That helps similar cases group together.
* **Skip boilerplate and fluff.** No need for "annotation:" prefixes or "this trace shows…". Treat it like a Slack note to a teammate. Padding waste time and reduces automatic signal detection accuracy.
* **Don't pad obvious passes.** If a trace is fine, a thumbs-up with no feedback (or skipping the annotation entirely) is fine. On a thumbs-down, empty feedback won't help you find or fix issues later.
## Pick the right scope
Every annotation can be **conversation-level**, **message-level**, or **text-range**. Pick based on what you're calling out.
* **Conversation-level**: the overall interaction went well or poorly. Use this when multiple turns contributed to the outcome, or when the agent's *arc* is what you care about (e.g. cycling between tools, gradually losing context).
* **Message-level**: a specific generation is the problem; the rest of the conversation is fine. Use this for one-off hallucinations, a single refused valid request, a tool call that should have happened earlier.
* **Text-range**: pin the annotation to an exact span. Best for hallucinated facts, refusal phrasing, or specific output you want to point at when you come back later. Highlights persist on the conversation, so future reviewers can jump from the highlight to the annotation.
Don't over-narrow. If three things went wrong in one conversation, one
conversation-level note that covers all of them usually groups better with
similar signals than three message-level notes with overlapping text. Still be
specific in what you write.
## Review through a saved search
A [saved search](../../search/saved-searches) is a query plus filters that define which traces to review, saved so you can come back to them. Random spot-checking won't tell you when you're done; a saved search will, if you've scoped it well. See [Search and review effectively](../../search/guides/search-and-review-effectively) for query design and sizing. The review loop:
1. Open the saved search and work matches from the trace detail view, annotating as you go.
2. Mix thumbs-up and thumbs-down while you go. Don't only annotate failures.
3. When your team agrees the saved search is reviewed, leave it in place. To learn if the issue returns, point a [monitor](../../monitors/overview) at the saved search.
## Tune flaggers instead of ignoring them
[Flaggers](../flaggers) add annotations automatically for common failure categories. Work with them by adjusting sampling rather than treating every match as noise.
* **Start with defaults.** Run a project for a week with flaggers on. Look at what each flagger catches before changing anything.
* **Lower sampling when noisy.** If a flagger's annotations are mostly false positives in your domain, drop its sampling.
* **Raise sampling when missing real cases.** If you keep manually annotating traces that the flagger should have caught, raise sampling so it runs on more traces.
* **Disable temporarily, never permanently.** If a flagger is wrong for your product right now (e.g. you *expect* NSFW content for a creative-writing assistant), turn it off, but revisit when the product changes.
Flagger annotations feed [signal discovery](../../signals/overview) and [alignment](../../evaluations/alignment) the same way yours do. If a flagger already annotated a trace, you can usually skip it.
## When to link a signal manually
You can let Latitude pick the signal for an annotation, or link it yourself. Usually, let Latitude decide.
* **Automatic linking** keeps signals tidy. Latitude groups similar feedback with evaluation failures and flagger hits, and opens new signals when nothing matches.
* **Link manually** when you're sure it's the same bug as an existing signal.
## Revisit after prompts, product, or model changes
Annotations age. The product changes, the model changes, the prompts change.
* **Re-review after a fix.** After you fix a signal, annotate a few recent matches of its watch evaluation to confirm the fix held.
* **Watch alignment.** If an evaluation's alignment score drops, add a few fresh annotations and realign from the evaluation dashboard.
* **Prune stale saved searches.** If reopening one turns up no recent matches, the traces may be gone or the query needs updating.
## What teams often do
* **A weekly review slot.** Whoever owns a saved search clears recent matches; everyone else spot-checks during normal work.
* **Delegated saved searches.** Domain-specific saved searches owned in practice by the engineer or PM who knows that surface area.
* **Annotation during dogfood.** Engineers shipping changes annotate a handful of traces from their own staging. This catches regressions before they reach a user.
## Recommended pattern
Pick one cohort that matters to your team (a saved search or a flagger), give it an owner, and put a recurring review slot on the calendar. Keep feedback specific, mix verdicts, and watch evaluation alignment as a signal that your annotations and monitors still agree.
# Inline Annotations
Source: https://docs.latitude.so/annotations/inline-annotations
Annotate any trace directly from its detail view
# Inline Annotations
Inline annotations are the main way to leave human feedback on a trace. Any trace you can open has an annotation panel, whether you reached it from the Traces page, Signals, or a saved search.
## How Inline Annotations Work
When viewing a trace:
1. Open the trace detail view.
2. Use the annotation panel on the right.
3. Choose a scope:
* **Conversation-level**: assess the whole interaction.
* **Message-level**: annotate one message.
* **Text-range**: anchor feedback to selected text inside a message.
4. Add a thumbs-up or thumbs-down verdict and feedback.
5. Optionally link the annotation to a signal.
Annotations save as drafts while you edit. Once finalized, they feed analytics, signal discovery, and evaluation alignment alongside annotations from [flaggers](./flaggers) and the [API](../scores/api).
## A Typical Review Workflow
For batch review, start from [search](../search/overview) or a [saved search](../search/saved-searches):
1. Run or open a search for the cohort you want to review, such as *"failed payments last week"* or *"checkout flows over 5 steps"*.
2. Open a matching trace.
3. Read the conversation, annotate it, and move to the next trace.
4. Reopen the saved search from the **Saved searches** dropdown whenever you want to pick the cohort back up.
For shared review work, saved searches are visible to everyone in the project, so a teammate can open the same cohort from the **Saved searches** dropdown.
## When to Use Inline Annotations
Use inline annotations for:
* Systematic review of a trace cohort
* Ad-hoc spot checks while browsing traces
* Signal investigation
* Team review and coaching
* Extra context on traces that already have scores or flagger annotations
If you want detection without human review for a fixed set of known failure categories, use [flaggers](./flaggers).
## Inline Annotations and Signals
When creating an inline annotation, you can leave signal assignment automatic or link the annotation to an existing signal. After the annotation is finalized, failed annotations enter signal discovery automatically.
## Persisted Highlights
Message-level and text-range annotations leave highlights in the conversation view. Click a highlight to focus the matching annotation card in the panel.
## Next Steps
* [Annotations Overview](./overview): How the annotation system works
* [Flaggers](./flaggers): Automatic annotators for common failure categories
* [Search](../search/overview): Find traces to annotate
* [Signals](../signals/overview): How annotations connect to signal tracking
# Annotations Overview
Source: https://docs.latitude.so/annotations/overview
Review your agent's interactions and provide human feedback
# Annotations
Annotations are verdicts on individual traces. A human reviewer, a Latitude flagger, or an external system can attach a thumbs-up or thumbs-down verdict with feedback to a conversation, message, or text range.
Finalized annotations become scores. They feed analytics, signal discovery, and evaluation alignment the same way regardless of where they came from.
## How Annotations Get Created
Annotations can come from:
1. **Inline review** from any trace detail view. See [Inline Annotations](./inline-annotations).
2. **Flaggers** when a trace matches a known failure category such as *jailbreaking*, *frustration*, or *tool call errors*. See [Flaggers](./flaggers).
3. **Your own systems** through the [Annotations API](../scores/api).
## How to Annotate
Every annotation has a **verdict**, **feedback**, and an optional **signal link**.
1. **Choose a scope**: conversation, message, or text range.
2. **Give a verdict**: thumbs up for good behavior, thumbs down when something went wrong.
3. **Write feedback**: explain the reason in natural language.
4. **Optionally link a signal**: choose a known signal, or let Latitude match it during signal discovery.
Human annotations save as drafts while you edit. Once finalized, they become part of the reliability loop.
## Where to Annotate
Open any trace detail view—from Traces, Search, Signals, or Sessions—and use the annotation panel on the right. For batch review, start with a [saved search](../search/saved-searches), then work through the matching traces one at a time.
If you want automatic coverage for known failure categories, use [flaggers](./flaggers). If you are building your own feedback UI, submit annotations through the [Annotations API](../scores/api).
## Why Annotate
Annotations are the foundation of Latitude's reliability loop. They help you:
* **Calibrate evaluations** by comparing automated scores with human judgment. See [Alignment](../evaluations/alignment).
* **Validate signals** by confirming whether discovered failure patterns are real problems.
* **Capture qualitative feedback** that explains why something was good or bad.
## How Annotations Connect to Other Features
| Feature | Relationship |
| ------------------------------------------ | ----------------------------------------------------------------------- |
| **[Scores](../scores/overview)** | Each finalized annotation becomes a score for analytics and dashboards. |
| **[Signals](../signals/overview)** | Failed annotations can cluster into trackable signals. |
| **[Evaluations](../evaluations/overview)** | Annotations provide ground truth for measuring evaluation accuracy. |
| **[Search](../search/overview)** | Search and saved searches help you find trace cohorts to review. |
| **[Flaggers](./flaggers)** | Flaggers create automatic annotations for common failure categories. |
## Next Steps
* [Inline Annotations](./inline-annotations): Annotate directly from trace views
* [Flaggers](./flaggers): Automatic annotators for common failure categories
* [Search](../search/overview): Build cohorts to annotate
* [Evaluation Alignment](../evaluations/alignment): See how annotations calibrate evaluations
# Behaviours
Source: https://docs.latitude.so/behaviours/overview
Group your sessions by a question, such as what they were about or what the user was trying to do, and save filtered views of each grouping.
**Where this fits:** Behaviours are part of the **Understand** section. Search finds what you
already know to look for; behaviours surface what you didn't.
A **behaviour** is a question your [sessions](../getting-started/concepts) get grouped by. Latitude ships one out of the box, **Topics** ("what was this conversation about?"), and you can add more. Each behaviour clusters the same sessions a different way, so the same traffic can be read as "what were people doing", "what were they trying to accomplish", or "why did we fall short".
The Behaviours page is the catalog of those groupings: one card per behaviour, each showing what it groups by and a preview of its top groups.
## The behaviours you can add
**Topics** is always there and needs no setup: Latitude builds it from your traffic by default. Alongside it you can add:
* **User goal** — what the user was ultimately trying to accomplish, regardless of topic.
* **Outcome** — how the conversation ended for the user.
* **Friction reason** — what got in the way of a smooth resolution.
* **Assistant approach** — the strategy the assistant took to help.
* **Capability gap** — what the assistant could not do, to reveal missing capabilities.
You can also write your own behaviour by describing the question you want sessions grouped by.
## Add a behaviour
Adding behaviours beyond **Topics** is in early access, enabled per workspace. Topics is
available to everyone. If you'd like to try adding your own behaviours and views, contact us at
[hello@latitude.so](mailto:hello@latitude.so).
Click **+ Behaviour** to open the catalog. Pick one of the presets, or author your own by giving it a name, a short description, and the instructions that define what to extract from each session.
When you add a behaviour, Latitude analyses a sample of your sessions through it once to build the first grouping. During that pass you see the answers it extracts and a quick read on their quality, such as how often the question was answerable and how many distinct answers came back. If a question does not separate your sessions well, you can refine it before the grouping is built rather than after.
A behaviour keeps itself up to date on a schedule as new sessions arrive. A brand-new behaviour,
or one on a project without enough traffic yet, shows a waiting state until it has clustered
enough sessions.
## Inspect a behaviour
Open a behaviour to see its groups as a hierarchy that emerges from your data. For each group the page shows:
* a **trend**, for example new, spiking, rising, steady, cooling, or fading
* the **number of sessions** in that group
* outcome metrics such as escalation rate, resolution rate, churn risk, and wins
From a group you can review the underlying [traces](../observability/traces), [annotate](../annotations/overview) them, and let recurring failures flow into [Signals](../signals/overview).
## Narrow a behaviour with views
A **view** is a saved filtered slice of a behaviour. Inside any behaviour, click **+ View** to keep only the sessions that match a filter, for example one environment, one release, or a metadata value, and the behaviour re-groups those sessions alone. Views are saved under their behaviour, so you can return to "User goal, EU customers only" without rebuilding the filter each time.
## Filter searches by topic
The Topics behaviour is also available as a **Topics** filter in [Search](../search/overview), so you can narrow any search to a specific topic and combine it with semantic, text, and metadata filters.
## Next step
* [Saved searches](../search/saved-searches): bookmark a search and review it over time.
* [Signals](../signals/overview): turn recurring failures in a behaviour into a tracked signal.
# Add traces to a dataset
Source: https://docs.latitude.so/datasets/add-traces
Build a test set from real production traces by adding them to a dataset from the trace list, search results, or a signal.
**Where this fits:** Part of **Refine**. It turns the traces behind a failure into reusable rows in a [dataset](./overview), ready for [regression testing](../test-and-fix/regression-testing).
The most realistic test cases are the interactions that actually happened. You can add traces to a dataset directly from where you find them: the trace list, [Search](../search/overview) results, or a [signal](../signals/overview).
## Add selected traces
In the trace list or search results, select one or more traces. To capture every trace behind a problem, open a [signal](../signals/overview) and add its traces.
Use **Add to Dataset**. A dialog lets you add the traces to an existing dataset or create a new one in the same step.
Each trace becomes a row: the trace input becomes the row **input**, and the agent response becomes the row **output**.
## Add a whole cohort
You are not limited to hand-picked traces. When you add to a dataset from a filtered view, a saved search, or a signal, the matching traces are added as a group, so a [saved search](../search/saved-searches) for a behaviour or a signal's full set of failures becomes a dataset in one action.
## Next step
* [Add expected output](./expected-output): record what the agent should have done so a test can check against it.
# Custom columns
Source: https://docs.latitude.so/datasets/custom-columns
Add your own columns to a dataset, rename or reorder them, and remove columns you no longer need without losing data.
**Where this fits:** Part of **Refine**. Custom columns let a [dataset](./overview) carry fields beyond the four built-ins, so a row can hold exactly the data your tests and workflows need.
Every dataset starts with four built-in columns — **Input**, **Output**, **Expected output**, and **Metadata**. **Custom columns** let you add more fields of your own (for example a `label`, a `category`, or a reference id) alongside them. A newly added column is empty on every existing row; it only gets a value when you fill that cell.
## Manage columns
Open a dataset and click **Columns** in the top bar. From the popover you can:
| Action | What it does |
| ----------- | ----------------------------------------------------------------------------------------------------------------------- |
| **Add** | Create a new custom column. Use **+ New column**, give it a name. It appears empty on every row. |
| **Rename** | Edit a column's display name (the pencil icon). Works for built-in and custom columns. Renaming never changes the data. |
| **Reorder** | Drag a column by its handle to change the left-to-right order in the table. |
| **Remove** | Soft-delete a column (the trash icon). It disappears from the table, exports, and the API — but its data is kept. |
| **Re-add** | Restore a removed column from the **Removed columns** group at the bottom of the popover. Its values come back intact. |
**Remove is always reversible.** Removing a column — built-in or custom — hides it everywhere but preserves its data, so you can re-add it at any time and its values reappear. Built-in columns can be removed too; nothing is permanently deleted.
## Editing custom values
Open any row to edit its cells. Active custom columns show their own editable fields next to the built-in ones, and the values you enter are saved with the row just like the built-ins. You can also set custom values programmatically — keyed by column identifier — over the API, the SDKs, or MCP; see [Edit row contents](./edit-rows).
## How columns affect the rest of the dataset
* **Adding, renaming, removing, or reordering a column does not create a new dataset version.** Only writing actual cell data does. So shaping your columns is free to iterate on.
* **CSV export honors your active columns.** Exports include your custom columns (under their display name) in column order and leave out removed columns. Built-in columns keep their standard `input` / `output` / `expected_output` / `metadata` headers.
* **Removed columns are excluded** from the table, row reads, the API, and exports until you re-add them.
## Manage columns from your coding agent
Through the [MCP server](../getting-started/mcp), an agent like Claude or Cursor can manage a dataset's columns for you — list columns (including removed ones), add, rename, reorder, remove, and restore them — the same operations available in the UI.
## Next step
* [Add expected output](./expected-output): record the known-good answer for a row.
* [Regression testing](../test-and-fix/regression-testing): replay the dataset against your agent.
# Edit row contents
Source: https://docs.latitude.so/datasets/edit-rows
Update the cells of an existing dataset row — over the API, the SDKs, or an MCP agent — to fill in expected output or correct any field after a row was created.
**Where this fits:** Part of **Refine**. Editing rows lets you curate a [dataset](./overview) after it is built — most commonly to add [expected output](./expected-output) to rows imported [from traces](./add-traces).
Every [dataset](./overview) row is made of cells: the four built-in fields — **Input**, **Output**, **Expected output**, and **Metadata** — plus any [custom columns](./custom-columns) you have added. You can edit those cells by hand in the UI, or programmatically over the API, the SDKs, or an [MCP](../getting-started/mcp) agent.
## Edit a row in the UI
Open a dataset and open any row to edit its cells inline. Built-in fields and active custom columns each show their own editable field; the values you enter are saved with the row. See [Add expected output](./expected-output) for the most common case.
## Edit a row programmatically
Updates are **partial**: you send only the cells you want to change, and every cell you omit keeps its current value. This makes it safe to, for example, set `expectedOutput` on a row without resending its input or output.
How cells are addressed:
* **Built-in cells** use their field name: `input`, `output`, `expectedOutput`, `metadata`.
* **Custom columns** are set through a `custom` map keyed by the column's **stable identifier** (not its display name). Custom values are merged onto the row's existing ones, so columns you omit are left untouched. Unknown or [removed](./custom-columns) columns are rejected.
A successful edit creates a new dataset version and returns its id.
You need the row's **id** and, for custom columns, the column **identifiers**. List rows to get their ids, and list the dataset's columns to get identifiers — both built-in (`input`, `output`, …) and custom. These are available in the API, every SDK, and over MCP.
### HTTP API
```
PATCH /v1/projects/{projectSlug}/datasets/{datasetSlug}/rows/{rowId}
```
| Field | Type | Required | Description |
| ---------------- | -------- | -------- | ----------------------------------------------------------------------------- |
| `input` | any JSON | No | New input cell. Omit to leave unchanged. |
| `output` | any JSON | No | New output cell. Omit to leave unchanged. |
| `expectedOutput` | any JSON | No | New correct answer for the row. Omit to leave unchanged. |
| `metadata` | any JSON | No | New metadata cell. Omit to leave unchanged. |
| `custom` | object | No | Custom column values keyed by column identifier. Merged onto existing values. |
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl -X PATCH \
https://api.latitude.so/v1/projects/my-project/datasets/my-dataset/rows/ROW_ID \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"expectedOutput": "The correct answer",
"custom": { "COLUMN_IDENTIFIER": "high" }
}'
```
The response carries the new dataset version:
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{ "versionId": "os4gxr0mhcocrydibwupq4jk", "version": 10 }
```
### SDKs
```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}}
import { LatitudeClient } from "@latitude-data/sdk";
const client = new LatitudeClient({ apiKey: process.env.LATITUDE_API_KEY! });
const { version } = await client.datasets.updateRow(
"my-project",
"my-dataset",
"ROW_ID",
{
expectedOutput: "The correct answer",
custom: { COLUMN_IDENTIFIER: "high" },
},
);
```
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
import os
from latitude_sdk import LatitudeClient
client = LatitudeClient(api_key=os.environ["LATITUDE_API_KEY"])
result = client.datasets.update_row(
"my-project",
"my-dataset",
"ROW_ID",
expected_output="The correct answer",
custom={"COLUMN_IDENTIFIER": "high"},
)
```
### From your coding agent (MCP)
Through the [MCP server](../getting-started/mcp), an agent like Claude or Cursor can edit a row for you — for example, *"in my dataset From Traces Drawer, set the expected output of the failing row to …"*. The agent lists the dataset's rows and columns to resolve the ids, then applies the same partial update described above. For the live tool list and full schemas, see the [API reference](https://api.latitude.so/docs).
## Next step
* [Add expected output](./expected-output): the most common reason to edit a row.
* [Custom columns](./custom-columns): add, rename, reorder, and remove the columns a row can hold.
* [Regression testing](../test-and-fix/regression-testing): replay the curated dataset against your agent.
# Add expected output
Source: https://docs.latitude.so/datasets/expected-output
Record the correct answer for dataset rows so regression tests can check your agent against a known-good result.
**Where this fits:** Part of **Refine**. Expected output turns a [dataset](./overview) of real traces into a test set with a known-good answer for [regression testing](../test-and-fix/regression-testing).
**Expected output** is the correct or desired answer for a dataset row. It lets a test compare your agent's actual output against a known-good result, rather than only checking the output in isolation. Expected output is optional, since many evaluations check a response on its own merits, but it is what makes a row a precise regression case.
## Add expected output to a row
In a [dataset](./overview), open a row to view its input, output, and fields.
Add the correct answer in the **Expected output** field. A row with no expected output shows an **Add expected output** prompt, so it is easy to see which rows still need one.
## Where the expected answer comes from
When you build a dataset from a failing [signal](../signals/overview), the agent's actual output was wrong, which is why the signal exists. The expected output is the response the agent should have produced. Common sources:
* the correct answer a human reviewer would give
* the behaviour described in the signal or in an [annotation](../annotations/overview)
* a corrected version of the original output
## Add it programmatically
You can also set expected output (or any other cell) over the API, the SDKs, or an [MCP](../getting-started/mcp) agent — handy for filling in answers in bulk or straight from your coding agent after building a dataset. See [Edit row contents](./edit-rows).
## Next step
* [Regression testing](../test-and-fix/regression-testing): replay the dataset against your agent and check the results.
# Datasets
Source: https://docs.latitude.so/datasets/overview
Curate collections of inputs, outputs, and expected outputs from real traces to test and improve your agent.
**Where this fits:** Datasets are part of **Refine**, after [Signals](../signals/overview). They turn real traces into reusable test cases for [regression testing](../test-and-fix/regression-testing).
A **dataset** is a collection of rows you curate for testing and improving your agent. Each row holds an **input**, the agent's **output**, an optional **expected output**, and arbitrary **metadata**. Teams use them as golden datasets: stable, known-good test sets that a fix has to keep passing.
## What a dataset row contains
| Column | Description |
| ------------------- | --------------------------------------------------------------------------------------------------------------- |
| **Input** | The input your agent received, for example the user message. |
| **Output** | What your agent actually returned. |
| **Expected output** | The correct or desired answer, used to check the agent. Optional, see [Add expected output](./expected-output). |
| **Metadata** | Arbitrary fields carried alongside the row. |
Beyond these built-ins you can add your own [custom columns](./custom-columns), and rename, reorder, or remove any column.
## Create a dataset
You can build a dataset three ways:
Select traces from the trace list, search results, or a signal, and add them to a dataset. The most realistic test cases come straight from production.
Open **Datasets** in your project, create a new dataset, then **Import** a CSV or **Add row** to enter cases by hand.
Through the [MCP server](../getting-started/mcp), an agent like Claude or Cursor can create datasets and pull in the traces behind a signal for you.
## How datasets are used
* **Regression testing**: replay a dataset's inputs against your agent and compare results to the expected outputs and your evaluations. See [Regression testing](../test-and-fix/regression-testing).
* **Curating test sets**: collect representative traces from [Search](../search/overview) and [Signals](../signals/overview) into a stable, reusable set.
* **Sharing with your harness**: export a dataset as CSV to drive tests in your own pipeline.
## Next step
* [Add traces to a dataset](./add-traces): build a test set from real production traces.
* [Custom columns](./custom-columns): add, rename, reorder, or remove columns.
# Cluster
Source: https://docs.latitude.so/deployment/cluster
Deploy a scalable, highly-available Latitude on any Kubernetes cluster with our Helm chart.
Cluster runs Latitude on **Kubernetes** through a cloud-agnostic [Helm chart](https://github.com/latitude-dev/latitude-llm/tree/development/charts/latitude): a Deployment per application service, a one-shot migrations job, ingress, and bundled infrastructure (Postgres, ClickHouse, Redis, Temporal, SeaweedFS) — every piece replaceable by a managed equivalent. It deploys the published [Docker images](https://hub.docker.com/u/latitudedata).
For a simpler start on one machine, use the [Single-host](/deployment/single-host) deployment — same images, same configuration contract, no cluster required.
## Quick start
The chart lives in the Latitude repository under `charts/latitude` — a sparse checkout downloads just that folder:
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
git clone --depth 1 --filter=blob:none --sparse https://github.com/latitude-dev/latitude-llm.git
cd latitude-llm
git sparse-checkout set charts/latitude
cd charts/latitude
```
Don't name it `values.yaml` — that file already holds the chart's defaults.
```yaml my-values.yaml theme={"theme":{"light":"github-light","dark":"github-dark"}}
config:
webUrl: https://latitude.example.com
apiUrl: https://api.latitude.example.com
ingestUrl: https://ingest.latitude.example.com
extraEnv:
# Email transport — required for magic-link sign-in (choose one provider)
- name: LAT_SMTP_HOST
value: smtp.example.com
- name: LAT_SMTP_PORT
value: "587"
- name: LAT_SMTP_USER
value: apikey
- name: LAT_SMTP_FROM
value: noreply@example.com
secrets:
masterEncryptionKey: # openssl rand -hex 32
betterAuthSecret: # openssl rand -hex 32
# Optional secret env vars (email passwords, AI provider keys, OAuth
# client secrets) — delivered through the chart-managed Secret.
extra:
LAT_SMTP_PASS: your-smtp-password
postgres:
auth:
password: # openssl rand -hex 16
runtimePassword: # openssl rand -hex 16
clickhouse:
auth:
password: # openssl rand -hex 16
ingress:
className: nginx
# tls: [...]
```
The secrets are permanent for the installation — keep this file (or move the keys into a pre-created Secret and set `secrets.existingSecret`). Every future `helm upgrade` must receive the same values.
The install brings up the bundled infrastructure, runs database migrations and the Temporal schema setup as one-shot jobs, and starts the five application services. App pods wait in `Init:0/1` until their dependencies accept connections, then start once, cleanly — the first install takes a few minutes (image pulls plus volume provisioning).
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
helm install latitude . \
--namespace latitude --create-namespace \
--values my-values.yaml --timeout 15m
```
All pods should reach `Running`/`Ready` and the `latitude-migrations`, `latitude-temporal-schema`, and `latitude-temporal-namespace` jobs should show `Complete`.
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
kubectl get pods -n latitude
kubectl get jobs -n latitude
```
Open the web UI (at your `config.webUrl`) and register. Latitude sends a magic link through the email transport you configured; click it to finish signing in and create your organization.
## What gets deployed
| Workload | Kind | Purpose |
| ----------------------- | ------------------------------- | --------------------------------------------------------------------------- |
| `web`, `api`, `ingest` | Deployment + Service | UI, public API & MCP server, OTLP trace ingestion — exposed via the ingress |
| `workers`, `workflows` | Deployment | BullMQ background jobs and Temporal workers — internal only |
| `migrations` | Job (post-install, pre-upgrade) | One-shot Postgres + ClickHouse migrations |
| `postgres` | StatefulSet + PVC | Primary store + pgvector |
| `clickhouse` | StatefulSet + PVC | Span/telemetry OLAP store |
| `redis`, `redis-bullmq` | Deployment / StatefulSet + PVC | Cache + durable queues |
| `temporal` | Deployment + schema Jobs | Workflow engine, Postgres-backed (no Elasticsearch/Cassandra) |
| `seaweedfs` | StatefulSet + PVC | S3-compatible object store (single container, same recipe as Single-host) |
## Secrets
Optional non-secret settings (email transport, `LAT_AI_*` model selection, integrations) go in `config.extraEnv` as standard `EnvVar` entries; optional **secret** values (AI provider keys, email passwords, OAuth client secrets) go in `secrets.extra`, which merges them into the chart-managed Secret — never as `extraEnv` literals, which would sit readable in the pod spec.
To manage secrets yourself (e.g. with an external secrets operator), create a Secret carrying the chart's expected keys and set `secrets.existingSecret` — the chart then renders no Secret of its own. The key list is in the [chart README](https://github.com/latitude-dev/latitude-llm/tree/development/charts/latitude#secrets).
See the [secrets reference](/deployment/configuration#secrets) for what each application secret does.
## AI features
The stack boots and core observability (ingest + trace viewing) works **without API keys**. AI-dependent features stay disabled until you add credentials for the providers you configure — keys in `secrets.extra`, `LAT_AI_*` provider/model selection in `config.extraEnv`:
| Capability | Providers | Feature |
| ---------- | -------------------------------------------------------------------------------------- | --------------------------------------------------------------------------- |
| Generation | Amazon Bedrock (default), Anthropic, OpenAI, Google, or any OpenAI-compatible endpoint | Flaggers, evaluations, signal summarization, taxonomy naming, AI generation |
| Embeddings | Voyage AI (default), OpenAI, Google, or any OpenAI-compatible endpoint | Semantic trace/signal search, search highlights, signal clustering |
| Reranking | Voyage AI (default) or Amazon Bedrock | Signal-discovery candidate matching |
Every provider and model is selectable per feature through environment variables — see the [AI configuration reference](/deployment/configuration#ai).
## Scaling
The five application services are stateless and scale horizontally:
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
helm upgrade latitude . --values my-values.yaml --set api.replicas=3
# or imperatively:
kubectl scale deploy/latitude-api -n latitude --replicas=3
```
Every service ships with resource **requests** (HPA-ready); add limits and `HorizontalPodAutoscaler`s to taste. The bundled stateful services stay single-instance by design — for HA move them to managed equivalents ([Bring your own infrastructure](#bring-your-own-infrastructure)).
## Backups
Persistent state lives in the PVCs of `postgres`, `clickhouse`, `redis-bullmq`, and `seaweedfs`. Use your storage provider's volume snapshots, plus:
* **Postgres** — `kubectl exec -n latitude latitude-postgres-0 -- pg_dump -U latitude latitude` (the source of truth for projects, users, and metadata).
* **ClickHouse** — volume snapshots or [ClickHouse `BACKUP`](https://clickhouse.com/docs/en/operations/backup) (span/telemetry data).
* **Object store** — volume snapshots (or rely on your managed S3's durability).
## Upgrading
The chart version tracks the Latitude release, and the images default to the chart's `appVersion` — so the chart always deploys the release it shipped with. To upgrade, pull the new chart and run:
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
helm upgrade latitude . --values my-values.yaml
```
To deviate from the chart's pinned release, set `image.tag` (`X.Y.Z` with no leading `v`, or `latest`). Migrations run as a `pre-upgrade` hook — the schema is upgraded before the new pods roll. Latitude migrations are backward-compatible, so the previous release keeps working during the rollout.
## Health and observability
Every service exposes an HTTP health endpoint, wired as readiness and liveness probes (`/health`; the web app uses `/api/health`; `workers`/`workflows` listen on internal ports 9090/9091). OpenTelemetry export is available through the `LAT_OBSERVABILITY_*` variables via `config.extraEnv`.
## TLS and reverse proxy
The chart's Ingress routes the three public hosts (taken from `config.{web,api,ingest}Url`) to the `web`, `api`, and `ingest` Services over plain HTTP inside the cluster — terminate TLS at your ingress controller. Reference your certificate Secrets in `ingress.tls`, or have cert-manager issue them via `ingress.annotations`:
```yaml theme={"theme":{"light":"github-light","dark":"github-dark"}}
ingress:
className: nginx
annotations:
cert-manager.io/cluster-issuer: letsencrypt
tls:
- secretName: latitude-tls
hosts:
- latitude.example.com
- api.latitude.example.com
- ingest.latitude.example.com
```
Keep the `config.*Url` values on `https://` — they feed the `LAT_*_URL` and CORS/trusted-origin settings.
## Custom domain
Latitude exposes three public surfaces — the **web** UI, the **API** (which also hosts the **MCP server**), and **ingest** (OTLP traces). You set them through `config.webUrl`, `config.apiUrl`, and `config.ingestUrl`; the [Ingress](#tls-and-reverse-proxy) derives its three hosts from those same values, and the chart wires the rest of the URL contract for you — `LAT_TRUSTED_ORIGINS` and `LAT_CORS_ALLOWED_ORIGINS` are derived from `config.webUrl`.
So a standard three-host setup needs nothing beyond getting those three URLs right and pointing DNS at the Ingress:
```yaml theme={"theme":{"light":"github-light","dark":"github-dark"}}
config:
webUrl: https://app.your-domain.com
apiUrl: https://api.your-domain.com
ingestUrl: https://ingest.your-domain.com
```
Each must be the real, externally-resolvable `https://` URL (scheme included): the chart feeds them into the in-cluster `LAT_*_URL`, CORS, and trusted-origin settings and routes the matching Ingress host to each Service. The `LAT_*_PORT` values stay cluster-internal — the Ingress maps your hostnames to them, so you leave them unchanged. Add the certificates as shown under [TLS and reverse proxy](#tls-and-reverse-proxy).
If you serve the web UI from more than one origin (e.g. a vanity host alongside the canonical one), override the allowlists explicitly through `config.extraEnv`:
```yaml theme={"theme":{"light":"github-light","dark":"github-dark"}}
config:
extraEnv:
- name: LAT_TRUSTED_ORIGINS
value: https://app.your-domain.com,https://latitude.your-domain.com
- name: LAT_CORS_ALLOWED_ORIGINS
value: https://app.your-domain.com,https://latitude.your-domain.com
```
**MCP clients** connect to `${apiUrl}/v1/mcp` but sign in via OAuth against the **web** origin, so both `config.apiUrl` and `config.webUrl` must be correct and externally reachable (Ingress + TLS).
## Bring your own infrastructure
Every bundled dependency is an independent toggle: set `.enabled: false` and fill its `external:` block, and the chart wires the right `LAT_*` configuration at your existing or managed instance instead of deploying the bundle.
```yaml theme={"theme":{"light":"github-light","dark":"github-dark"}}
postgres:
enabled: false
external:
databaseUrl: postgres://latitude_app:...@my-rds:5432/latitude
adminDatabaseUrl: postgres://latitude:...@my-rds:5432/latitude
temporal:
enabled: false
external:
address: my-namespace.a1b2c.tmprl.cloud:7233
namespace: my-namespace.a1b2c
apiKey: ...
```
| Dependency | Toggle | Notes |
| -------------- | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Postgres | `postgres.enabled` | Needs the `vector` (pgvector) extension and a schema-create admin role |
| ClickHouse | `clickhouse.enabled` | Dedicated database; `external.migrationUrl` is host:port only (`clickhouse://host:9000`) |
| Redis (cache) | `redis.enabled` | Latitude namespaces all keys under `latitude:`, so a shared Redis is safe |
| Redis (queue) | `redisBullmq.enabled` | Same namespacing; needs `noeviction` |
| Temporal | `temporal.enabled` | Temporal Cloud or the official [temporalio/helm-charts](https://github.com/temporalio/helm-charts) (SQL persistence, no Cassandra/Elasticsearch) for an HA cluster |
| Object storage | `seaweedfs.enabled` | Any S3-compatible store; omit `endpoint`/`forcePathStyle` for AWS S3 |
## Troubleshooting
| Symptom | Meaning and fix |
| -------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Pod stuck in `Init:0/1` | It's waiting for a dependency — `kubectl logs -n latitude -c wait-for-deps` prints which `host:port` it's polling. Usually a bundled service still starting, or an unreachable `external:` endpoint. |
| A hook job failed and `helm install` errored | Read the job logs — `kubectl logs -n latitude job/latitude-migrations` (or `job/latitude-temporal-schema`, `job/latitude-temporal-namespace`) — fix the cause and re-run the install; the jobs are idempotent. |
| Pod stuck in `Pending` | Usually no default `StorageClass` or an unbound volume claim — check `kubectl get pvc -n latitude`. |
| `ImagePullBackOff` | Check `image.registry` / `image.tag` and the node's network; the public images live at `docker.io/latitudedata/`. |
## Next steps
The full Latitude configurable environment-variable reference for self-hosting.
# Configuration
Source: https://docs.latitude.so/deployment/configuration
The full Latitude configurable environment-variable reference for self-hosting.
Every application variable is prefixed with `LAT_*`. A few infrastructure variables (`POSTGRES_*`, `CLICKHOUSE_*`) are read directly by the bundled database containers. The committed [`.env.example`](https://github.com/latitude-dev/latitude-llm/blob/development/.env.example) is the canonical starting point — it documents both local development and production: copy it to `.env.production` and apply the values marked **`# For production`** on each line.
Each variable is either **required** (the deployment won't boot or function without it) or **optional** — it has a safe default, or it unlocks a feature that stays disabled until you set it (SSO, AI, email).
## Secrets
Latitude needs two secrets, **generated uniquely per deployment** — never reuse the example values:
| Variable | Notes |
| --------------------------- | ------------------------------------------------------------ |
| `LAT_MASTER_ENCRYPTION_KEY` | Encrypts stored provider credentials and integration tokens. |
| `LAT_BETTER_AUTH_SECRET` | Signs auth sessions and magic-link tokens. |
Generate each with:
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
openssl rand -hex 32
```
Moreover, remember to change the default infrastructure passwords (`POSTGRES_PASSWORD_*`, `CLICKHOUSE_PASSWORD`...) too!
## Email
A working email transport is required for anyone to log in. Local development captures mail in Mailpit; **production is not bundled with an email service** — configure exactly one provider.
| Variable | Notes |
| ---------------- | ----------------------------------------------------------- |
| `LAT_MAILPIT_*` | Local-dev capture only (UI at `:8025`); not for production. |
| `LAT_SMTP_*` | Any SMTP server (AWS SES, Postmark, SendGrid SMTP, …). |
| `LAT_MAILGUN_*` | Mailgun API transport. |
| `LAT_SENDGRID_*` | SendGrid API transport. |
## AI
The stack boots and core observability (ingest + trace viewing) works **without API keys** — AI-dependent features stay disabled, or fail, until you add the relevant provider key:
Every internal AI capability is configurable through `LAT_AI_*` environment variables. With nothing set, the defaults uses **Amazon Bedrock** for generation and **Voyage AI** for embeddings and reranking.
### Providers
You have to set the credentials for whichever providers you end up using:
| Variable | Provider | Available for |
| ----------------- | ------------------------------- | ----------------------------------------- |
| `LAT_AWS_*` | Amazon Bedrock `amazon-bedrock` | Generation (default), reranking |
| `LAT_VOYAGE_*` | Voyage AI `voyage` | Embeddings (default), reranking (default) |
| `LAT_ANTHROPIC_*` | Anthropic `anthropic` | Generation |
| `LAT_OPENAI_*` | OpenAI `openai` | Generation, embeddings |
| `LAT_GOOGLE_*` | Google `google` | Generation, embeddings |
| `LAT_CUSTOM_AI_*` | OpenAI-compatible `custom` | Generation, embeddings |
### Generation
Each feature resolves every setting through a three-tier fallback:
1. Per-feature variable `LAT_AI__`
2. Global variable `LAT_AI_GENERATION_`
3. Built-in default `minimax.minimax-m2.5`
Available ``:
* `PROVIDER`
* `MODEL`
* `REASONING` (`none`/`default`/`minimal`/`low`/`medium`/`high`/`xhigh`)
* `TEMPERATURE`
* `MAX_TOKENS`
Always set `PROVIDER` and `MODEL` together — model ids are provider-specific.
| Feature | What it powers | Built-in default |
| ------------------------- | ----------------------------------------------------- | ------------------------------------------------ |
| `ISSUE_DETAILS_GENERATOR` | Issue names/descriptions | `amazon-bedrock` / `minimax.minimax-m2.5` |
| `FLAGGER_CLASSIFIER` | Trace classification and annotation | `amazon-bedrock` / `anthropic.claude-haiku-4-5` |
| `FLAGGER_EXTRACTOR` | Instruction extraction | `amazon-bedrock` / `minimax.minimax-m2.5` |
| `FLAGGER_ANNOTATOR` | Flagger annotation drafting | `amazon-bedrock` / `minimax.minimax-m2.5` |
| `ANNOTATION_ENRICHER` | Annotation enrichment | `amazon-bedrock` / `minimax.minimax-m2.5` |
| `EVALUATION_JUDGE` | LLM-as-judge evaluation runs | `amazon-bedrock` / `minimax.minimax-m2.5` |
| `TAXONOMY_NAMING` | Taxonomy cluster naming | `amazon-bedrock` / `minimax.minimax-m2.5` |
| `GEPA_PROPOSER` | Evaluation-optimization proposals | `amazon-bedrock` / `anthropic.claude-sonnet-4-6` |
| `MOMENT_CLASSIFIER` | Conversation-intelligence moment candidate validation | `amazon-bedrock` / `minimax.minimax-m2.5` |
### Embeddings
Embeddings are configured globally through `LAT_AI_EMBEDDING_PROVIDER` and `LAT_AI_EMBEDDING_MODEL`. It defaults to `voyage` / `voyage-4-large`, and powers semantic trace/signal search, search highlights, and signal clustering. Without a working embeddings provider, search falls back to lexical.
The embedding model is a **one-time choice — pick it at install time and keep it**.
Once an embedding model is used on a live deployment, changing it is a difficult task. Different models produce incompatible vector spaces and existing data is never re-embedded, so switching breaks semantic search, signal matching, and clustering.
The model must also **emit 2048-dimensional vectors** — the dimension is fixed by the database schema and is not configurable.
### Reranking
Reranking is configured globally through `LAT_AI_RERANKING_PROVIDER` and `LAT_AI_RERANKING_MODEL`. It defaults to `voyage` / `rerank-2.5`, and refines signal-discovery candidate matching. Reranking is optional — when unavailable, discovery falls back to embedding-order matching.
## SSO
By default users sign in with email magic links. Add an OAuth provider to enable social sign-in:
| Variable | Notes |
| -------------------------- | -------------------------------------------------------------------------- |
| `LAT_GOOGLE_*` | Enables Google OAuth sign-in. |
| `LAT_GITHUB_*` | Enables GitHub OAuth sign-in. |
| `LAT_ALLOWED_EMAIL_DOMAIN` | Restrict sign-up / sign-in to a single email domain (e.g. your company's). |
## Integrations
Optional, and cleanly no-op when unset:
| Variable | Notes |
| ------------------ | --------------------------------------------------------------------------- |
| `LAT_SLACK_*` | Slack notifications and interactivity. |
| `LAT_GITHUB_APP_*` | GitHub App integration; see the [setup guide](/more/github) to register it. |
| `LAT_IPINFO_TOKEN` | GeoIP enrichment for the sessions panel; uses the free tier when unset. |
## Infrastructure
These wire Latitude to its datastores, depending on your chosen [deployment option](/deployment/overview) you can either point them to the bundled infrastructure services or bring your own infrastructure.
### Application
| Variable | Notes |
| ---------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `NODE_ENV` | Controls under what environment the code runs. Set to `production`. |
| `LAT_WEB_URL`, `LAT_API_URL`, `LAT_INGEST_URL` | Public URLs where users and clients reach each service. |
| `LAT_TRUSTED_ORIGINS`, `LAT_CORS_ALLOWED_ORIGINS` | Comma-separated list of origins allowed to call the API. |
| `LAT_WEB_PORT`, `LAT_API_PORT`, `LAT_INGEST_PORT` | Host bind ports (default `3000` / `3001` / `3002`). |
| `LAT_WORKERS_HEALTH_PORT`, `LAT_WORKFLOWS_HEALTH_PORT` | Health-check ports for the background workers (default `9090` / `9091`). |
| `LAT_IMAGE_TAG` | Image tag the stack pulls (default `latest`; pin `X.Y.Z` in production). |
| `LAT_OSS_TELEMETRY_ENABLED` | Anonymous OSS deployment heartbeat sent to Latitude's PostHog project (default `true` in production, `false` in development). Set `false` to opt out. |
| `LAT_EXPORT_RATE_LIMIT_*`, `LAT_INGEST_TRACE_RATE_LIMIT_*` | Rate-limit tuning for exports and per-organization/API-key trace ingestion. |
| `LAT_INGEST_TRACE_MAX_PAYLOAD_BYTES` | Maximum trace request body size in bytes (default `33554432`, or 32 MiB). Larger declared or streamed payloads receive `413`. |
| `LAT_INGEST_TRACE_MAX_IN_FLIGHT_BYTES` | Per-process budget for trace payloads being read or processed (default `67108864`, or 64 MiB). This must be at least twice `LAT_INGEST_TRACE_MAX_PAYLOAD_BYTES` to cover chunked-body assembly. |
| `LAT_INGEST_TRACE_MAX_CONCURRENT_PAYLOADS` | Maximum trace payloads being read or processed concurrently per ingest process (default `16`). |
### Postgres
Primary store for projects, users, and metadata. Requires the `vector` (pgvector) extension.
| Variable | Notes |
| ------------------------------------------------------------------------ | ------------------------------------------------------------------------------------- |
| `LAT_DATABASE_URL` | Runtime connection (RLS-bound `latitude_app` user, `latitude` schema). |
| `LAT_ADMIN_DATABASE_URL` | Superuser connection — migrations, seeds, cross-org queries. |
| `POSTGRES_USER`, `POSTGRES_PASSWORD`, `POSTGRES_DB` | Bundled-Postgres container credentials. Omit if bringing your own Postgres. |
| `POSTGRES_RUNTIME_USER`, `POSTGRES_RUNTIME_PASSWORD` | RLS runtime user, created by `docker/init-db.sh`. Omit if bringing your own Postgres. |
| `LAT_PG_POOL_MAX`, `LAT_PG_IDLE_TIMEOUT_MS`, `LAT_PG_CONNECT_TIMEOUT_MS` | Connection-pool tuning. |
### ClickHouse
OLAP store for spans and telemetry, in a dedicated database.
| Variable | Notes |
| ------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `LAT_CLICKHOUSE_URL`, `LAT_CLICKHOUSE_USER`, `LAT_CLICKHOUSE_PASSWORD`, `LAT_CLICKHOUSE_DB` | Application connection + dedicated database. |
| `LAT_CLICKHOUSE_MIGRATION_URL` | Native-protocol host and port (e.g. `clickhouse://clickhouse:9000`) for the migration tool (goose); credentials and database are taken from the other `LAT_CLICKHOUSE_*` variables. |
| `LAT_CLICKHOUSE_CLUSTER_ENABLED` | Set `true` for a clustered ClickHouse; default `false`. |
| `CLICKHOUSE_USER`, `CLICKHOUSE_PASSWORD`, `CLICKHOUSE_DB` | Bundled-ClickHouse container credentials. Omit if bringing your own ClickHouse. |
### Redis
Two Redis roles — a cache and the BullMQ job queue (same server or separate). Latitude namespaces all keys under `latitude:`, so it can share an instance with other apps.
| Variable | Notes |
| ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `LAT_REDIS_HOST`, `LAT_REDIS_PORT` | Cache connection. |
| `LAT_BULLMQ_HOST`, `LAT_BULLMQ_PORT` | Queue connection. |
| `LAT_REDIS_TLS`, `LAT_REDIS_CLUSTER` | Enable TLS / cluster mode for the cache. |
| `LAT_BULLMQ_PASSWORD`, `LAT_BULLMQ_CLUSTER` | Queue auth / cluster mode. |
| `LAT_BULL_BOARD_USERNAME`, `LAT_BULL_BOARD_PASSWORD` | Basic-auth for the optional queue dashboard, served at `/bull-board` on the workers health port. Only mounted while both are set; leave unset to disable it. |
### Temporal
Workflow engine for durable background processes.
| Variable | Notes |
| --------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
| `LAT_TEMPORAL_ADDRESS`, `LAT_TEMPORAL_NAMESPACE`, `LAT_TEMPORAL_TASK_QUEUE` | Connection, namespace, and task queue. |
| `LAT_TEMPORAL_API_KEY` | Only for Temporal Cloud. |
| `LAT_TEMPORAL_MAX_CONCURRENT_ACTIVITY_TASKS` | Optional per-worker cap for concurrently executing Temporal activities. Lower this when CPU-heavy activities share one task queue. |
### Object storage
Stores ingested payloads, datasets, and exports — shared by all services. Pick one of two drivers with `LAT_STORAGE_DRIVER`: **`fs`** writes to the local filesystem (a path or volume shared by every service), or **`s3`** points at any S3-compatible service — AWS S3, SeaweedFS, MinIO, or any other managed or self-hosted store.
| Variable | Notes |
| ------------------------------------------------------------------ | -------------------------------------------------------------------------- |
| `LAT_STORAGE_DRIVER` | `fs` (local filesystem) or `s3` (any S3-compatible store). |
| `LAT_STORAGE_FS_ROOT` | Absolute path for the `fs` driver (required when `LAT_STORAGE_DRIVER=fs`). |
| `LAT_STORAGE_S3_BUCKET`, `LAT_STORAGE_S3_REGION` | Bucket and region (required when `LAT_STORAGE_DRIVER=s3`). |
| `LAT_STORAGE_S3_ENDPOINT`, `LAT_STORAGE_S3_FORCE_PATH_STYLE` | Set for non-AWS S3 (SeaweedFS, MinIO); omit for AWS S3. |
| `LAT_STORAGE_S3_ACCESS_KEY_ID`, `LAT_STORAGE_S3_SECRET_ACCESS_KEY` | Omit to use the AWS default credential chain (e.g. an IAM role). |
# Overview
Source: https://docs.latitude.so/deployment/overview
Use Latitude as a fully-managed cloud service, or self-host it — from a single machine to a Kubernetes cluster.
Latitude is [MIT-licensed](https://github.com/latitude-dev/latitude-llm/blob/development/LICENSE) and self-hostable at any scale, on fully open infrastructure. However, there are easier ways to start benefiting from Latitude's agent monitoring features in no time.
## Latitude Cloud (recommended)
[**Latitude Cloud**](https://latitude.so) is the fully-managed version — the fastest way to start and the easiest to operate. Sign up, grab an API key, and start sending traces in minutes. There's nothing to install, scale, patch, or back up.
Free to start — 20K credits/month, 30-day retention, and unlimited seats.
Why most teams choose Cloud:
* **Zero ops.** No servers, databases, queues, or object stores to run, patch, scale, or back up — we handle all of it.
* **Every AI feature on by default.** Semantic search, flaggers, evaluations, and signal clustering work out of the box — no proprietary AI provider accounts or keys to manage.
* **Always current.** New features and fixes the moment they ship, with zero-downtime upgrades and no migrations to run yourself.
* **Scales with you.** Trace spikes, retention, and growth are handled automatically — no capacity planning.
* **Enterprise-grade security & compliance.** SOC 2, ISO 27001, and GDPR, with managed backups and high availability. See [Security & Compliance](/security/data-protection).
* **Support from the team that builds Latitude.**
## Self-host
Prefer to run Latitude yourself? It's as easy as pulling the ready-to-go container images on [Docker Hub](https://hub.docker.com/u/latitudedata). Follow these guides to get you deploying Latitude in no time.
[](https://railway.com/deploy/latitude?referralCode=GND3X9\&utm_medium=integration\&utm_source=template\&utm_campaign=generic)
A production-grade instance on one machine with Docker Compose.
A scalable, highly-available deployment on Kubernetes via a Helm chart.
Latitude is a set of five application services (`web`, `api`, `ingest`, `workers`, `workflows`) backed by Postgres (+ pgvector), ClickHouse, Redis, Temporal, and an object store. Every self-host option deploys the **same images** with the same [configurable values](/deployment/configuration).
# Single-host
Source: https://docs.latitude.so/deployment/single-host
Run a production-grade Latitude instance on a single machine with Docker Compose or Docker Swarm.
Single-host runs the **entire Latitude product on one machine**: all five application services, a one-shot migrations job, and bundled infrastructure (Postgres, ClickHouse, Redis, Temporal, SeaweedFS). It uses the published [Docker images](https://hub.docker.com/u/latitudedata) and a single `.env.production`, all defined in [`docker-stack.yml`](https://github.com/latitude-dev/latitude-llm/blob/development/docker-stack.yml).
For a scalable, highly-available deployment, deploy Latitude to a Kubernetes cluster using our Helm chart instead — see [Cluster](/deployment/cluster) deployment.
## Quick start
Create a folder `mkdir -p latitude` and download all files into it:
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
base=https://raw.githubusercontent.com/latitude-dev/latitude-llm/development
curl -fsSL --create-dirs "$base/docker-stack.yml" -o docker-stack.yml
curl -fsSL --create-dirs "$base/.env.example" -o .env.example
curl -fsSL --create-dirs "$base/docker/init-db.sh" -o docker/init-db.sh
curl -fsSL --create-dirs "$base/docker/seaweedfs/init.sh" -o docker/seaweedfs/init.sh
curl -fsSL --create-dirs "$base/docker/clickhouse/storage.xml" -o docker/clickhouse/storage.xml
```
Give permissions to the initialization scripts for postgres and seaweedfs:
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
chmod +x docker/init-db.sh docker/seaweedfs/init.sh
```
The `.env.example` file documents the neccessary production variables, create a copy of it and modify it to your needs.
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
cp .env.example .env.production
```
Generate a unique value for each key and paste them into `.env.production`:
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
echo "LAT_MASTER_ENCRYPTION_KEY=$(openssl rand -hex 32)"
echo "LAT_BETTER_AUTH_SECRET=$(openssl rand -hex 32)"
```
Moreover, remember to change the default infrastructure passwords (`POSTGRES_PASSWORD_*`, `CLICKHOUSE_PASSWORD`...) too!
Choose one of the email providers supported (Mailgun, Sendgrid, SMTP...) and configure it so login (and other) emails reach you.
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
docker compose --env-file .env.production -f docker-stack.yml up -d
```
The `migrations` container runs first and the application services wait for it to finish.
To pin a specific release instead of `latest`, set `LAT_IMAGE_TAG=X.Y.Z` in `.env.production` (image tags have no leading `v`).
Open the web UI (at your `LAT_WEB_URL`) and register. Latitude sends a magic link through the email transport you configured; click it to finish signing in and create your organization.
## What gets deployed
| Container | Image | Purpose |
| ----------------------- | ------------------------------ | ----------------------------------------- |
| `web` | `latitudedata/web` | The Latitude UI |
| `api` | `latitudedata/api` | Public API & MCP server |
| `ingest` | `latitudedata/ingest` | OTLP trace ingestion |
| `workers` | `latitudedata/workers` | BullMQ background jobs |
| `workflows` | `latitudedata/workflows` | Temporal workers |
| `migrations` | `latitudedata/migrations` | One-shot Postgres & ClickHouse migrations |
| `postgres` | `pgvector/pgvector` | Primary store + pgvector |
| `clickhouse` | `clickhouse/clickhouse-server` | Span/telemetry OLAP store |
| `redis`, `redis-bullmq` | `redis` | Cache + queues |
| `temporal` | `temporalio/auto-setup` | Workflow engine (Postgres-backed) |
| `seaweedfs` | `chrislusf/seaweedfs` | S3-compatible object store |
## Secrets
All configuration, secrets included, lives in `.env.production`. Keep `.env.production` out of version control and back it up with your other operational secrets.
See the [secrets reference](/deployment/configuration#secrets) for what each application secret does.
## AI features
The stack boots and core observability (ingest + trace viewing) works **without API keys**. AI-dependent features stay disabled until you add credentials for the providers you configure:
| Capability | Providers | Feature |
| ---------- | -------------------------------------------------------------------------------------- | --------------------------------------------------------------------------- |
| Generation | Amazon Bedrock (default), Anthropic, OpenAI, Google, or any OpenAI-compatible endpoint | Flaggers, evaluations, signal summarization, taxonomy naming, AI generation |
| Embeddings | Voyage AI (default), OpenAI, Google, or any OpenAI-compatible endpoint | Semantic trace/signal search, search highlights, signal clustering |
| Reranking | Voyage AI (default) or Amazon Bedrock | Signal-discovery candidate matching |
Every provider and model is selectable per feature through environment variables — see the [AI configuration reference](/deployment/configuration#ai).
## Scaling
`docker-stack.yml` also runs under **Docker Swarm** (`docker swarm init`, then `docker stack deploy -c docker-stack.yml latitude`), which is the better fit for scaling. The stateless application services — `web`, `api`, `ingest`, `workers`, `workflows` — scale horizontally: raise their `deploy.replicas` in the file (or `docker service scale latitude_api=3`), and Swarm load-balances across replicas, including across extra machines joined with `docker swarm join`.
The bundled stateful services (Postgres, ClickHouse, Redis, Temporal, SeaweedFS) stay single-instance — for HA, point them at managed/external instances ([Bring your own infrastructure](#bring-your-own-infrastructure)) or move to the Cluster deployment. On plain Compose (one host), the services with no published ports — `workers` and `workflows` — can still scale directly: `docker compose -f docker-stack.yml up -d --scale workers=3`.
## Backups
Persisted state lives in the `postgres_data`, `clickhouse_data`, `seaweedfs_data`, and `redis_bullmq_data` volumes. Back them up regularly:
* **Postgres** — `docker compose -f docker-stack.yml exec postgres pg_dump -U "$POSTGRES_USER" "$POSTGRES_DB"` (the source of truth for projects, users, and metadata).
* **ClickHouse** — back up `clickhouse_data` or use [ClickHouse `BACKUP`](https://clickhouse.com/docs/en/operations/backup) (span/telemetry data).
* **Object store** — back up `seaweedfs_data` (or rely on your managed S3's durability).
## Upgrading
Pin a specific `X.Y.Z` tag for Latitude's images in production so upgrades are deliberate. For easiness, you can bump `LAT_IMAGE_TAG` to a new release (or use :latest) in `.env.production`. Then pull the new images and restart the services.
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
docker compose --env-file .env.production -f docker-stack.yml pull
docker compose --env-file .env.production -f docker-stack.yml up -d
```
The `migrations` container runs automatically on every `up`, applying any new Postgres and ClickHouse migrations before the application services start.
## Health and observability
Every service exposes an HTTP health endpoint, wired as the container healthchecks in `docker-stack.yml` (`/health`; the web app uses `/api/health`; `workers`/`workflows` listen on internal ports 9090/9091). Check them with `docker compose -f docker-stack.yml ps` — every service should report `healthy`. OpenTelemetry export is available through the `LAT_OBSERVABILITY_*` variables in `.env.production`.
## TLS and reverse proxy
`docker-stack.yml` publishes `web` (3000), `api` (3001), and `ingest` (3002) on the host over plain HTTP. For anything internet-facing, put a TLS-terminating reverse proxy (Caddy, nginx, Traefik) in front, route your domain(s) to those ports, and set the `LAT_*_URL` / `*_ORIGINS` values to the public `https://` URLs.
## Custom domain
Latitude exposes three public surfaces, each typically on its own hostname behind your [reverse proxy](#tls-and-reverse-proxy): the **web** UI, the **API** (which also hosts the **MCP server**), and **ingest** (OTLP traces). `.env.example` ships these pointing at `localhost`; for a real domain you must update the public URLs **and** the origin allowlists, so that browsers, the API, and MCP clients all agree on where each service lives.
In `.env.production`:
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
# Public URLs — what users, SDKs, and MCP clients connect to
# (https, terminated by your reverse proxy)
LAT_WEB_URL=https://app.your-domain.com
LAT_API_URL=https://api.your-domain.com
LAT_INGEST_URL=https://ingest.your-domain.com
# Origin allowlists — must contain your web origin, or browser calls to the API
# are rejected and sign-in fails. Comma-separate to allow several origins.
LAT_TRUSTED_ORIGINS=https://app.your-domain.com
LAT_CORS_ALLOWED_ORIGINS=https://app.your-domain.com
```
Then restart so the services pick up the new values:
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
docker compose --env-file .env.production -f docker-stack.yml up -d
```
The hostnames are yours to pick — three subdomains (above), three separate domains, or one domain with path routing — as long as each `LAT_*_URL` exactly matches (scheme included) the URL your reverse proxy serves, and the proxy forwards each host to the matching service port. Leave the `LAT_*_PORT` values as their defaults (`3000` / `3001` / `3002`): they're the ports each service listens on internally, not part of the public hostname.
**MCP clients** connect to `${LAT_API_URL}/v1/mcp` but sign in via OAuth against the **web** origin, so both `LAT_WEB_URL` and `LAT_API_URL` must be correct.
## Bring your own infrastructure
Each infra service in `docker-stack.yml` is a clearly-marked, independently removable block. To use an existing or managed datastore, comment out its block and point the matching `LAT_*` value at your instance:
| Dependency | Repoint | Notes |
| -------------- | ---------------------------------------- | ---------------------------------------------------------------------- |
| Postgres | `LAT_DATABASE_*`, `LAT_ADMIN_DATABASE_*` | Needs the `vector` (pgvector) extension and a schema-create admin role |
| ClickHouse | `LAT_CLICKHOUSE_*` | Dedicated `LAT_CLICKHOUSE_DB` database |
| Redis (cache) | `LAT_REDIS_*` | Latitude namespaces all keys under `latitude:` |
| Redis (queue) | `LAT_BULLMQ_*` | Latitude namespaces all keys under `latitude:` |
| Temporal | `LAT_TEMPORAL_*` | Pointing to Temporal Cloud is recommended |
| Object storage | `LAT_STORAGE_*` | Point at any S3-compatible store or the local filesystem |
If you remove a bundled service that another service lists under `depends_on` (`postgres`/`clickhouse` for `migrations`, `postgres` for `temporal`), delete that reference too, or Compose will refuse to start.
## Next steps
The full Latitude configurable environment-variable reference for self-hosting.
# Contributing
Source: https://docs.latitude.so/development/contributing
How to contribute to Latitude — set up locally, propose changes, and follow our pull-request conventions and Code of Conduct.
Latitude is open source under the [MIT License](https://github.com/latitude-dev/latitude-llm/blob/development/LICENSE), and contributions are welcome — code, docs, bug reports, and feature ideas alike.
The full contribution guide — making changes, reporting issues, the CLA.
The standards we hold ourselves and our community to.
## Ways to contribute
* Pick up a [good first issue](https://github.com/latitude-dev/latitude-llm/contribute) — curated, newcomer-friendly tasks.
* Open and vote on [issues](https://github.com/latitude-dev/latitude-llm/issues).
* Improve the [docs](https://docs.latitude.so).
For typos, small docs fixes, and clearly-scoped bugs, just open a PR. **For new features or anything significant, open an issue first** so we can discuss the approach — undiscussed changes may be rejected.
> If you like the project but don't have time to contribute code: star the repo, share Latitude with people who'd find it useful or mention it at meetups or in your project's README.
## Reporting issues
Search [existing issues](https://github.com/latitude-dev/latitude-llm/issues) first. A good bug report has expected vs. actual behavior, exact repro steps, and your environment. Found a security vulnerability? Don't open a public issue — check out the [Security Policy](https://github.com/latitude-dev/latitude-llm/blob/development/SECURITY.md).
## Opening a pull request
Fork and branch from `development` (our trunk).
Split large changes — schema separate from logic, refactors first.
Run `pnpm check`, `pnpm typecheck`, `pnpm knip`, and `pnpm test` before pushing.
Use [Conventional Commits](https://www.conventionalcommits.org/) for commit and PR titles (e.g. `fix(traces): handle empty spans`). We squash-merge.
Reference the issue with `Closes #123`. The first time you open a PR, a bot asks you to sign our Contributor License Agreement — a one-time step. PRs can't be merged until it's signed.
> We're a small team, we read everything but may take a few days, longer for big changes. Stale or out-of-scope PRs may be closed, but you're welcome to reopen.
## Community
We all hang out in our [Slack community](https://join.slack.com/t/trylatitude/shared_invite/zt-35wu2h9es-N419qlptPMhyOeIpj3vjzw) — a good place to ask questions and share what you're building.
# Setup
Source: https://docs.latitude.so/development/setup
Run the full Latitude stack on your machine ready for development with hot reload and infrastructure in Docker.
Infrastructure (Postgres, ClickHouse, Redis, Mailpit, Temporal) runs in Docker, while the five Latitude services run directly on your host for fast hot reload.
Core observability (trace ingest and viewing) works out of the box. AI-dependent features light up as you add the documented provider keys, see [AI features](#ai-features).
This guide is for running Latitude locally for development. To self-host a production instance, see the [Deployment guide](/deployment/single-host) instead.
## Prerequisites
* **Node.js 25 and Python 3.13**. If you use [mise](https://mise.jdx.dev), `mise install` provisions both.
* **pnpm 10**. Enable it with `corepack enable`.
* **Docker**. Docker Desktop or any Docker Engine, used for the infrastructure containers.
* **goose** — the ClickHouse migration tool. Install with `brew install goose` (or see [goose releases](https://github.com/pressly/goose/releases) on other platforms).
* **uv** — the Python package manager for the evaluation optimizer runtime. Install with `brew install uv` (or see [the uv docs](https://docs.astral.sh/uv/getting-started/installation/)).
## Local setup
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
pnpm install
```
This also configures the git pre-commit hooks via the `prepare` script.
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
cd packages/platform/op-gepa/python
uv venv && uv sync --all-extras --all-groups
```
Latitude includes a Python sidecar for the evaluation optimizer runtime.
The `.env.example` file carries working local defaults and boots the whole stack API key-free.
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
cp .env.example .env.development
cp .env.example .env.test
```
Then, set `NODE_ENV=test` in `.env.test` (`.env.development` already defaults to `development`). And point `LAT_STORAGE_FS_ROOT` (in both environments) at an **absolute** path for the local object store (the default is `/tmp/latitude`, created automatically).
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
pnpm build
```
Some platform packages must be compiled before the database migrations run.
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
docker compose up -d
```
`docker-compose.yml` is for local **infrastructure-only**: Postgres (with `pgvector` extension), ClickHouse, two Redis instances (cache + BullMQ), Mailpit (local SMTP), and self-hosted Temporal instance and its UI.
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
pnpm migrate
```
Runs the Postgres migrations (Drizzle Kit) and the ClickHouse migrations (goose).
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
pnpm seed
```
Optionally create a sample organization, with a default project, a handful of users, an API key, and realistic sample telemetry across Postgres and ClickHouse — enough to sign in and exercise traces, search, signals, and evaluations immediately.
Pick whichever you prefer — all run with hot reload:
```bash tmuxinator theme={"theme":{"light":"github-light","dark":"github-dark"}}
# Infra + all services + Postgres studio, each in its own pane.
# Requires tmux and tmuxinator. Runs `docker compose up` for you,
# so you can skip the standalone infra step above.
pnpm tmux
```
```bash turbo theme={"theme":{"light":"github-light","dark":"github-dark"}}
# Runs every workspace `dev` task together.
pnpm dev
```
```bash manual theme={"theme":{"light":"github-light","dark":"github-dark"}}
# Spin up each service separately.
pnpm --filter @app/web dev
pnpm --filter @app/api dev
pnpm --filter @app/ingest dev
pnpm --filter @app/workers dev
pnpm --filter @app/workflows dev
```
Open [http://localhost:3000](http://localhost:3000), regsiter or enter a seeded user (`owner@acme.com` or `admin@acme.com`), and click the magic link Mailpit captures at [http://localhost:8025](http://localhost:8025).
When dependencies, migrations, or generated artifacts drift, `pnpm reset` (clean →
install → build → reset DBs) gives you a clean slate; `pnpm catchup` rebuilds and
applies any new migrations without dropping data.
## Local services
Once everything is running, these are the local endpoints you can access:
| Service | URL | Notes |
| -------------- | ---------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- |
| Web UI | [http://localhost:3000](http://localhost:3000) | The Latitude app UI |
| API | [http://localhost:3001](http://localhost:3001) | Public API; health at `/health` |
| Ingest | [http://localhost:3002](http://localhost:3002) | OTLP trace ingest; health at `/health` |
| Workers | [http://localhost:9090](http://localhost:9090) | BullMQ workers; health at `/health` |
| Workflows | [http://localhost:9091](http://localhost:9091) | Temporal workers; health at `/health` |
| Mailpit | [http://localhost:8025](http://localhost:8025) | Local inbox — magic links land here |
| Temporal UI | [http://localhost:8233](http://localhost:8233) | See workflow executions |
| Drizzle Studio | [https://local.drizzle.studio/?port=3003](https://local.drizzle.studio/?port=3003) | Postgres console — run `pnpm --filter @platform/db-postgres pg:studio` first |
## Contributing workflow
The pre-commit hook runs `pnpm check`, `pnpm typecheck`, and `pnpm knip`. It's configured automatically on `pnpm install`; to (re)configure it in an existing clone run `pnpm prepare`.
Before opening a pull requests, run the same checks CI runs:
| Task | Command |
| ----------------------- | ---------------- |
| Format | `pnpm format` |
| Lint | `pnpm check` |
| Typecheck | `pnpm typecheck` |
| Dead-code / unused deps | `pnpm knip` |
| Tests | `pnpm test` |
You can scope any task to a single package with `--filter`, and pass flags after `--`:
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
pnpm --filter @app/api test # one package
pnpm --filter @app/api test -- src/some-file.test.ts # one file
pnpm --filter @app/api test -- -t "health endpoint" # by test name
```
Check out the full [Contributing guidelines](/development/contributing).
## AI features
The stack boots and core observability (ingest + trace viewing) works **without API keys**, and the full test suite is guaranteed to pass with no keys set. However, there are AI-dependent features that you must add the relevant keys to your environment file to enable them:
| Capability | Providers | Feature |
| ---------- | -------------------------------------------------------------------------------------- | --------------------------------------------------------------------------- |
| Generation | Amazon Bedrock (default), Anthropic, OpenAI, Google, or any OpenAI-compatible endpoint | Flaggers, evaluations, signal summarization, taxonomy naming, AI generation |
| Embeddings | Voyage AI (default), OpenAI, Google, or any OpenAI-compatible endpoint | Semantic trace/signal search, search highlights, signal clustering |
| Reranking | Voyage AI (default) or Amazon Bedrock | Signal-discovery candidate matching |
Every provider and model is selectable per feature through environment variables — see the [AI configuration reference](/deployment/configuration#ai).
## What's next
How to propose changes, our PR conventions, and the Code of Conduct.
Point an application at your local instance and watch traces arrive.
# Evaluation alignment
Source: https://docs.latitude.so/evaluations/alignment
Measure how well your automated evaluations agree with human judgment.
# Evaluation alignment
Alignment measures how closely an evaluation matches human judgment. It answers one question: can you trust this detector to represent the behavior your team cares about?
## Why alignment matters
An evaluation is useful only when it agrees with the way your team reviews real traffic. Without alignment tracking:
* You may not notice that an evaluation is too strict or too lenient.
* You may miss drift as your agent, users, or product change.
* You may keep scoring a behavior against outdated examples.
Alignment helps Latitude keep generated evaluations calibrated over time.
## How alignment works
Alignment is computed when an evaluation and a human annotation score the same trace. Latitude compares their verdicts and uses the result as feedback for the detector. The point is not to display a metric; it is to keep the evaluation close to the latest human-reviewed examples.
## Viewing alignment
Each evaluation detail page shows alignment when enough human-reviewed traces are available. Use it to see whether the evaluation still matches reviewer expectations and where it may be drifting.
## Alignment and generated evaluations
When Latitude generates an evaluation from a signal:
1. It collects examples from annotations, signal-linked scores, and trace context.
2. It builds a detector for the behavior.
3. The detector is compared against known examples.
4. The detector is attached to the signal.
A detector can start from a small amount of evidence. As more annotations and scores arrive, Latitude has more to work with, and it can realign the detector as new annotations, flagger matches, evaluation results, and custom scores come in. This keeps the detector matched to the behavior as production traffic evolves.
## Manually defined detectors
A detector you write when you [create a signal](../signals/create) works differently. It runs exactly as you defined it and is not automatically realigned to annotations. That is deliberate: it does what you specified, and nothing changes it behind your back.
If a detector you defined turns out too strict or too lenient, edit it. Adjust the conditions, rewrite the judge criteria, change a threshold in a script, and preview the change against recent sessions before saving. See [Detection methods](./detection-methods).
## Improving alignment
When a generated evaluation looks misaligned:
1. Review traces where the evaluation and human review disagree.
2. Add annotations with specific feedback.
3. Confirm the signal has representative examples of the behavior.
4. Let the new evidence improve the next realignment.
This keeps automated scoring grounded in human judgment.
## Next steps
* [Annotations](../annotations/overview): how human review produces alignment signal
* [Flaggers](../annotations/flaggers): automatic annotators that contribute signal
* [Detection methods](./detection-methods): the three ways to define a detector
* [Signals](../signals/overview): how evaluation matches become tracked signals
# Custom scripts
Source: https://docs.latitude.so/evaluations/custom-scripts
Write a JavaScript detector that reads a session and decides whether a behavior is present.
A custom script is a detector you write by hand in JavaScript. It reads one session and returns a score saying how strongly a behavior is present. Reach for it when a [set of conditions](./detection-methods#set-of-conditions) is too rigid and an [LLM judge](./detection-methods#llm-as-judge) is too loose, or when you need logic that combines several checks.
Conditions and LLM judges compile down to the same kind of script, so a custom script can express anything they can, plus whatever else you write.
Most signals don't need a custom script. Start with conditions or a judge, and drop to a script when you hit their limits.
## The shape of a script
Your code runs as the body of an async function, so you can use `await` at the top level. It must `return` a score built with `Score`, `Passed`, or `Failed`.
```js theme={"theme":{"light":"github-light","dark":"github-dark"}}
// Match sessions where the assistant said it couldn't help.
const messages = session.conversation
const lastReply = messages.length ? messages[messages.length - 1].content : ''
return lastReply.includes("I can't help") ? Passed() : Failed()
```
`Passed()` and `Failed()` are shorthand for a full match (`1`) and no match (`0`). Use `Score(value, feedback)` when you want a strength in between, or a note explaining the verdict.
## Returning a verdict
A script returns a strength between `0` and `1`, not a pass or fail. Latitude compares that strength against a threshold (0.5 by default): at or above it, the behavior counts as present and the session joins the signal.
| Helper | Returns | Use for |
| --------------------------- | ------------------------------- | ------------------------------------------- |
| `Score(value, feedback?)` | `value`, from 0 to 1 | A graded strength, optionally with a reason |
| `Passed(value?, feedback?)` | `value` if given, otherwise `1` | A clear match |
| `Failed(value?, feedback?)` | `value` if given, otherwise `0` | A clear non-match |
`feedback` is optional free text. Latitude stores it on the score and uses it when it groups and displays matches, so a short reason pays off later.
`passed = true` means the behavior is present, not that the session was good. A signal tracks a behavior, and a matching session is one that exhibits it. A signal for "made-up information" passes when the model hallucinated.
## The session object
Every script receives one global, `session`: a read-only snapshot of the conversation being checked. It is frozen, so you can read it but not change it.
| Field | Type | Description |
| --------------------------------------- | ---------- | ------------------------------------------------------------- |
| `id` | `string` | The session id |
| `userId` | `string` | The end user's id, empty if none was sent |
| `startTime`, `endTime` | `string` | ISO 8601 timestamps |
| `duration` | `number` | Total wall-clock time, in nanoseconds |
| `timeToFirstToken` | `number` | Time to the first token, in nanoseconds |
| `traceCount`, `spanCount`, `errorCount` | `number` | Counts across the session |
| `cost` | `object` | `{ input, output, total }`, in microcents |
| `tokens` | `object` | `{ input, output, total, cacheRead, cacheCreate, reasoning }` |
| `tags` | `string[]` | Tags on the session |
| `metadata` | `object` | The string metadata your app sent |
| `conversation` | `array` | The transcript, each entry `{ role, content }` |
| `traces` | `array` | Per-trace breakdown (see below) |
Numbers are in raw units: durations are nanoseconds, costs are microcents (one US cent is 1,000,000 microcents), and token counts are integers. Most scripts read `conversation`, `tags`, `metadata`, and the tool data rather than the cost and token fields.
### Messages
`session.conversation` is the deduplicated transcript for the whole session. Each entry is `{ role, content }`.
```js theme={"theme":{"light":"github-light","dark":"github-dark"}}
const userTurns = session.conversation.filter((m) => m.role === 'user').length
return Score(userTurns > 5 ? 1 : 0, `${userTurns} user turns`)
```
Interpolating the array into a string renders it as `[role] content` lines, one per message, which is handy for prompts:
```js theme={"theme":{"light":"github-light","dark":"github-dark"}}
const transcript = `${session.conversation}`
// [user] where is my order?
// [assistant] let me check that for you...
```
### Traces and tools
`session.traces` breaks the session down by trace. Each trace carries its own rollups plus the models, providers, finish reasons, and tool calls it used.
| Field | Type | Description |
| -------------------------------------- | ---------- | ------------------------------------------------------ |
| `id`, `name`, `status` | `string` | Trace id, name, and status (`ok`, `error`, or `unset`) |
| `errorCount`, `spanCount` | `number` | Counts within the trace |
| `duration`, `timeToFirstToken` | `number` | Nanoseconds |
| `cost`, `tokens` | `object` | Same shape as the session totals |
| `models`, `providers`, `finishReasons` | `string[]` | What the trace used |
| `tools` | `array` | Tool calls in the trace (see below) |
Each tool call has this shape:
| Field | Type | Description |
| ----------------- | --------- | ---------------------------------------------------------------------- |
| `name` | `string` | The tool's name |
| `input`, `output` | `string` | The call's arguments and result, possibly truncated for large payloads |
| `error` | `boolean` | `true` if the call failed |
| `duration` | `number` | Nanoseconds |
```js theme={"theme":{"light":"github-light","dark":"github-dark"}}
// Match sessions where any tool call failed.
const failed = session.traces.some((t) => t.tools.some((tool) => tool.error))
return failed ? Passed() : Failed()
```
## Built-in functions
Alongside the score helpers, a few functions are always available, and two more appear only when your script calls them.
### semanticSimilarity(query)
Returns the highest cosine similarity between `query` and any message in the session, from `0` to `1` (0 when the session has no messages). It is async.
```js theme={"theme":{"light":"github-light","dark":"github-dark"}}
const score = await semanticSimilarity('the user is angry or frustrated')
return score >= 0.55 ? Passed(score) : Failed(score)
```
Latitude reuses the message embeddings computed when the session was ingested and embeds only your query, so this stays cheap. As a starting point, about 0.4 is broad, 0.55 is balanced, and 0.7 is strict. A session that hasn't been embedded yet is scored once its embeddings are ready.
### llm(prompt, options)
Sends a prompt to an LLM and returns a structured object matching `schema`. Latitude manages the model and the system prompt. `schema` is required and must be built with `z` (below). It is async.
```js theme={"theme":{"light":"github-light","dark":"github-dark"}}
const result = await llm(
`Did the assistant promise a refund in this conversation?\n${session.conversation}`,
{ schema: z.object({ promised: z.boolean(), quote: z.string() }) },
)
return result.promised ? Passed(1, result.quote) : Failed()
```
### z
A schema builder for shaping `llm()` output and `parse()` input. It mirrors a subset of Zod: `z.string()`, `z.number()`, `z.boolean()`, `z.literal()`, `z.enum()`, `z.array()`, `z.object()`, and `z.union()`, with `.optional()`, `.nullable()`, `.describe()`, `.min()`, `.max()`, and `.int()`.
### parse(value, schema)
Validates a value against a `z` schema and returns it, or throws if it doesn't match. Useful for checking JSON your agent produced.
```js theme={"theme":{"light":"github-light","dark":"github-dark"}}
const messages = session.conversation
const output = messages.length ? messages[messages.length - 1].content : ''
try {
parse(JSON.parse(output), z.object({ answer: z.string(), confidence: z.number() }))
return Failed(0, 'output matched the expected schema')
} catch {
return Passed(1, 'output was missing or malformed')
}
```
Call `llm` and `semanticSimilarity` by name in your source. Latitude reads those names to allocate the right resources and to make the functions available, so building the call dynamically won't work.
### What isn't available
The sandbox is deliberately small. There is no network access, no `fetch`, no timers, no `Date.now` or `Math.random`, and no Node or browser APIs. The only import allowed is `zod`. A script's verdict depends only on the session and any `llm()` call, which keeps results reproducible.
## Where scripts run and their limits
Detectors run in the background, never inside your app's request path. When a session finishes, Latitude runs the matching detectors on it. A preview runs the same way, over recent sessions, on demand.
Latitude picks a resource budget from what your script uses:
| The script | Time budget |
| ----------------------------------------------- | ----------------- |
| Uses neither `llm()` nor `semanticSimilarity()` | about 1 second |
| Calls `semanticSimilarity()` | about 15 seconds |
| Calls `llm()` | about 120 seconds |
Every script gets 64 MiB of memory. These limits are enforced automatically and are generous for typical detectors.
Errors are handled at two points:
* Invalid JavaScript is caught when you save, so a script that won't compile can't be stored.
* A script that throws at runtime, or exceeds its time or memory budget, scores that session as `errored`. Errored sessions show up in the preview. A detector that errors often is flagged as unhealthy.
LLM and embedding calls cost money, so use the sampling control in the [Scope step](../signals/create#scope) to check a slice of traffic rather than every session.
## Editing and detaching
If you build a detector with conditions or an LLM judge, the Custom script tab shows the exact script Latitude compiled from your settings, read-only. Choose "Edit as custom script" to take it over by hand. This clears the conditions or criteria form and switches the detector to your script.
It is a one-way move. Once the script is the source of truth, the settings form is gone. Switching between the conditions and judge tabs before you detach keeps both drafts, so detach only when you're ready to hand-write.
## Related pages
* [Detection methods](./detection-methods): conditions and LLM judges, the other two ways to define a detector
* [Create a signal](../signals/create): the full creation flow
* [Scores](../scores/overview): what a detector produces when it matches
* [Sessions](../observability/sessions): what a session is
# Detection methods
Source: https://docs.latitude.so/evaluations/detection-methods
The three ways an evaluation decides whether a session matches a signal: a set of conditions, an LLM judge, or a custom script.
When you create a signal, you define how Latitude decides whether a session belongs to it. That check is the signal's evaluation, and you can build it three ways.
| Method | How it decides | Cost | Good for |
| --------------------------------------- | ----------------------------------------------- | -------------------------------- | ------------------------------------------------------------- |
| [Set of conditions](#set-of-conditions) | Deterministic checks on facts about the session | Free, instant | Concrete facts: a phrase, a failed tool, latency over a limit |
| [LLM as judge](#llm-as-judge) | An LLM reads each session and decides | One LLM call per checked session | Fuzzy behavior: tone, frustration, made-up answers |
| [Custom script](#custom-script) | JavaScript you write | Depends on the script | Logic the other two can't express |
All three produce the same kind of detector, and a signal has one active detector at a time. You can switch methods while you build or edit a signal.
## Set of conditions
A set of conditions checks concrete facts about a session. Each condition is a small deterministic test. Conditions are free and run instantly, so this is the method to start with.
Add up to 10 conditions and choose how they combine:
* **All**: every condition must hold (AND).
* **Any**: at least one condition must hold (OR).
The match selector appears once you have two or more conditions. A single condition just has to hold.
### Condition types
| Condition | What it checks | Options |
| ------------------- | ------------------------------------------------------------------------------ | ------------------------------------------ |
| Text match | A message contains, doesn't contain, matches, or doesn't match text or a regex | Where; operator; value; case sensitive |
| Semantic similarity | A message is semantically close to a query | Query; sensitivity, or a custom threshold |
| Empty output | The assistant produced no output | None |
| Output length | The assistant output's length compared to a value | Characters or words; operator; value |
| JSON output | The assistant output is valid or invalid JSON | Valid or invalid |
| Metric | A session or trace metric compared to a value | Metric; aggregation; operator; value |
| Tool used | A specific tool was called | Tool name |
| Tool failed | A tool call ended with an error status | Tool name (optional; empty means any tool) |
| Tool call count | How many tool calls happened | Operator; value |
| Error | The trace or session ended in an error state | None |
| Finish reason | The model's finish reason, such as stop, length, or tool\_calls | Value |
A few details are worth knowing:
* **Where** (for text match) picks which messages to look at: the last assistant message, any assistant message, any user message, any tool message, or the whole conversation.
* **Comparison operators** read as "greater than", "at least", "less than", and "at most".
* **Metric** works in display units: duration in milliseconds, cost in dollars. The available metrics are duration, cost, total or input or output tokens, error count, trace count, and span count. Aggregation decides whether a metric is summed across the session or compared per trace.
* **Tool failed** and **Error** look at the error status of a tool call or trace, not at the content of a tool's output. A tool that returns the word "error" in an otherwise normal result does not count as failed.
* **Semantic similarity** compares messages to a query using the embeddings Latitude already computed for the session. It is the one condition that isn't instant, because it works on embeddings.
### Semantic similarity sensitivity
The sensitivity presets map to a similarity threshold:
| Preset | Threshold |
| -------- | --------- |
| Broad | 0.40 |
| Balanced | 0.55 |
| Strict | 0.70 |
Broad matches loosely and catches more. Strict matches only close paraphrases. Start with Balanced and adjust from the preview, or set an exact threshold and operator under Advanced.
## LLM as judge
An LLM judge reads each matching session and decides whether the behavior is present, with a short reason. You describe the behavior in plain language in the "A session matches when..." field:
> the user got frustrated, repeating themselves, complaining, or giving up before getting a useful answer
Use a judge for behavior that is hard to pin to a fixed rule: tone, frustration, whether the answer actually resolved the request, or whether the model made something up. Latitude writes the prompt and manages the model, so you only supply the criteria.
Each check sends a session to an LLM, which costs money and takes longer than a condition. On high traffic, lower the sampling rate in the [Scope step](../signals/create#scope) so a judge checks a representative slice instead of every session.
## Custom script
A custom script is JavaScript you write that reads a session and returns a score. It can express anything conditions and judges can, plus logic they can't, such as combining several checks or inspecting tool arguments.
The [Custom scripts](./custom-scripts) page documents the full API: the `session` object, the built-in functions, and the limits scripts run under.
## Choosing a method
* If the behavior is a concrete fact you can name, use a set of conditions. It is free, fast, and easy to reason about.
* If the behavior is fuzzy or semantic, use an LLM judge.
* If you need logic the builder can't express, write a custom script.
You aren't locked in. Editing a signal lets you switch methods, and the Custom script tab always shows the script your conditions or criteria compiled to, so you can start simple and take over by hand later.
## Related pages
* [Create a signal](../signals/create): where you pick a method and scope the detector
* [Custom scripts](./custom-scripts): the full scripting reference
* [Triggers](./triggers): scope and sampling for a detector
* [Evaluations overview](./overview): how detectors fit the wider evaluation model
# Evaluations overview
Source: https://docs.latitude.so/evaluations/overview
Understand how evaluations score traffic to monitor your agent's quality over time.
# Evaluations overview
An evaluation is an automated detector that scores sessions as they arrive. It watches for one behavior or quality criterion, runs on completed traffic, and produces a [score](../scores/overview) each time it checks a session. Those scores feed the same analytics, signal, and alignment workflows as annotations and flaggers.
Every signal is backed by an evaluation. When a signal's evaluation matches a session, that session joins the signal.
## What an evaluation has
* A name and description: the behavior being detected.
* A detection method: how it decides whether a session matches. See [Detection methods](./detection-methods).
* A trigger: which sessions it runs on, and at what sampling rate. See [Triggers](./triggers).
## How an evaluation runs
1. A session completes in your project.
2. Latitude checks it against each active evaluation's scope and sampling.
3. Matching evaluations score the session.
4. Each returns a pass or fail verdict with feedback, stored as a score.
5. A passing score adds the session to the evaluation's signal.
`passed = true` means the behavior is present, not that the session was good. A signal for a bad behavior passes when that behavior happens.
## Where evaluations come from
An evaluation can be created two ways.
### Generated from a signal
When Latitude discovers a signal, or when you choose to monitor one, it can generate an evaluation from the signal's description, example traces, annotations, and scores. You don't pick the method. Latitude builds a detector from the evidence and keeps it aligned to human judgment over time.
### Defined by you
When you [create a signal](../signals/create) yourself, you define its evaluation directly. You choose one of three [detection methods](./detection-methods):
* Set of conditions: deterministic checks, free and instant.
* LLM as judge: describe the behavior and let an LLM decide.
* Custom script: JavaScript for anything the other two can't express.
A detector you define runs exactly as written. It is not automatically realigned to annotations the way a generated one is. See [Alignment](./alignment).
## Choosing a detection method
Clear structural failures, such as tool errors, empty responses, or latency over a limit, are a good fit for a set of conditions. Semantic behavior, such as relevance, tone, or whether an answer resolved the request, usually needs an LLM judge. When neither fits, a custom script gives you full control. See [Detection methods](./detection-methods) for the full catalog.
## Evaluation lifecycle
* Active: scoring matching sessions in real time.
* Paused: sampling set to `0`, configuration preserved.
* Archived: read-only and no longer scoring new sessions.
* Deleted: removed from management views, while historical results stay in analytics.
## Next steps
* [Detection methods](./detection-methods): the three ways an evaluation decides
* [Custom scripts](./custom-scripts): the scripting reference
* [Triggers](./triggers): scope and sampling
* [Alignment](./alignment): how evaluations stay calibrated to human judgment
* [Signals](../signals/overview): how evaluation matches become tracked signals
# Evaluation triggers
Source: https://docs.latitude.so/evaluations/triggers
Configure which sessions an evaluation runs on, and how many.
# Evaluation triggers
An evaluation's trigger decides which sessions it runs on and how many of them. It controls monitoring scope and cost without changing how the evaluation decides a match.
## Scope: which sessions to check
By default an evaluation runs on every session in your project. Narrow it with filters. When you [create a signal](../signals/create), the Scope step offers these dimensions:
* Tags
* Services
* Models
* Providers
* Metadata (any `metadata.*` key your app sends)
With filters set, only matching sessions run through the evaluation, and everything else is skipped. An empty filter means every session.
Scope uses the same shared filter system as trace views and [saved searches](../search/saved-searches), so a filter you build for search translates directly to an evaluation's scope. You can also open the builder pre-scoped from a search, using "Create signal from this search."
## Sampling: how many to check
Sampling is the percentage of matching sessions the evaluation actually runs on, from 0 to 100.
* It defaults to 10 percent for a new signal.
* Setting it to 0 pauses the evaluation. The configuration is kept, but no sessions are checked.
* A [set of conditions](./detection-methods#set-of-conditions) is free and instant, so 100 percent is usually fine. An LLM judge, or a script that calls an LLM, costs money and time per check, so a lower rate keeps costs down while still catching the pattern on a high-traffic project.
## Timing
Latitude runs an evaluation as sessions complete, so it acts on finished work rather than partial executions. The exact turn it runs on, and any debouncing for multi-turn sessions, are handled for you. You set the scope and the sampling rate, and Latitude manages the rest.
## Scope, search, and annotations
Scope and sampling control automated monitoring. [Search](../search/overview) and [annotations](../annotations/overview) cover human review: use search to inspect relevant sessions, then annotate the ones that need human judgment for alignment or discovery. [Flaggers](../annotations/flaggers) add automatic signal for a fixed list of common categories.
## Next steps
* [Detection methods](./detection-methods): how an evaluation decides a match
* [Alignment](./alignment): how human annotations calibrate evaluations
* [Evaluations overview](./overview): how evaluations work
* [Search](../search/overview): build cohorts of sessions to review
# Experiments
Source: https://docs.latitude.so/experiments/overview
Compare two or more slices of your data side by side — every metric across sessions, users, tools, signals, behaviours and more.
**Where this fits:** Experiments are part of the **Understand** section. A [search](../search/overview) answers "show me these sessions"; an experiment answers "how do these slices differ from each other?"
An **Experiment** is a side-by-side dashboard. You define a **baseline** slice of your data and one or more comparison slices, and Latitude computes every metric it already tracks for each slice, then shows how each comparison moved relative to the baseline — greener when a change is good, redder when it's bad.
## Variants
Each slice is a **Variant**: a saved population selector made of three things.
* **Filters** — the same session filters you use on the dashboard (model, status, user, tool, metadata, duration, and so on).
* **Search query** — an optional free-text or semantic query.
* **Time range** — a preset (last day, last week, last month…) or a fixed calendar window. Presets stay live, so a "last 7 days" variant always covers the trailing week.
## Baseline
You can mark one Variant as the **baseline**. Every other Variant's metrics are compared against it, allowing you to see easily how your new changes compare to previous versions of your agent.
## What gets compared
For every Variant, an experiment computes the full set of analytics Latitude produces, grouped by type:
* **Sessions** — count, distinct users, total and average cost and tokens, error rate, cache hit rate, and duration percentiles.
* **Users** — distinct users and per-user rollups (sessions, traces, cost, duration, error rate).
* **Tools** — calls, distinct tools, usage rate, error rate, and duration percentiles, plus the top tools.
* **Signals** — distinct signals, occurrences, affected sessions/traces/users, and cost impact, plus the top signals.
* **Behaviours** — observations, distinct clusters, and detected moments, plus the top behaviours.
## Population deviation
A Variant whose population differs from the baseline by more than 25% (in session or user count) is flagged, since very different population sizes make the comparisons less reliable.
## Semantic queries
A semantic search query returns a ranked sample rather than an exact set, so any Variant whose query has a semantic component is marked **approximate** and its metrics are best-effort. Filter-only and literal/phrase queries are exact.
## Creating an experiment
From a project, open **Experiments** and create one with a name and optional description. A new experiment starts with two variants (a baseline and one comparison) so it's useful immediately; edit their filters, query, and time range, then read the comparison. "Import from search" seeds a Variant from a saved search's filters and query.
# CLI
Source: https://docs.latitude.so/getting-started/cli
Install and use the Latitude CLI to manage your Latitude organization right from the terminal
## Overview
The **`latitude` CLI** is a single, self-contained binary that exposes your Latitude organization on the command line. Like the [MCP server](/getting-started/mcp), its commands are **generated directly from the Latitude API**, so the command surface automatically stays in sync with the platform. For the live list of commands and their input/output schemas, check the [API reference](https://api.latitude.so/docs).
It's built for two audiences:
* **Humans**, a fast, scriptable way to inspect and manage projects, traces, datasets, members, keys, and more without leaving the terminal.
* **Agents**, a zero-dependency way for an agent (Claude Code, Cursor, Codex, …) to act on Latitude without wiring up an MCP connection, using `--format json` for machine-readable output and `--schema` for machine-readable help.
Prefer a network connection? The [MCP server](/getting-started/mcp) exposes the same capabilities over OAuth.
## Installation
The CLI ships as a pre-built binary on our [GitHub Releases](https://github.com/latitude-dev/latitude-llm/releases) — grab the latest `cli-vX.Y.Z` release. Download the archive for your platform, extract it, and put the `latitude` binary somewhere on your `PATH`.
| OS | Architecture |
| ------- | ----------------------- |
| Linux | x86-64 (`amd64`) |
| Linux | ARM64 (`aarch64`) |
| macOS | Intel (`amd64`) |
| macOS | Apple Silicon (`arm64`) |
| Windows | x86-64 (`amd64`) |
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
# Set and pick the asset for your OS/arch: latitude--.tar.gz
curl -fsSL -o latitude.tar.gz \
https://github.com/latitude-dev/latitude-llm/releases/download/cli-/latitude-macos-arm64.tar.gz
tar -xzf latitude.tar.gz
chmod +x latitude
sudo mv latitude /usr/local/bin/latitude # or any directory on your PATH
latitude --version
```
On MacOS the first run may be blocked by Gatekeeper. Allow it with `xattr -d com.apple.quarantine /usr/local/bin/latitude`, or via **System Settings → Privacy & Security**.
Download `latitude-windows-amd64.zip`, extract `latitude.exe`, and move it to a directory on your `PATH` (or add its folder to `PATH`). Then:
```powershell theme={"theme":{"light":"github-light","dark":"github-dark"}}
latitude --version
```
Then verify it works and, optionally, set up [shell completion](#shell-completion) and the [man page](#man-page).
## Authentication
The CLI authenticates with an **organization-scoped API key**. Create one in the Latitude UI under **Settings → Keys → API Keys**. There are two ways to give the key to the CLI:
Store the key once in your operating system's secret store (Keychain on MacOS, Secret Service on Linux, Credential Manager on Windows):
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
latitude auth login
```
You'll be prompted to paste your API key; it's saved to the keyring under `latitude:ApiKeyAuth` and reused on every subsequent command. Manage it with:
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
latitude auth status # show which credential sources are set, for each scheme
latitude auth logout # remove the stored key from the keyring
```
Export the key in your shell — handy for CI or ephemeral environments:
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
export LATITUDE_API_KEY="lat_..."
latitude projects list
```
`LATITUDE_API_KEY` takes precedence over the keyring, so it's easy to override the stored credential per-session. `latitude auth status` shows every source the CLI can see and which one wins.
## Usage
The general shape is `latitude [flags]`, where each resource mirrors an area of the Latitude API:
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
latitude --help # list every resource
latitude projects --help # list the commands for a resource
latitude projects list # run one
latitude projects create --help # discover a command's flags/arguments
```
Resources include `projects`, `traces`, `datasets`, and more — run `latitude --help` for the full list. Common examples:
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
latitude account get # the current org + your role
latitude projects list # every project in the org
latitude projects create --name "My App" # create a project
latitude traces list --help # inspect traces (project-scoped — see --help for the flags)
latitude api-keys list
```
A few global flags worth knowing (run `latitude --help` for the full list):
* `--query ` — project/filter the response before it's formatted, e.g. `--query "items[].slug"`.
* `--dry-run` — validate the request locally without sending it to the API.
* `--quiet` / `-q` — suppress success output (errors still print to stderr).
* `--debug` — dump the raw HTTP request and response to stderr.
## Output formats
Every command accepts `--format` to control how results are rendered. This makes the CLI equally good for humans reading a terminal and for scripts or agents parsing output.
| Format | Description |
| ------- | ------------------------------------------------------------------------------------------------------------------------- |
| `table` | Human-readable table. **Default when stdout is a TTY.** |
| `json` | Pretty JSON. **Default when the output is piped.** |
| `jsonl` | Newline-delimited JSON (NDJSON) — one compact value per line; arrays are flattened. Ideal for streaming into other tools. |
| `yaml` | YAML. |
| `csv` | CSV — convenient for spreadsheets. |
| `raw` | The unmodified server response bytes. |
| `http` | The full HTTP response (status line + headers + body), like `curl -i`. |
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
latitude projects list --format json
latitude projects list --format csv > projects.csv
latitude traces list --format jsonl | jq . # stream trace-by-trace
# Combine with --query to reshape before formatting
latitude projects list --query "items[].{slug: slug, name: name}" --format yaml
```
You can also set a default format for a session with the `LATITUDE_OUTPUT` environment variable (e.g. `export LATITUDE_OUTPUT=json`); the `--format` flag always overrides it.
## Environment variables
| Variable | Purpose |
| ----------------------- | -------------------------------------------------------------- |
| `LATITUDE_API_KEY` | API key used to authenticate (alternative to the keyring). |
| `LATITUDE_OUTPUT` | Default output format when `--format` is omitted. |
| `LATITUDE_BASE_URL` | Override the API base URL (e.g. self-hosted or a mock server). |
| `LATITUDE_TIMEOUT_SECS` | Total request timeout, in seconds. |
| `LATITUDE_PROXY` | HTTP(S) proxy URL. |
| `LATITUDE_CA_BUNDLE` | Path to a PEM file with extra trust roots. |
| `LATITUDE_INSECURE=1` | Skip TLS verification (debugging only). |
The standard `HTTPS_PROXY` / `HTTP_PROXY` / `NO_PROXY` / `SSL_CERT_FILE` variables are honored as well.
## Shell completion
Generate a completion script for your shell — `bash`, `zsh`, `fish`, `powershell`, or `elvish` — and load it to get tab-completion for every resource, command, and flag:
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
latitude completion zsh > ~/.latitude-completion.zsh
echo 'source ~/.latitude-completion.zsh' >> ~/.zshrc
```
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
latitude completion bash | sudo tee /etc/bash_completion.d/latitude > /dev/null
```
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
latitude completion fish > ~/.config/fish/completions/latitude.fish
```
## Man page
The CLI can emit its own manual page in roff format, so `man latitude` works like any native tool:
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
latitude man > latitude.1
man ./latitude.1
# or install it system-wide:
sudo mv latitude.1 /usr/local/share/man/man1/ && man latitude
```
## For Agents
The CLI pairs naturally with agents: `--format json`/`jsonl` for structured output, `--schema` for machine-readable help, and `latitude generate-skills` to teach an agent the command surface — everything it needs to drive Latitude locally with no MCP connection. Prefer the [MCP server](/getting-started/mcp) when you'd rather connect the agent over the network with OAuth.
# Core Concepts
Source: https://docs.latitude.so/getting-started/concepts
Latitude's core concepts: projects, spans, traces, sessions, users, tools, search, behaviours, scores, signals, evaluations, and monitors.
# Core Concepts
Latitude is organized around a small set of concepts, grouped by the loop you work in: **Observe** what your agent did, **Understand** what matters, and **Refine** with monitoring and tests.
```text theme={"theme":{"light":"github-light","dark":"github-dark"}}
Project
├─ Observe
│ ├─ Sessions
│ │ └─ Traces
│ │ └─ Spans (LLM calls, tool calls, retrieval, and more)
│ ├─ Users
│ └─ Tools
├─ Understand
│ ├─ Search and Behaviours over traces
│ ├─ Scores attached to traces
│ └─ Signals grouped from failed scores, or defined by you
└─ Refine
├─ Evaluations that score live traffic
└─ Monitors that alert on signals, searches, or raw traffic
```
## Projects
A **project** is the main workspace for one agent, application, or AI feature. Projects keep production data and reliability workflows scoped to the right product boundary.
Project-scoped data includes:
* spans, traces, and sessions
* users and tools
* search, saved searches, and behaviours
* scores from annotations, evaluations, flaggers, and custom checks
* signals grouped from failed scores
* evaluations and monitors
* project settings such as monitoring behaviour
Projects belong to organizations. Organization membership, API keys, and settings control who can access a project and how defaults cascade.
## Spans
A **span** is the smallest unit of captured work. It represents one operation inside an agent run, such as an LLM call, tool call, retrieval step, HTTP request, or custom instrumented operation.
Spans can capture:
* input and output content when available
* start time, end time, and duration
* success or error status
* tags and metadata
* provider, model, token usage, and cost for LLM calls
Use spans when you need to understand exactly what happened inside a trace.
## Traces
A **trace** is one complete interaction from start to finish, composed of one or more spans. For a chat agent, a trace commonly represents a single user turn and the agent work needed to respond.
Traces are the main unit used across Latitude:
* the trace detail view shows the full execution path
* search returns matching traces
* annotations, scores, and evaluations attach to traces
* flaggers run on completed traces
* failed scores on traces feed signal discovery
Latitude waits for a trace to stop receiving new spans before treating it as complete. Downstream workflows such as search indexing, flaggers, evaluations, scores, and signal discovery run on completed traces so they do not act on partial executions.
## Sessions
A **session** groups related traces into a multi-turn conversation or workflow. Traces always exist; sessions exist when your application sends a stable session id with telemetry.
Use sessions when multiple traces belong to the same user conversation, support ticket, workflow, or agent task. For example, each chat turn can be one trace while the whole conversation shares one session.
## Users
A **user** is an end user of your agent, identified by the `userId` you send with telemetry. The Users view groups everything one user did: their sessions, errors, cost, and activity over time, along with the signals affecting them. Use it to see who is impacted by a problem and who is most active.
## Tools
A **tool** is a function your agent can call. Latitude discovers every tool from your telemetry and tracks per-tool usage, error rate, and latency in the Tools view, so you can see which tools are heavily used, failing, or never called.
## Search
**Search** finds traces by meaning, exact text, metadata filters, or a combination of all three. It is the discovery layer for behaviours your team wants to inspect, review, and track. Saved searches let teams bookmark useful cohorts and review them repeatedly.
Use search to find traces such as:
* users showing frustration
* failed payments or onboarding problems
* tool loops and tool call errors
* hallucinated or incomplete answers
* traces from a specific user, model, release, environment, tag, or metadata value
## Behaviours
A **behaviour** is a topic Latitude automatically discovers by clustering your sessions by meaning. Behaviours surface what your users are actually doing, with trends and outcome metrics, so you find patterns you did not know to search for. Where search starts from a query, behaviours start from a map of your traffic.
## Scores
A **score** is Latitude's common verdict model for trace quality. Scores can come from human annotations, built-in flaggers, automated evaluations, or custom checks submitted through the API.
Scores answer questions such as:
* Did this trace pass or fail a quality check?
* What feedback explains the verdict?
* Which source produced the verdict?
* Should this failure contribute to signal discovery?
Failed, eligible scores are the primary input for signal discovery. Scores also power analytics and help teams compare automated judgments with human feedback.
## Signals
A **signal** is a recurring failure pattern Latitude tracks. Instead of leaving failures as isolated examples, Latitude groups similar failed scores into named signals with example traces, trends, affected users, lifecycle states, and linked evaluations. You can also define a signal yourself when you already know the behavior to track. See [Create a signal](../signals/create).
Signals created from negative human annotations are also called **issues**; the word is still used in that narrower context.
Signals help teams:
* triage new and escalating production problems
* inspect representative traces for the failure pattern
* decide whether a pattern is real, noise, resolved, or ignored
* generate evaluations that monitor the same failure mode on future traces
* catch regressions after a fix ships
Signal states are **new**, **escalating**, and **ongoing**. A signal can have more than one at once, such as new and escalating.
## Evaluations
An **evaluation** is an automated monitor that checks traces for a specific behaviour or quality criterion. Evaluations run on completed traces and produce scores, so their results feed the same analytics and signal discovery workflows as annotations, flaggers, and custom checks.
Evaluations help teams:
* monitor known failure modes on live traffic
* measure whether a fix reduced or eliminated a behaviour
* catch regressions after a signal is resolved
* turn important signals into ongoing monitors
* compare automated judgments with human annotations through alignment metrics
Latitude can generate evaluations from signals, then use human feedback to keep those evaluations aligned with the team's judgment.
## Monitors
A **monitor** watches a target, such as a signal, a saved search, a tool, or your raw traffic, and opens an **incident** when a condition is met. When an incident opens, Latitude notifies you in-app, by email, or in Slack. Every project comes with system monitors that watch the signal lifecycle, and you can create your own.
## How the concepts fit together
1. A **project** receives telemetry from your agent.
2. Agent operations arrive as **spans**.
3. Related spans form completed **traces**, and related traces group into **sessions**.
4. **Search** and **Behaviours** help you find the traces and topics that matter; **Users** and **Tools** break the same activity down by end user and by tool.
5. Annotations, flaggers, **evaluations**, and custom checks create **scores** on traces.
6. Failed scores cluster into **signals** you can triage, monitor, and fix, and you can also define a signal yourself. Signals from negative annotations are called issues.
7. **Monitors** watch signals (and searches, tools, or raw traffic) and open incidents that alert you when something needs attention.
## Next steps
* [Start tracing](../telemetry/start-tracing): send your first spans and traces to Latitude.
* [Observability](../observability/overview): inspect traces, sessions, users, and tools.
* [Search](../search/overview) and [Behaviours](../behaviours/overview): find the traffic that matters.
* [Signals](../signals/overview): understand how failed scores become tracked signals.
* [Monitors](../monitors/overview): get alerted when something needs attention.
# How to use Latitude
Source: https://docs.latitude.so/getting-started/how-to-use-latitude
The recommended workflow for finding, tracking, and fixing production failures in your AI agent.
Latitude works best as a continuous loop for production agents: observe real traffic, understand what is going wrong, and refine your agent until it is fixed and stays fixed. The product is organized around that loop: **Observe, Understand, Refine.**
## The core workflow
Send traces from your agent to Latitude. Each interaction becomes a trace of spans (LLM calls, tool calls, retrieval, and more), and multi-turn conversations group into [sessions](../observability/sessions). Send a `userId` and `sessionId` so you can also break activity down per [user](../observability/users) and review reliability, errors, and latency per [tool](../observability/tools).
If you have not connected your app yet, follow [Start tracing](../telemetry/start-tracing).
Use [Search](../search/overview) to find conversations by meaning, exact text, or metadata filters: frustrated users, tool loops, hallucinations, failed workflows, or anything specific to your product. [Behaviours](../behaviours/overview) goes further, automatically clustering your sessions into the topics users actually hit, so you discover patterns without writing a query.
Open traces from search results, behaviours, or the trace list and leave [annotations](../annotations/overview). A thumbs-down with clear feedback tells Latitude this behaviour is worth tracking. [Flaggers](../annotations/flaggers) also annotate common failure categories automatically, such as frustration, refusal, jailbreaking, tool errors, and empty responses.
Failed annotations, flagger matches, evaluation failures, and custom scores become [scores](../scores/overview). Latitude groups similar failures into named, prioritized [signals](../signals/overview), each with example traces, affected-user counts, trends, and a lifecycle. Signals created from negative annotations are called issues.
Triage signals on the Signals page: set priority, inspect example traces, and resolve noise. [Monitors](../monitors/overview) watch a signal, a saved search, a tool, or your raw traffic and open an incident when something needs attention, notifying you in-app, by email, or in Slack. Generate [evaluations](../evaluations/overview) to keep scoring live traffic for the same failure.
Fix the underlying behaviour in your code, prompts, tools, retrieval, or product flow. Turn the failing traces into a [dataset](../datasets/overview) and [regression test](../test-and-fix/regression-testing) the fix, using the [MCP server](./mcp) to drive it from your coding agent so the failure cannot quietly return. Resolve the signal once it is fixed, and the regressed monitor tells you if it comes back. Repeat the loop as new production traffic arrives.
## What to focus on first
If you are setting up Latitude for the first time:
1. Connect tracing for one production agent.
2. Add `userId` and `sessionId` so traces group by user and conversation.
3. Search for one failure mode your team already cares about, or browse Behaviours to see what stands out.
4. Annotate representative traces with specific feedback.
5. Watch the Signals page for grouped patterns, and let the built-in monitors notify you.
6. Generate evaluations for the signals you want to score continuously.
## Work with agents and self-host
* **MCP**: manage your workspace straight from Claude, Cursor, and other agents through the [MCP server](./mcp).
* **Self-hosting**: run Latitude in your own infrastructure, from a [single host](../deployment/overview) to a full cluster.
## Why this works
Latitude does not require you to define every possible failure upfront. You discover failures from real traffic, validate them with human review, and turn important patterns into automated monitoring. Over time, the system becomes a living map of what goes wrong in your agent and whether your fixes are working.
# Latitude Overview
Source: https://docs.latitude.so/getting-started/introduction
Understand what Latitude is, why teams use its MIT-licensed open-source platform, and how Observe, Understand, and Refine fit together.
# Latitude Overview
Latitude is an **open-source, MIT-licensed** platform for improving production AI agents. It helps teams capture real agent traffic, understand what is going wrong, and turn repeated failures into tracked signals you can monitor and fix.
The product is organized around the loop you work in every day: **Observe, Understand, Refine**.
## Observe
Latitude starts by collecting agent telemetry. Each interaction becomes a trace made of spans for LLM calls, tool calls, retrieval steps, and other instrumented work. Related traces are grouped into sessions so you can review full multi-turn conversations.
Observe lets you inspect:
* traces, spans, and sessions
* model providers, models, tokens, latency, cost, and errors
* tool calls and nested execution paths
* per-user activity in the **Users** view, and per-tool reliability, errors, and latency in the **Tools** view
* tags, metadata, environments, and releases
Learn more in [Observability](../observability/overview), [Sessions](../observability/sessions), [Users](../observability/users), and [Tools](../observability/tools).
## Understand
Once traffic is flowing, Understand helps you find what matters and group it into something you can track.
* **Search** finds behaviours across production traces by meaning, exact text, or metadata filters, so you can move from a broad question to a focused set of real examples.
* **Behaviours** clusters your sessions by meaning into the topics users actually hit, with trends and outcome metrics, so you discover what matters without writing a query.
* **Signals** are the recurring failure patterns Latitude groups from failed scores. Human annotations, flaggers, evaluations, and custom checks all feed in, and each signal carries example traces, affected-user counts, trends, and a lifecycle your team can act on.
Learn more in [Search](../search/overview), [Behaviours](../behaviours/overview), and [Signals](../signals/overview).
## Refine
Refine turns what you learned into guardrails. **Monitors** watch a signal, a saved search, or your raw traffic and open an incident when something needs attention, notifying you in-app, by email, or in Slack. Evaluations generated from a signal keep scoring live traffic, so you catch regressions after you ship a fix.
Learn more in [Monitors](../monitors/overview) and [Evaluations](../evaluations/overview).
## Why Latitude?
AI agents fail in ways that are hard to predict upfront. Logs and dashboards show individual events, but teams also need to understand repeated behavioural failures: what users experienced, how often it happened, who was affected, and whether a fix prevented it from coming back.
Latitude is built around that loop:
* **Agent-native telemetry**: inspect multi-step agent behaviour, not just isolated API calls.
* **Semantic discovery**: search by meaning, and let Behaviours surface patterns you did not know to look for.
* **Signal-centric workflow**: turn failures into named, trackable problems instead of scattered examples.
* **Human-aligned monitoring**: annotations, evaluations, and scores work together so automated monitoring stays connected to human judgment.
* **Open source and MIT licensed**: run, inspect, self-host, fork, and contribute to the platform your team depends on.
## How Latitude is different
Most LLM observability platforms focus on one narrow part of the production quality loop. Tools such as Langfuse, Datadog, Sentry, and PostHog are useful for visibility, evaluation, analytics, or feedback workflows, but they usually leave the work of turning scattered findings into trackable AI-agent signals to the team using them.
Latitude is built around **LLM signal discovery**. Observability, semantic search, behaviours, annotations, flaggers, scores, and evaluations are designed to work together automatically so teams can track production agent failures with minimal manual setup.
It is also **open source**, where most tools in this space are closed, proprietary SaaS. Latitude is MIT licensed and self-hostable, so you can run it in your own infrastructure, audit exactly how your data is handled, and adapt it to your needs. See [Open source](#open-source) below.
The difference is the combined workflow:
* **Flaggers** detect common failure categories automatically, such as frustration, refusal, jailbreaking, tool errors, and empty responses.
* **Search and Behaviours** help teams find product-specific failure modes across real conversations.
* **Annotations** turn human review into structured input that can create or refine signals.
* **Automatically human-aligned evaluations** convert important signal patterns into monitors that stay connected to human judgment.
* **Signals** bring those findings together into named, prioritized, lifecycle-managed production failure patterns.
Instead of only showing traces, only running evaluations, or only collecting annotations, Latitude combines these primitives into a signal-centric workflow for finding, tracking, fixing, and monitoring AI agent failures in production.
## Open source
Latitude is **fully open source under the MIT license**. The entire platform lives on [GitHub](https://github.com/latitude-dev/latitude-llm), so you can read the code, audit how your data is handled, fork it, and contribute.
You can run it two ways:
* **Self-hosted**: host Latitude yourself for free, from a [single host](../deployment/overview) to a full cluster. Your data stays in your own infrastructure.
* **Latitude Cloud**: the managed version, so you do not have to operate it.
Both run the same open-source platform, so you are never locked in. Contributions are welcome: pick up a [good first issue](https://github.com/latitude-dev/latitude-llm/contribute) or read the [contributing guide](../development/contributing).
## Getting started
* [Start tracing](../telemetry/start-tracing): connect your agent and send your first traces to Latitude.
* [How to use Latitude](./how-to-use-latitude): follow the recommended workflow for finding, tracking, and fixing production signals.
Latitude also exposes everything through an [MCP server](./mcp), so your coding agents can read and manage your workspace, and supports [self-hosting](../deployment/overview) from a single host to a full cluster.
## Community & contact
Latitude is open source, MIT licensed, and built with the community.
* [Open-source repository](https://github.com/latitude-dev/latitude-llm)
* [Report a bug or request a feature](https://github.com/latitude-dev/latitude-llm/issues)
* [Join the Slack community](https://join.slack.com/t/trylatitude/shared_invite/zt-35wu2h9es-N419qlptPMhyOeIpj3vjzw)
# MCP Server
Source: https://docs.latitude.so/getting-started/mcp
Connect AI agents like Claude, Cursor, Codex, and Zed to your Latitude organization through the Latitude MCP server
## Overview
The **Latitude MCP** is a remote, OAuth-authenticated, streamable HTTP [Model Context Protocol](https://modelcontextprotocol.io) server, living at `https://api.latitude.so/v1/mcp`.
It is the portal that AI agents — Claude, Cursor, Codex, Gemini, Zed, OpenCode, and others — can use to read and manage your Latitude workspace.
The MCP exposes everything you can do in the Latitude UI as tools — managing projects, members, keys, traces, annotations, scores, searches, signals, datasets, and more.
Tools are **dynamically generated** from the Latitude API, so the catalog automatically stays in sync with the platform. For the live list of tools, their descriptions, and their input/output schemas, you can check the [API reference](https://api.latitude.so/docs)
To see or revoke connected agents at any time, go to your organization's **Settings → Keys** and look at the **OAuth Keys** section. Revoking immediately cuts off the agent's access.
Prefer a local binary? The [Latitude CLI](/getting-started/cli) exposes the same capabilities from your terminal.
## Installation
The MCP is compatible with all the most common agents, harnesses or IDEs you use. We have built extensions for many of them, otherwise check the **Manual** section tab with the exact install steps. If your agent isn't listed below, tell your agent to add the Latitude MCP as a `Remote Streamable HTTP MCP server` with URL `https://api.latitude.so/v1/mcp` and `OAuth` enabled — those are the only two things the agent needs to know!
### Cursor Desktop
Go to **Cursor Settings → Tools & MCPs → Add Custom MCP**, or edit `~/.cursor/mcp.json` directly.
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
"mcpServers": {
"latitude": {
"url": "https://api.latitude.so/v1/mcp"
}
}
}
```
Go back to **Cursor Settings** and click **Connect** on the `latitude` MCP.
Sign in (if needed) and pick the organization you want this Cursor Agent to access in the consent screen.
### Cursor CLI
Edit `~/.cursor/mcp.json`.
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
"mcpServers": {
"latitude": {
"url": "https://api.latitude.so/v1/mcp"
}
}
}
```
Verify the Latitude MCP was added running:
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
agent mcp list
```
Run (outside of Cursor CLI):
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
agent mcp login latitude
```
Sign in (if needed) and pick the organization you want this Cursor Agent to access in the consent screen.
### Claude Code Desktop
Go to **Settings → Developer → Edit Config** or edit `~/Library/Application Support/Claude/claude_desktop_config.json` directly.
Claude Code Desktop does not support remote OAuth MCP servers natively, so we use the `mcp-remote` bridge.
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
"mcpServers": {
"latitude": {
"command": "npx",
"args": ["mcp-remote", "https://api.latitude.so/v1/mcp"]
}
}
}
```
After restart, Claude Code will open a browser tab to authenticate the Latitude MCP.
Sign in (if needed) and pick the organization you want this Claude Code Agent to access in the consent screen.
### Claude Code CLI
Run (outside of Claude Code):
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
claude mcp add --transport http latitude https://api.latitude.so/v1/mcp --scope user
```
Or edit `~/.claude.json` directly:
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
"mcpServers": {
"latitude": {
"type": "http",
"url": "https://api.latitude.so/v1/mcp"
}
}
}
```
Verify the Latitude MCP was added running:
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
claude mcp list
```
Run Claude Code (`claude`), type `/mcp`, select `latitude`, then **Authenticate**.
Sign in (if needed) and pick the organization you want this Claude Code Agent to access in the consent screen.
### Codex Desktop
Open Codex and go to **Settings → MCP Servers → Add Server**, or edit `~/.codex/config.toml`.
```toml theme={"theme":{"light":"github-light","dark":"github-dark"}}
[mcp_servers.latitude]
transport = "http"
url = "https://api.latitude.so/v1/mcp"
```
Restart Codex or click **Connect** on the `latitude` MCP.
Sign in (if needed) and pick the organization you want this Codex Agent to access in the consent screen.
### Codex CLI
Edit `~/.codex/config.toml`.
```toml theme={"theme":{"light":"github-light","dark":"github-dark"}}
[mcp_servers.latitude]
transport = "http"
url = "https://api.latitude.so/v1/mcp"
```
Run Codex (`codex`), type `/mcp`, select `latitude`, then **Authenticate**.
Sign in (if needed) and pick the organization you want this Codex Agent to access in the consent screen.
### Gemini CLI
Edit `~/.gemini/settings.json`.
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
"mcpServers": {
"latitude": {
"httpUrl": "https://api.latitude.so/v1/mcp"
}
}
}
```
Run Gemini (`gemini`), type `/mcp`, select `latitude`, then **Authenticate**.
Sign in (if needed) and pick the organization you want this Gemini Agent to access in the consent screen.
### Google Antigravity
Go to **Antigravity Settings → MCP servers**, or edit `~/.antigravity/mcp.json` directly.
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
"mcpServers": {
"latitude": {
"url": "https://api.latitude.so/v1/mcp"
}
}
}
```
Go back to **Antigravity Settings** and click **Connect** on the `latitude` MCP.
Sign in (if needed) and pick the organization you want this Antigravity Agent to access in the consent screen.
### Zed
Go to **Agent Settings → Model Context Protocol (MCP) Servers → Add Server → Add Custom Server → Remote**, or edit `~/.config/zed/settings.json` directly.
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
"context_servers": {
"latitude": {
"url": "https://api.latitude.so/v1/mcp"
}
}
}
```
Go back to **Agent Settings** and click **Authenticate** on the `latitude` MCP.
Sign in (if needed) and pick the organization you want this Zed Agent to access in the consent screen.
### OpenCode Desktop
OpenCode Desktop does not currently support adding MCP servers from the desktop UI. Instead, set it up via the [OpenCode CLI](#opencode-cli) steps. After a restart you can enable it typing `/mcp`.
### OpenCode CLI
Run (outside of OpenCode):
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
opencode mcp add
```
and answer the prompts with name `latitude`, type `remote`, url `https://api.latitude.so/v1/mcp`, oauth `yes`, client id `no`.
Or edit `~/.config/opencode/opencode.json` directly:
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
"mcp": {
"latitude": {
"type": "remote",
"url": "https://api.latitude.so/v1/mcp"
}
}
}
```
Verify the Latitude MCP was added running:
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
opencode mcp list
```
Run (outside of OpenCode):
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
opencode mcp auth latitude
```
Sign in (if needed) and pick the organization you want this OpenCode Agent to access in the consent screen.
# Self-Healing Agents
Source: https://docs.latitude.so/getting-started/self-healing-agents
Close the loop from detected production failure to reviewed pull request by dispatching your coding agent with the evidence attached.
A self-healing agent is one whose production failures reach a coding agent automatically, with the evidence attached, so remediation starts from the detection instead of from a person relaying context. Latitude closes that loop end to end: traffic is observed, recurring patterns become tracked [signals](../signals/overview), escalations dispatch your coding agent, and the fix lands as a pull request that a person reviews, backed by a regression test in your CI.
## How the loop closes
Connect [telemetry](../telemetry/start-tracing) so every interaction arrives as a trace and multi-turn conversations group into [sessions](../observability/sessions). Find failures with [Search](../search/overview) when you can describe them, or let [Behaviours](../behaviours/overview) surface the topics and trends you would not think to look for. [Annotations](../annotations/overview) and [flaggers](../annotations/flaggers) record what you find, and Latitude groups those into named, tracked [signals](../signals/overview).
Give a signal an [evaluation](../evaluations/overview), a script that can combine code rules, semantic similarity, and LLM judgment, and live traffic is scored as it arrives. The evaluation's [trigger](../evaluations/triggers) controls how much: generated evaluations start at a 10 percent sample, so raise the sampling and adjust the filters if the signal needs full coverage. Evaluations generated from your annotations carry an [alignment record](../evaluations/alignment), so how often the script agrees with a human reviewer is a measured number.
A signal escalates when its recent occurrences clear its own weekly rhythm, and [monitors](../monitors/overview) extend the same mechanism to saved searches and raw traffic metrics such as cost, latency, or cache hit rate. Either one opens an incident.
With [agent dispatch](../agent-dispatch/overview) configured, an escalating signal or an open incident wakes your coding agent with a prompt, a deep link, and sample failing traces. Latitude is the trigger and the context provider, not the agent runtime: the dispatched agent runs in your environment, with your credentials, against your repository. Direct integrations exist for [Claude Code](../agent-dispatch/claude-code), [Cursor](../agent-dispatch/cursor), and [Linear](../agent-dispatch/linear), with [webhooks](../agent-dispatch/webhooks) for everything else.
The dispatched agent investigates through the [MCP server](./mcp), reading the signal, slicing occurrences, and reading the failing conversations, then opens a pull request. Turn the failing traces behind the signal into a [dataset](../datasets/overview) and add a [regression test](../test-and-fix/regression-testing) that replays it in CI, so the fix has to pass before it merges and every later prompt change is checked against the same failure.
## Where humans stay
Two points of the loop are human by design. Annotations are the ground truth that evaluations are aligned against, and a person reviews every pull request before it merges. Everything between those points, finding examples, reconstructing context, and carrying trace ids between tools, is what the loop automates.
## Set it up
1. [Start tracing](../telemetry/start-tracing) one production agent.
2. Annotate a handful of real failures, or let [flaggers](../annotations/flaggers) do it.
3. Generate an [evaluation](../evaluations/overview) for the signal that matters most.
4. Add a [monitor](../monitors/overview) for any metric you cannot afford to miss.
5. Configure [agent dispatch](../agent-dispatch/overview) under **Settings → Integrations**.
Your coding agent can drive the whole workspace through the MCP server:
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
claude mcp add --transport http latitude https://api.latitude.so/v1/mcp
```
For other coding agents, see the [MCP server docs](./mcp).
# Skills
Source: https://docs.latitude.so/getting-started/skills
Give your agent knowledge on how to set up and manage Latitude autonomously with our skills.
## Overview
The fastest way to add Latitude to an app is to let your coding agent do it. Latitude publishes a set of **[Agent Skills](https://github.com/latitude-dev/skills)** that teach an agent (Claude Code, Cursor, Codex, Windsurf, OpenCode, …) how to instrument your code, drive the [CLI](/getting-started/cli), and confirm that real traces arrive — following Latitude best practices.
There are two entry points, depending on whether you already have a Latitude account.
## The skills
All skills live in the public [`latitude-dev/skills`](https://github.com/latitude-dev/skills) repo. Most agents can install them with the `skills` CLI:
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
npx skills add https://github.com/latitude-dev/skills --skill latitude-setup,latitude-cli,latitude-telemetry
```
| Skill | What it does |
| -------------------- | ------------------------------------------------------------------- |
| `latitude-setup` | Zero-account orchestrator: bootstrap → instrument → verify → claim. |
| `latitude-telemetry` | Adds Latitude / OpenTelemetry instrumentation to your app. |
| `latitude-cli` | Installs and drives the [`latitude` CLI](/getting-started/cli). |
`latitude-setup` builds on the other two, so install all three together for the from-scratch flow.
## No account yet
If you don't have a Latitude account, paste this prompt into your coding agent:
```text theme={"theme":{"light":"github-light","dark":"github-dark"}}
Install the `latitude-setup` skill from `github.com/latitude-dev/skills`, and use it to add Latitude tracing to this app following best practices.
```
The `latitude-setup` skill orchestrates the whole zero-account flow:
1. Installs the [`latitude` CLI](/getting-started/cli) and the Latitude skills.
2. **Bootstraps a temporary account** — no signup — with an API key and a project.
3. Instruments your app (via the `latitude-telemetry` skill) against that project.
4. Runs your real code and inspects the resulting traces, iterating until they look right.
5. Cleans up the trace noise and hands you a **claim link**.
Open the claim link in your browser to take ownership of the temporary account and keep it. Unclaimed temporary accounts expire automatically.
The skill presents a plan and waits for your approval before it edits any code, and it never prints your API key in the chat.
## Already have an account
If you're already signed in, skip the temporary account and point the agent straight at your existing project:
```text theme={"theme":{"light":"github-light","dark":"github-dark"}}
Install the `latitude-telemetry` skill from `github.com/latitude-dev/skills`, and use it to add Latitude tracing to this app following best practices.
```
Give the agent your `LATITUDE_API_KEY` and `LATITUDE_PROJECT_SLUG` when it asks — or connect the [MCP server](/getting-started/mcp) so it can look them up — and it will instrument your app and verify traces the same way.
***
Once traces are flowing, head to the [telemetry guides](/telemetry/start-tracing) for provider-specific details, or the [CLI](/getting-started/cli) and [MCP server](/getting-started/mcp) pages to keep driving Latitude from your terminal or agent.
# Monitors Overview
Source: https://docs.latitude.so/monitors/overview
Watch your signals and saved searches, and get alerted when something needs your attention.
# Monitors
A **monitor** watches your production traffic and opens an **incident** when a condition you care about is met. Each monitor has one **alert**, which watches a single signal or saved search with its own condition and severity. When the alert fires, Latitude records an incident and sends you a notification through your existing notification channels.
Monitors live on the **Monitors** page of each project. Open it from the project sidebar.
## System monitors vs. your monitors
There are two kinds of monitors:
* **System monitors** are set up automatically for every project. They watch your [signals](../signals/overview) and are marked with a **System** badge. You can't create, rename, or delete them, and you can't change what they watch, but you *can* mute them.
* **Your monitors** are the ones you create. Today they watch [saved searches](../search/saved-searches). You have full control: edit the alert, rename, mute, and delete.
The Monitors list shows both, with columns for:
| Column | What it shows |
| ----------------- | ------------------------------------------------------------------------------------------ |
| **Monitor** | The monitor's name, plus a **System** badge for system monitors |
| **Status** | **Live** (the alert is active) or **Muted** (still recording incidents, but not notifying) |
| **Last incident** | When the monitor most recently fired, or `—` if it never has |
| **Condition** | A plain-English summary of the monitor's alert |
Click any row to open its details panel, where you can see the alert, the full incident history, and the mute control.
## The three system monitors
Every project comes with three monitors that watch the signal lifecycle. Each one notifies you about a different moment:
* **Signal discovered**: fires each time a brand-new signal is detected in your traffic.
* **Signal regressed**: fires when a signal that had gone quiet starts being detected again, so a problem you thought was behind you doesn't slip back unnoticed.
* **Signal escalating**: fires when an ongoing signal is being detected *more than expected* for this time of day and week. This is the one monitor with a tunable knob: its **sensitivity** (a value from 1 to 6) controls how large a deviation from the normal pattern is needed before it fires. Open the monitor and edit the alert to change it.
System monitors **can't be deleted**: they're part of how Latitude keeps you informed about signals. If you don't want to be notified by one, **mute** it (see [Mute](#mute) below).
The three system monitors replace the old per-project signal-notification checkboxes. Tuning who gets notified, and on which channel, still happens in your notification preferences. See [How notifications work](#how-notifications-work).
## Creating a monitor for a saved search
You create your own monitors on top of [saved searches](../search/saved-searches). A saved search defines *which* traces you care about; the monitor decides *when* that pattern is worth an alert.
There are two ways into the create flow:
1. **From the Monitors page**: click the **Monitor** button in the top-right and pick a saved search inside the form.
2. **From a saved search**: open the **Saved searches** dropdown next to the search bar on the Traces page. Hover a saved search and click its bell icon for **Create monitor** (or **View monitor** if one already watches it), which opens the same form with that search pre-selected.
Searches containing a **semantic** part (plain unquoted words) can't be monitored. Semantic search ranks the closest traces by meaning instead of applying an exact rule, so a monitor has no match rule to count new traces against. Use quoted `"literal"` or backtick `` `phrase` `` terms — those match exactly, and the monitor counts every match.
The create form asks for:
* **Name**: a human-readable label, e.g. *"Tool error spikes"*.
* **Description**: optional; what the monitor is for.
* **An alert**: a card where you pick the saved search, the alert kind, the condition, and a severity.
As you fill in the alert, a one-line **preview sentence** at the bottom of the card restates the configuration in plain English, so you can confirm it does what you intend before saving. For example:
```text theme={"theme":{"light":"github-light","dark":"github-dark"}}
Alerts each time a new trace matching 'Checkout 5xx errors' is detected.
```
The alert card has a **severity** selector (**Low**, **Medium**, or **High**) that sets the tone of the incident badge and the notification.
## The three saved-search alert kinds
A saved-search alert can watch its search in one of three ways, shown as tabs on the alert card: **Match**, **Threshold**, and **Escalating**.
### Match: alert when matching traces start arriving
Use **Match** when even one matching trace is worth knowing about, for example a saved search for `status = 5xx in production`.
```text theme={"theme":{"light":"github-light","dark":"github-dark"}}
Alerts each time a new trace matching 'Production 5xx' is detected.
```
To keep this from becoming noise, matches are **limited to one alert every 5 minutes**:
* A burst of 500 matches arriving at once produces **one** alert, not 500.
* If matching activity is **continuous**, you'll get a fresh alert **every 5 minutes** for as long as it keeps happening, so an ongoing problem stays visible instead of going quiet after the first alert.
* After a **quiet period** with no matches, the next match opens a fresh alert again.
The 5-minute cadence is deliberate: continuous matching keeps re-surfacing so you don't tune out and forget the problem is still live.
### Threshold: alert at a milestone or a spike
Use **Threshold** when one match doesn't matter, but a lot of them do. The threshold row reads as a sentence you complete: **Alert when traces are detected … \[amount] \[comparison] \[baseline]**. There are three flavors.
#### Absolute count
A simple count: alert once you've seen a set number of matching traces.
```text theme={"theme":{"light":"github-light","dark":"github-dark"}}
Alerts when traces matching 'Production 5xx' are detected 100 times.
```
#### Multiplier: relative to a baseline
Spike detection: alert when the current volume rises to some multiple **above a baseline** you choose. Pick the comparison **times more than** and then choose the baseline:
* **The average of the last N hours/days**: the normal-traffic case. Latitude divides the baseline window into 5-minute slices and compares your current rate against the average slice.
* **The previous period** (yesterday, or the previous week): for traffic that follows **daily or weekly patterns**, where "normal" depends on the day or time.
```text theme={"theme":{"light":"github-light","dark":"github-dark"}}
Alerts when traces matching 'Production 5xx' are detected 3 times more than the average of the last 7 days.
```
**Worked example.** Your saved search averages **20 matches per 5-minute slice** over the last 7 days. With a multiplier of **3**, the alert fires when the current 5-minute window crosses **60 matches** (3 × 20). If your baseline rises to 30/slice next week, the trigger automatically rises to 90, and you don't re-tune it.
#### Expected: a baseline that learns your patterns
The smartest option: alert when you're seeing **more traffic than expected for this time of day and week**. Unlike average or previous-period, you don't pick a comparison window at all: Latitude learns what's normal for each time of day and day of the week on its own.
```text theme={"theme":{"light":"github-light","dark":"github-dark"}}
Alerts when traces matching 'Production 5xx' are detected 3 times more than expected.
```
Choose **expected** when your traffic swings a lot depending on the time of day or day of week (busy weekday mornings, quiet weekends). A single average blurs those swings together; expected accounts for them.
**How "expected" works.** From your trace history, Latitude builds a profile of normal volume for each slot in the week (every hour-of-day × day-of-week pair) together with a tolerance band for how much that volume naturally varies. On each check it compares the current window's count against the expectation for *that* slot and fires when the count breaks above the band. **Sensitivity** sets how wide the band is: a higher value tightens it, so smaller deviations trip the alert. It's the same seasonal detector that powers the **Signal escalating** system monitor.
### Escalating: alert only when a spike sticks around
Use **Escalating** when you want to ignore short blips that fix themselves, and only hear about a problem that **sticks around**. An escalating alert combines any of the threshold modes above with a **window**:
```text theme={"theme":{"light":"github-light","dark":"github-dark"}}
Alerts when traces matching 'Production 5xx' are detected 3 times more than expected, sustained for at least 15 minutes.
```
The **window** field does two things:
* It's the **measurement window**: the condition has to stay true for the whole window, not just for a moment. A single one-minute spike won't trip it.
* It's the **cool-down**: once an incident opens, it stays open until things have been calm for that same window, so it doesn't rapidly open and close.
An escalating alert can wrap any threshold mode (including a plain absolute count, as below) with that window:
That's why escalating alerts are the right choice for the kind of thing you'd want to be paged about: short, noisy spikes get filtered out automatically.
## Mute, delete, and edit
### Mute
Muting a monitor stops it from **notifying** you, but it keeps **recording incidents**. Open the monitor and use the **Mute** / **Unmute** button, or use the actions menu on the list row. Muting is the right tool for a monitor that's correct but currently noisy: you keep the incident history for later, you just stop the notifications.
A muted monitor shows the **Muted** status in the list; a live one shows **Live**.
### Delete
You can **delete** your own monitors from the list's actions menu. A few rules:
* **System monitors can't be deleted**: mute them instead.
* A monitor keeps its alert for life — the alert can't be removed on its own; it's edited in place or deleted along with the monitor.
* Deleting a monitor stops it firing and hides it from the list. Incidents it already recorded remain queryable.
### Edit
What's editable depends on the monitor type:
| Action | Your monitors | System monitors |
| ------------------------------------------- | ------------- | ------------------------------------------ |
| Mute / unmute | ✅ | ✅ |
| Rename / edit description | ✅ | ❌ |
| Change an alert's kind, source, or severity | ✅ | ❌ |
| Edit a condition value | ✅ | Only the **Signal escalating** sensitivity |
| Delete the monitor | ✅ | ❌ |
System monitors are **locked**: you can't change their name, alert, or severity. The single tunable value is the **Signal escalating** sensitivity.
## How notifications work
When a monitor fires, the incident goes through Latitude's normal notification system, the same one used everywhere else. Monitors themselves **don't carry notification settings**:
* **Who** gets notified and **on which channel** is controlled by your per-user and per-project notification preferences, not by the monitor.
* A monitor's only effect on delivery is its **mute** state: a muted monitor still records incidents but stops the notifications from going out.
Incidents can be delivered by email and to **Slack**. Email is toggled per person in notification preferences. Slack is connected once per organization at **Settings → Integrations**: after you connect a workspace, you route the **Incidents** group (the Signal discovered, regressed, and escalating notifications, plus monitor incidents) to the Slack channels you choose.
To change channel routing or per-user preferences, use your notification settings rather than the monitor.
## Next steps
* [Signals overview](../signals/overview): what the system monitors watch
* [Saved searches](../search/saved-searches): the basis for your own monitors
* [Search overview](../search/overview): building the queries your monitors watch
# API Reference
Source: https://docs.latitude.so/more/api-reference
Open the Latitude API reference.
# API Reference
The Latitude API reference is hosted separately.
[Open the API Reference](https://api.latitude.so/docs)
# Overview
Source: https://docs.latitude.so/more/data-destinations/overview
Continuously sync your Latitude data into the systems you already use.
# Data destinations
A **data destination** continuously syncs a Latitude project's data into a system **you own** — your analytics tool, your data warehouse, your object storage. Connect a destination once and new data keeps flowing to it within minutes, with no exports to schedule and no pipeline to maintain.
The data Latitude can send grows over time. Today that's your **traces and spans**; the same engine is built to forward other entities Latitude generates — like [signals](../../signals/overview) — as they become available.
Destinations are **bring-your-own**: you point Latitude at your own external system with your own credentials. Latitude never sends your data anywhere you didn't connect.
Send Latitude spans to PostHog LLM Analytics as native `$ai_*` events.
## Supported destinations
| Destination | Sources | Status |
| ------------------------------------------------- | ------- | -------------- |
| [PostHog (LLM Analytics)](./posthog) | Spans | Implemented |
| Object storage / data warehouse (S3, BigQuery, …) | Spans | *comming soon* |
| Mixpanel | Spans | *comming soon* |
| Generic webhook | Spans | *comming soon* |
The destinations below PostHog are **examples of where this is headed**, not a commitment or a timeline. The engine is built to grow into more destinations — tell us which one you need.
## Available sources
A **source** is the kind of telemetry a destination receives. Today every destination reads **spans**; the rest are directions we're exploring.
| Source | What it is | Status |
| -------- | ---------------------------------------------------------------------------- | -------------- |
| Spans | The individual steps inside a trace — each LLM call, tool call, or retrieval | Implemented |
| Traces | A full end-to-end request, from first step to last | *comming soon* |
| Sessions | Multiple traces grouped into one conversation or user journey | *comming soon* |
| Signals | Recurring failure patterns Latitude detects in your traffic | *comming soon* |
| Scopes | Named segments of your telemetry you define and track | *comming soon* |
## Connect a destination
Open your project's **Settings → Data destinations** and create one. Each destination asks for the credentials and configuration Latitude needs to deliver telemetry into that external system.
Before saving, use **Test connection** to confirm Latitude can reach the destination and your credentials are accepted.
## Excluding payloads
Turn **Exclude payloads** on when you don't want prompt and completion content leaving Latitude. It nulls every content-bearing field — inputs, outputs, tool definitions, and error messages (replaced by an error type only). Everything non-sensitive still flows: token counts, cost, latency, model and provider, identifiers, and timing. This is useful when content is subject to compliance constraints but you still want the metrics.
## How delivery works
* **Near real-time.** New data is delivered continuously, on a short interval, so it shows up in your destination within minutes — not on a manual export.
* **No duplicates.** Delivery is at-least-once and idempotent: each event has a stable identity, so retries and overlapping windows never duplicate data in your destination.
* **Pause and resume.** Pause a destination anytime; while paused nothing is sent. On resume, Latitude catches up the backlog it missed.
* **Auto-pause when idle.** If a project sends no new data for about a week, Latitude automatically pauses its destination so it isn't checked indefinitely. Nothing is lost — just resume the destination when you start sending data again, and it catches up the backlog within your retention window like any other resume.
* **Backfill.** When you connect a destination you can also import past traces, so your destination isn't limited to data created after you connected it. How far back you can reach is bounded by your plan's data retention.
# PostHog
Source: https://docs.latitude.so/more/data-destinations/posthog
Send Latitude spans to PostHog LLM Analytics as native AI events.
# PostHog destination
The PostHog destination sends your Latitude spans to **PostHog LLM Analytics** as native `$ai_*` events in your own PostHog project, so you can analyze cost, latency, and usage alongside the rest of your product data.
PostHog destinations are **bring-your-own**: you point Latitude at your own PostHog project with your own API key. Latitude never sends your data to PostHog unless you connect this destination.
## Connect PostHog
Open your project's **Settings → Data destinations** and create a PostHog destination. You'll provide:
* **Region / host** — PostHog US (`us.i.posthog.com`), PostHog EU (`eu.i.posthog.com`), or a custom host for self-hosted PostHog.
* **Project API key** — your PostHog **project** key (`phc_…`).
* **Name** — a label to recognize the destination by.
* **Exclude payloads** — see [Excluding payloads](#excluding-payloads) below.
Before saving, use **Test connection** to confirm Latitude can reach PostHog and your key is accepted.
PostHog `phc_` keys are **write-only**, so Test connection proves *reachability and key acceptance* — **not** that the key belongs to the project you think it does. A valid key for the *wrong* PostHog project will pass the test and silently send your data there. Double-check you copied the key from the right project. The test also sends a small **canary event**, which is visible in your PostHog.
## What lands in PostHog
Each Latitude span becomes a native PostHog LLM Analytics event:
* `$ai_generation` — an LLM call (with token counts and **cost in USD**)
* `$ai_embedding` — an embedding call
* `$ai_span` — any other step, such as a tool call or retrieval
* `$ai_trace` — emitted for the root step, so the trace view is populated
Events carry the trace, span, parent, and session identifiers (so PostHog groups them correctly), latency, model and provider, and cost. They also include `latitude_project_id` and `latitude_span_url` so you can jump from a PostHog event straight back into Latitude.
With PostHog, traces and sessions don't need their own source: each span carries its trace and session identifiers, so PostHog reconstructs the full trace and session views for you automatically.
## Excluding payloads
Turn **Exclude payloads** on when you don't want prompt and completion content leaving Latitude. It nulls every content-bearing field — inputs, outputs, tool definitions, and error messages (replaced by an error type only). Everything non-sensitive still flows: token counts, cost, latency, model and provider, identifiers, and timing. This is useful when content is subject to compliance constraints but you still want the metrics.
## Backfills in PostHog
When you connect PostHog, you can also import past traces so PostHog isn't limited to data created after you connected it. How far back you can reach is bounded by your plan's data retention.
Backfilled events keep their **original** timestamps, so they land at their real historical date in PostHog — not the date you ran the backfill. If a backfill looks empty, widen PostHog's time filter to the historical range you imported.
Review supported destinations, available sources, payload exclusion, and delivery behavior.
# GitHub integration
Source: https://docs.latitude.so/more/github
Auto-resolve a Latitude signal when the pull request or commit that fixes it merges, and see the linked PRs and commits on the signal page.
The GitHub integration closes the loop that [agent dispatch](/agent-dispatch/overview) opens. When a
pull request or commit that mentions a signal's slug merges into a branch you monitor, Latitude
**links** it to the signal and applies the matched action: resolve, reopen, or just reference. It
works exactly like GitHub's own `Fixes #123`, but for Latitude signals.
Latitude asks for **three read-only permissions and nothing else** — Pull requests, Contents, and
Metadata. The app never comments on your PRs, pushes code, or changes anything in your repositories.
## How it works
1. You connect the Latitude GitHub App to your GitHub account and pick the repositories it can see.
2. You tell Latitude which repository and branch to watch (an organization default, optionally
overridden per project).
3. When someone opens a PR whose title, description, or branch name references a signal slug, the PR
shows up on that signal's page.
4. When the PR **merges into the monitored branch**, Latitude resolves the signal automatically (or
reopens it, for a revert). A commit pushed directly to the branch does the same on landing.
Latitude only ever *reacts* to what merges — the merge itself (which needs write access to your repo)
is the trust boundary, so nobody outside your team can move your signals by typing a slug.
## Connect
Under **Settings → Integrations**, open the GitHub card and click **Connect**. GitHub asks you to
install the Latitude app and choose **All repositories** or a selected subset, then sends you back to
Latitude, which verifies you own the installation and binds it to your organization. One GitHub
installation maps to one Latitude organization.
To disconnect, use **Disconnect** on the same card (Latitude stops processing events; your linked
PRs and commits are kept for history) or uninstall the app on GitHub — either does the same thing.
## Configure what to watch
On the GitHub manage page (**Settings → Integrations → GitHub**):
* **Default repository & branch** — the repo/branch every project watches by default.
* **What to watch** — toggle pull requests and commits, and which text sources are scanned (PR
title, PR description, branch name, commit message).
* **Magic words** — the keyword lists that decide the action (below).
A single project can point at a different repository/branch from **its** signals settings; projects
without an override inherit the organization default. A monorepo is first-class — point several
projects at the same repository.
## Magic words
Latitude scans the enabled text sources for a **keyword plus a signal slug** in the same sentence or
line. The keyword can come before or after the slug, and an optional `#` prefix is allowed.
| Action | Default keywords | Effect on merge |
| ------------- | -------------------------------------------------------------------------------- | ------------------------------- |
| **Resolve** | close, fix, resolve, complete, implement, address, solve (and their inflections) | Resolves the signal |
| **Reopen** | reopen, revert, roll back, back out (and inflections) | Reopens (unresolves) the signal |
| **Reference** | ref, references, part of, related to, contributes to, toward | Links only, no lifecycle change |
All of these link `LAT-XY9Z` and resolve it on merge:
```
Fixes LAT-XY9Z
fixes: #LAT-XY9Z
LAT-XY9Z is fixed
Resolves LAT-XY9Z and LAT-AB12 (applies to both slugs)
branch: fix/lat-xy9z-timeout-handling (branch names are lowercase by convention)
```
A slug on its own never does anything — there must be a keyword. When a sentence matches both a
reopen and a resolve keyword, reopen wins (so `Revert "Fix LAT-XY9Z"` reopens). You can edit each
keyword list, or reset it to the defaults, on the manage page. (Custom regular expressions are not
supported.)
Use the slug shown on the signal page (for example `LAT-XY9Z`) — the three-letter prefix is derived
from your project, and slugs are unique across your organization, so one reference always points at
exactly one signal.
### Let dispatched agents write it for you
If you use [agent dispatch](/agent-dispatch/overview), the default prompt already tells the agent to
name its branch `fix/-…` and title its PR `Resolves : …`. An agent-authored fix then
links on open and resolves the signal on merge with nobody typing a slug by hand.
## See the links on a signal
A signal with linked PRs or commits shows a **GitHub pill** at the top of its detail page: the most
relevant reference (a merged PR, the latest open PR, or the most recent commit) with its status. When
there is more than one, the pill opens a list of them all, each linking out to GitHub, and shows
which one resolved or reopened the signal.
## Troubleshooting
The manage page has a **Recent deliveries** table — every webhook GitHub sent, newest first, with its
event, repository, status, and a detail column. If a merge didn't resolve a signal, check here:
* **skipped `no-config`** — the repository or branch isn't the one you're watching.
* **skipped `suspended`** — the GitHub app is suspended for your account; unsuspend it on GitHub.
* **processed** but the signal didn't move — the text had no matching keyword next to the slug, or the
slug doesn't exist / was deleted. Re-check the magic words above.
If a delivery never arrived at all (for example during a Latitude outage — GitHub does not retry
failed deliveries automatically), you can re-send it from your GitHub App's **Advanced → Recent
Deliveries** page; Latitude processes redeliveries safely.
## Self-hosting
Latitude Cloud runs the GitHub App for you. Self-hosters register **their own** GitHub App and provide
its credentials, so there is no dependency on Latitude's app and no data leaves your control. When the
credentials are unset, the integration stays hidden.
Register a new GitHub App under **GitHub → Settings → Developer settings → GitHub Apps → New GitHub
App** with:
* **Callback URL** and **Setup URL**: `https:///integrations/github/setup/callback`,
with "Request user authorization (OAuth) during installation" and "Redirect on update" checked.
* **Webhook URL**: `https:///v1/webhooks/github`, and a high-entropy **secret**.
* **Repository permissions** (read-only): Pull requests, Contents, Metadata.
* **Subscribe to events**: Pull request, Push.
Then set these environment variables:
```
LAT_GITHUB_APP_ID # the App ID
LAT_GITHUB_APP_SLUG # from github.com/apps/
LAT_GITHUB_APP_PRIVATE_KEY # the generated .pem, base64-encoded
LAT_GITHUB_WEBHOOK_SECRET # the webhook secret you generated
LAT_GITHUB_APP_CLIENT_ID # the App's own OAuth client id
LAT_GITHUB_APP_CLIENT_SECRET # the App's own OAuth client secret
LAT_GITHUB_BASE_URL # optional — set to your GitHub Enterprise Server host
```
`LAT_GITHUB_APP_CLIENT_ID`/`_SECRET` are the GitHub **App's own** OAuth client, not your GitHub
single-sign-on credentials. For **GitHub Enterprise Server**, set `LAT_GITHUB_BASE_URL` to your GHES
host; every URL and API base is derived from it (best-effort support).
# Slack integration
Source: https://docs.latitude.so/more/slack
Send Latitude notifications to Slack channels.
The Slack integration sends Latitude notifications to channels in your Slack workspace. Use it to route incidents and other team-level updates where your team already triages work.
## Connect Slack
1. In Latitude, open a project and go to **Settings → Integrations**.
2. Find **Slack** and click **Connect**.
3. Review the Slack install screen and choose the workspace Latitude should notify.
4. Approve the installation.
5. After Slack redirects back to Latitude, configure notification routes for the channels that should receive updates.
Latitude connects one Slack workspace per organization. If Slack reports that the workspace is already connected to another Latitude organization, disconnect it there before installing it again.
## Configure notification routes
After Slack is connected, the integration card shows a **Notifications** section. Each notification group can be routed to a Slack channel.
* Choose a channel for each notification group you want Slack to receive.
* Leave a notification group unconfigured to keep those notifications out of Slack.
* Update routes any time from **Settings → Integrations**.
Latitude sends Slack notifications at the organization level. Slack routing is independent from each user's in-app and email preferences.
## Reconnect or disconnect
Slack tokens can expire or be revoked from Slack. If Latitude cannot refresh the token, the integration shows **Slack connection expired**. Click **Reconnect** and approve the Slack installation again to restore delivery.
To stop Slack delivery, click **Disconnect** on the Slack integration card. Disconnecting revokes the bot token, stops all Slack notifications for the workspace, and resets channel routing if you reconnect later.
## Troubleshooting
* **No channels appear**: confirm the Slack install completed and that the Latitude Slack app can access the workspace.
* **Notifications are not posted**: verify the notification group has a channel route configured.
* **Connection expired**: reconnect Slack from the integration card.
* **Workspace already connected**: disconnect the Slack workspace from the other Latitude organization before reconnecting it here.
# Duration
Source: https://docs.latitude.so/observability/features/duration
Track trace and session duration to investigate latency and long-running conversations.
# Duration
Latitude tracks trace and session duration so you can spot slow interactions, long-running agent workflows, and conversations that take longer than expected.
## Trace duration
A trace is one complete interaction or agent turn. Its duration runs from the first span to the last span.
Use trace duration to find:
* slow LLM responses
* long tool-call chains
* retries or fallback paths
* agent loops
* high-latency production requests
Trace duration appears in the Traces table, trace detail view, search results, and filters.
## Session duration
A session groups related traces into a multi-turn conversation. Its duration is the time covered by those traces.
Use session duration to understand:
* long-running conversations
* support interactions that take many turns
* workflows that span multiple user messages
* sessions where latency accumulates across turns
The Sessions view shows duration alongside session-level cost, time to first token, models, span count, session id, and user id when available.
## Filter by duration
Use duration filters to find slow traces or sessions. Combine them with search and other filters, such as:
* traces with high duration and errors
* expensive traces that also took a long time
* slow traces for a specific model or provider
* long sessions for one user or customer segment
* production traces where users expressed frustration
Duration pairs well with semantic search. For example, search for *agent loops between tools* and filter to high-duration traces to find slow looping behaviours.
## Related
* [Percentile cohorts](./percentile-cohorts): Compare duration against similar tagged traces
* [Traces](../traces): Trace-level duration and span waterfalls
* [Sessions](../sessions): Session-level duration and multi-turn conversations
* [Filters](../filters): Filter by duration
* [Search](../../search/overview): Combine duration filters with behavioural search
# Environments
Source: https://docs.latitude.so/observability/features/environments
Separate production, staging, preview, and local traces with tags or metadata.
# Environments
Latitude does not require a separate environment object. Add environment context to telemetry so you can filter, search, and review traces by source.
Use either pattern:
* **Tags** for a small, fixed set of environments you filter by often, such as `production`, `staging`, or `preview`.
* **Metadata** when environment is part of a richer deployment context, such as environment, region, service, and deployment id.
## Send environment as a tag
```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
import { capture } from "@latitude-data/telemetry"
await capture(
"support-agent-turn",
async () => agent.run(userMessage),
{
tags: ["production"],
},
)
```
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
from latitude_telemetry import capture
capture(
"support-agent-turn",
lambda: agent.run(user_message),
{
"tags": ["production"],
},
)
```
## Send environment as metadata
```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
await capture(
"support-agent-turn",
async () => agent.run(userMessage),
{
metadata: {
environment: "production",
region: "us-east-1",
},
},
)
```
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
capture(
"support-agent-turn",
lambda: agent.run(user_message),
{
"metadata": {
"environment": "production",
"region": "us-east-1",
},
},
)
```
## Use environments in Latitude
With environment context, you can:
* filter traces to production traffic
* search only staging or preview traces
* save searches for production failure modes
* scope signal monitoring to a specific environment
* compare whether a behaviour appears in one environment or across all environments
## Related
* [Tags](./tags): Add lightweight environment labels
* [Metadata](./metadata): Attach structured deployment context
* [Filters](../filters): Filter traces by tags or metadata
* [Search](../../search/overview): Search within an environment
# Log levels
Source: https://docs.latitude.so/observability/features/log-levels
Track application-defined severity with metadata and combine it with trace status filters.
# Log levels
Latitude does not require a first-class log-level field. If your application uses severity levels such as `debug`, `info`, `warn`, or `error`, send them as metadata and filter on those fields.
Use log-level metadata for application-defined severity; use trace status and error-count filters for OpenTelemetry span errors.
## Send a log level
```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
import { capture } from "@latitude-data/telemetry"
await capture(
"support-agent-turn",
async () => agent.run(userMessage),
{
metadata: {
logLevel: "warn",
reason: "tool_retry",
},
},
)
```
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
from latitude_telemetry import capture
capture(
"support-agent-turn",
lambda: agent.run(user_message),
{
"metadata": {
"log_level": "warn",
"reason": "tool_retry",
},
},
)
```
Choose one naming convention and keep it consistent across your application, such as `metadata.logLevel` or `metadata.log_level`.
## Filter by log level
After sending log-level metadata, filter by fields such as:
* `metadata.logLevel = error`
* `metadata.logLevel = warn`
* `metadata.log_level = error`
Combine log-level filters with other filters and search queries. For example:
* search for *agent got stuck* with `metadata.logLevel = warn`
* filter production traces where `metadata.logLevel = error`
* find expensive traces where `metadata.reason = tool_retry`
## Log levels vs trace status
Log levels are application-defined metadata. Trace status is derived from OpenTelemetry span status.
Use:
* **Trace status / error count** when an operation errored
* **Metadata log levels** when your application marks severity even if the trace succeeded
## Related
* [Metadata](./metadata): Attach structured context to traces
* [Filters](../filters): Filter by metadata, status, and error count
* [Search](../../search/overview): Combine severity metadata with semantic search
# Metadata
Source: https://docs.latitude.so/observability/features/metadata
Attach structured key-value context to traces for filtering and investigation.
# Metadata
Metadata adds structured key-value context to traces for investigation, filtering, and grouping.
Common metadata includes:
* request id
* account id
* plan or tier
* region
* environment
* feature flag or experiment id
* product surface
## Send metadata
Pass metadata through `capture()`.
```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
import { capture } from "@latitude-data/telemetry"
await capture(
"support-agent-turn",
async () => {
return agent.run(userMessage)
},
{
metadata: {
requestId: request.id,
accountId: account.id,
plan: account.plan,
environment: "production",
},
},
)
```
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
from latitude_telemetry import capture
capture(
"support-agent-turn",
lambda: agent.run(user_message),
{
"metadata": {
"request_id": request.id,
"account_id": account.id,
"plan": account.plan,
"environment": "production",
},
},
)
```
Nested captures inherit parent metadata. Child metadata is shallow-merged with parent metadata.
## Use metadata in Latitude
Metadata appears on traces and in filters, so you can review cohorts such as:
* production traces for one account
* failed traces for a specific feature flag
* expensive traces from one region
* signal examples from a specific plan tier
Metadata filters use dot notation, such as `metadata.environment = production`.
## Metadata vs tags
Use metadata for structured fields with values, such as environment, release, commit SHA, deployment id, or log level. Use [tags](./tags) for simple labels that do not need key-value structure.
## Related
* [Tags](./tags): Add lightweight labels
* [Filters](../filters): Filter by metadata fields
* [Search](../../search/overview): Combine metadata filters with semantic search
# Percentile cohorts
Source: https://docs.latitude.so/observability/features/percentile-cohorts
Compare trace duration, TTFT, cost, and token count against similar traces with the same tag set.
# Percentile cohorts
Latitude compares each trace with similar traces, so outliers are judged in context instead of against one global baseline.
A cohort is defined by the trace's exact tag combination. For example, traces tagged `production` and `support-agent` are compared with other traces that have the same tags. This keeps percentile badges meaningful across agents, environments, features, and workloads.
## Tracked dimensions
Latitude builds cohort baselines for:
* **Duration**: end-to-end trace duration
* **TTFT**: time to first token
* **Cost**: total estimated generation cost
* **Tokens**: total token count
These dimensions appear in trace tables, trace details, filters, and outlier badges.
## Percentile badges
When a trace is unusually high for one of its cohort dimensions, Latitude can show a percentile badge such as `p90`, `p95`, or `p99`.
A `p95` duration badge means the trace took longer than at least 95% of traces in its cohort. A `p99` cost badge means it cost more than at least 99% of cohort traces.
The badge tooltip shows:
* the trace's value
* the cohort's baseline values
* the tags that define the cohort
* how far the trace is from the cohort median
## Tag-scoped cohorts
Because cohorts use exact tag combinations, the same raw value can be normal in one cohort and unusual in another.
For example:
* a long-running research agent may normally have higher duration
* a checkout support agent may normally have lower token usage
* staging traces may have different latency than production traces
* canary traffic may have different cost characteristics than stable traffic
Use tags consistently when you want Latitude to compare traces against the right baseline.
## Minimum baseline size
Percentile cohorts need enough historical traces before higher percentiles are meaningful. Until then, Latitude does not show higher-percentile outlier badges for that cohort.
As more traces arrive with the same tag combination, Latitude can compute stronger baselines and more precise comparisons.
## Using percentile cohorts
Use percentile cohorts to:
* spot slow traces relative to similar traffic
* find high-cost traces for a specific agent or environment
* identify token-heavy behaviours in one product area
* investigate high TTFT for one model/provider cohort
* combine outlier badges with search to discover behavioural causes
For example, filter to `p95` duration traces, then search for *agent loops between tools* to find looping behaviour behind slow traces.
## Related
* [Duration](./duration): Track trace and session duration
* [Token and cost tracking](./token-cost-tracking): Track usage and estimated cost
* [Tags](./tags): Define useful cohorts with consistent tags
* [Filters](../filters): Filter by percentile thresholds
* [Search](../../search/overview): Discover behaviours inside outlier cohorts
# Releases and versioning
Source: https://docs.latitude.so/observability/features/releases-versioning
Track agent versions, deployment ids, and rollout cohorts with tags or metadata.
# Releases and versioning
Add release and version context through tags or metadata to investigate whether a behaviour started after a deploy, compare canary and stable traffic, or filter signal examples to one agent version.
Use:
* **Metadata** for exact values such as commit SHA, release version, deployment id, or prompt version.
* **Tags** for broad rollout labels such as `canary`, `stable`, `release-candidate`, or `v2-agent`.
## Send version metadata
```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
await capture(
"support-agent-turn",
async () => agent.run(userMessage),
{
metadata: {
release: "checkout-release",
commitSha: process.env.GIT_COMMIT_SHA,
deploymentId: process.env.DEPLOYMENT_ID,
promptVersion: "support-prompt",
},
},
)
```
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
capture(
"support-agent-turn",
lambda: agent.run(user_message),
{
"metadata": {
"release": "checkout-release",
"commit_sha": os.environ.get("GIT_COMMIT_SHA"),
"deployment_id": os.environ.get("DEPLOYMENT_ID"),
"prompt_version": "support-prompt",
},
},
)
```
## Send rollout tags
```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
await capture(
"support-agent-turn",
async () => agent.run(userMessage),
{
tags: ["canary", "v2-agent"],
},
)
```
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
capture(
"support-agent-turn",
lambda: agent.run(user_message),
{
"tags": ["canary", "v2-agent"],
},
)
```
## Use versions in Latitude
With version context, you can:
* search for behaviours introduced by a release
* filter traces by prompt version or deployment id
* compare signal examples before and after a rollout
* scope saved searches to canary traffic
* monitor whether a signal you fixed comes back in a new version
## Related
* [Tags](./tags): Track rollout labels
* [Metadata](./metadata): Track exact release fields
* [Search](../../search/overview): Discover version-specific behaviours
* [Signals](../../signals/overview): Track failure modes across releases
# Sampling
Source: https://docs.latitude.so/observability/features/sampling
Understand where Latitude does and does not sample trace processing.
# Sampling
Latitude does not sample traces before making them observable. If a trace is ingested and retained, it can appear in the Traces view, filters, search results, and manual review workflows.
Latitude does not sample semantic search, exact-text search, or the trace stream used by automatic flaggers. Full coverage matters for rare failures, edge-case user behavior, and low-volume regressions.
## Where sampling exists
Sampling only applies to selected downstream processing. It does not affect trace visibility or search coverage.
### Evaluation sampling
Evaluations can run on a percentage of matching incoming traces. This controls monitoring cost and volume while keeping traces available for search and review.
A sampling rate of `0%` pauses the evaluation.
### Flagger sampling
Flaggers can sample how aggressively Latitude runs additional automated checks for a category. This balances detection coverage with processing volume.
Flagger sampling does not remove traces from search, filtering, or manual signal investigation.
## Related
* [Search](../../search/overview): Search across all searchable traces
* [Evaluations](../../evaluations/overview): Monitor signal patterns on incoming traces
* [Flaggers](../../annotations/flaggers): Automatic detection for common failure categories
# Sandbox
Source: https://docs.latitude.so/observability/features/sandbox
An isolated, free environment to debug your agents with Latitude's traces UI from your dev and staging environments — without polluting production.
# Sandbox
A **sandbox** is an isolated environment where you can send and inspect traces while you build. Point your agent at a sandbox from your **dev or staging** environment and watch exactly what it did in Latitude's traces UI — without your development traffic ever touching your production dashboards, billing, or alerts.
A sandbox is deliberately **minimal and focused on trace-level debugging**: it ingests and shows your traces, and nothing more. It mirrors your organization's project layout but holds completely separate data — your development traces.
## Switching to a sandbox
Your organization has a single sandbox. Flip the **Sandbox** toggle in the sidebar to switch into it — the first time, Latitude creates your sandbox and a sandbox copy of the project you're currently in; after that, the toggle just takes you there (and mirrors any new project you switch from). While you're in a sandbox, a blue banner across the top makes it unmistakable which environment you're in, and **Switch to live** takes you back.
## What's available in a sandbox
A sandbox is intentionally light. It gives you the full **trace-level debugging** experience — the Traces and Sessions list, grouping, sorting, filters, and per-trace detail (duration, tokens, spans, model, status).
The production monitoring layer is **not** available in a sandbox. The following features do **not** run on sandbox traces, by design:
* Semantic and saved searches
* Monitors and alerts
* Signal detection and clustering
* Evaluations, annotations, and flaggers
* Outbound notifications (email, Slack, webhooks) — a sandbox never reaches an external channel, so your experiments can't page teammates or hit customer alert channels
This keeps a sandbox free and fast, and means none of your development traffic triggers LLM work or costs.
## Using the same project identifiers as production
You instrument your code with the **same project names everywhere** — the API key decides where the traces land. Inside a sandbox you add the production projects you want to debug (which links them by stable id), or create sandbox-only projects for prototyping.
The only thing you change when developing against a sandbox is the **API key**. Sandbox keys are created from inside the sandbox and are prefixed **`lat_sandbox_`**, so they're easy to spot in code, logs, and review:
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
# Production
LATITUDE_API_KEY=lat_...
# Sandbox — same project names, different key
LATITUDE_API_KEY=lat_sandbox_...
```
A sandbox key can only ever write to its sandbox, and a production key can only ever write to production — the key is the boundary. Sending a trace to a project the sandbox doesn't have is **rejected** (just as production rejects unknown projects), so there's never a silently created typo project.
## Differences from production
Sandbox keys have their own, lower ingest rate limit (**60 requests/min**, **64 MB/min**). Pointing real production traffic at a sandbox key fails loudly and immediately with a `429` — a clear signal to fix the key.
Sandbox traces are short-lived — spans are retained for **7 days** and then age out. A sandbox is for active debugging, not long-term storage.
Sandbox usage is **completely free**. Sandbox traces are never metered or charged and never touch your credit balance. Abuse is bounded by the rate limit and a per-period span quota instead.
Sandbox ingestion is bounded by a per-period **span quota** that scales with your plan: **100,000 spans** on Free, **1,000,000 spans** on Pro, and unlimited on Enterprise.
## Sleep and reactivation
A sandbox is put to **sleep** (archived) after **7 days of inactivity**. Sleeping sandboxes hold no live capacity.
Sending traces to a sleeping sandbox is **rejected** with a `403` (mirroring how an out-of-credits production org behaves) — nothing is silently accepted, and the sandbox isn't silently revived. To wake it back up, open the sandbox and press **Activate** in the banner (which turns gray while the sandbox is asleep) to resume ingestion.
## Related
* [Environments](../observability/features/environments): Separate dev, staging, and production traces with tags or metadata
* [Start tracing](../telemetry/start-tracing): Instrument your agent and send traces to Latitude
* [Traces](../observability/traces): The traces UI you use inside a sandbox
# Tags
Source: https://docs.latitude.so/observability/features/tags
Add lightweight labels to traces so you can filter and search cohorts.
# Tags
Tags are lightweight trace labels for broad, repeated categories such as environment, agent name, feature area, rollout, or customer segment.
## Send tags
Pass tags through `capture()`.
```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
import { capture } from "@latitude-data/telemetry"
await capture(
"support-agent-turn",
async () => {
return agent.run(userMessage)
},
{
tags: ["production", "support-agent", "checkout"],
},
)
```
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
from latitude_telemetry import capture
capture(
"support-agent-turn",
lambda: agent.run(user_message),
{
"tags": ["production", "support-agent", "checkout"],
},
)
```
Nested captures inherit parent tags. Additional tags are appended and deduplicated.
## Use tags in Latitude
Tags appear in trace metadata.
Use tags to:
* filter the Traces page
* narrow semantic or exact-text searches
* save reusable search cohorts
* scope evaluation monitoring
* compare signal patterns across environments, agents, or features
## Tags vs metadata
Use tags for labels you commonly filter by, such as environments or rollout cohorts. Use [metadata](./metadata) for structured key-value context such as request ids, plan names, regions, experiment ids, or account properties.
## Related
* [Metadata](./metadata): Attach structured context
* [Filters](../filters): Filter traces by tags
* [Search](../../search/overview): Combine tags with semantic search
# Token and cost tracking
Source: https://docs.latitude.so/observability/features/token-cost-tracking
Track token usage and estimated LLM cost across traces and sessions.
# Token and cost tracking
Latitude aggregates token usage and estimated cost from each trace's LLM spans. Use these fields to understand expensive interactions, compare models, and investigate behaviour that drives high usage.
## What Latitude tracks
When the provider or framework instrumentation reports usage, Latitude shows:
* input tokens
* output tokens
* total tokens
* estimated cost
* models used
* providers used
Trace rows show aggregate token and cost values across their spans. Session rows aggregate usage across their traces.
## Find expensive traces
Filter by token usage or cost to find:
* high total cost
* high input or output tokens
* expensive traces for one model or provider
* expensive traces within a tag, user, session, or metadata cohort
You can combine these filters with [Search](../../search/overview). For example, search for *agent loops between tools* and filter to traces above a cost threshold.
## Where usage appears
Token and cost data appears in:
* the Traces table
* trace detail views
* session aggregates
* filters
* search result rows
Cost values are estimates based on the usage and model/provider information available in telemetry.
## Related
* [Percentile cohorts](./percentile-cohorts): Compare cost and token usage against similar tagged traces
* [Traces](../traces): Trace-level usage and cost
* [Sessions](../sessions): Session-level aggregation
* [Filters](../filters): Filter by cost and token usage
* [Search](../../search/overview): Discover costly behaviours
# Trace IDs
Source: https://docs.latitude.so/observability/features/trace-ids
Use OpenTelemetry trace ids to connect Latitude traces with the rest of your observability stack.
# Trace IDs
Latitude is built on OpenTelemetry and preserves OpenTelemetry trace ids. A trace id identifies one complete interaction across your application's spans.
## How trace ids work
When your application emits spans with the same OpenTelemetry trace id, Latitude groups them into one trace. The trace detail view can then show the full waterfall of LLM calls, tool calls, retrieval steps, and other instrumented operations.
Trace ids are useful when another observability tool also uses OpenTelemetry trace context. The same id can connect a Latitude trace with logs, metrics, or traces elsewhere in your stack.
## Find traces by id
Filter traces by trace id when:
* a log line includes an OpenTelemetry trace id
* another observability system links to the same trace id
* you need to share an exact trace with a teammate
## Distributed tracing
Latitude can receive spans from OpenTelemetry-compatible instrumentation. If your services propagate OpenTelemetry trace context, spans from the same interaction can share one trace id before they reach Latitude.
For setup details, see:
* [TypeScript SDK](../../telemetry/typescript)
* [Python SDK](../../telemetry/python)
* [OpenTelemetry Exporter](../../telemetry/otel-exporter)
## Related
* [Traces](../traces): Understand trace structure and lifecycle
* [Filters](../filters): Filter by trace id
# Trace URLs
Source: https://docs.latitude.so/observability/features/trace-urls
Open and share specific traces from the Latitude web app.
# Trace URLs
When you open a trace in the Latitude web app, the selected trace is reflected in the page URL. Share that URL with teammates who have access to the same organization and project.
Trace URLs point teammates to an exact production interaction from a search result, saved search, signal, or trace list.
## Open a trace URL
You can open a trace from:
* the Traces page
* Search results
* Saved searches
* Signal logs
* Session rows
After opening the trace, copy the browser URL and share it with someone who has project access.
## Trace ids
Latitude also shows the trace id in the trace detail view. Copy it and use the trace-id filter to find the same trace later.
Trace ids are especially useful when you need to connect Latitude with logs or another OpenTelemetry-compatible observability system.
## Related
* [Trace IDs](./trace-ids): Use OpenTelemetry trace ids across systems
* [Traces](../traces): Browse and inspect traces
* [Search](../../search/overview): Find traces by behaviour
# User tracking
Source: https://docs.latitude.so/observability/features/user-tracking
Group and filter traces by the end user that triggered them.
# User tracking
Latitude can associate traces with an end-user identifier so you can investigate one user's experience, find users affected by an issue, or filter search results to a specific customer or account.
## Send a user id
Pass a user id through `capture()` at the request, conversation turn, or agent entry point.
```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
import { capture } from "@latitude-data/telemetry"
await capture(
"support-agent-turn",
async () => {
return agent.run(userMessage)
},
{
userId: user.id,
},
)
```
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
from latitude_telemetry import capture
capture(
"support-agent-turn",
lambda: agent.run(user_message),
{
"user_id": user.id,
},
)
```
## Use user ids in Latitude
With user ids, you can:
* filter traces by user
* search within one user's traces
* inspect sessions and traces associated with that user
* understand which users are affected by a recurring issue
User ids are most useful when they are stable identifiers from your application, such as an internal user, account, or customer id.
## Related
* [Sessions](../sessions): Group multiple traces into a conversation
* [Filters](../filters): Filter traces by user id
* [Search](../../search/overview): Search within filtered trace cohorts
# Filters
Source: https://docs.latitude.so/observability/filters
Use the shared filter system to narrow traces, save searches, and configure evaluation triggers
# Filters
Latitude uses a universal filter system across the platform. The same filters you use to narrow traces in a search also power saved searches and evaluation triggers. Learn it once, use it everywhere.
## How Filters Work
A filter is a set of field conditions. Active conditions combine with **AND** logic. For example, "Status is error" and "Cost is greater than \$1" returns only traces that match both conditions.
Filters appear on the Traces page (alongside the search query) and in the configuration UI for evaluation triggers.
## Available Filter Fields
| Field | Description | Example |
| ----------------- | -------------------------------------------------- | ---------------------------------- |
| **Status** | Trace completion status: OK, ERROR, or UNSET | `status in [error]` |
| **Name** | Root span name (the `path` you set in `capture()`) | `name eq "invoke_agent"` |
| **Session ID** | Filter to a specific multi-turn session | `sessionId eq "session-abc"` |
| **User ID** | End-user identifier from telemetry metadata | `userId eq "user-42"` |
| **Tags** | Custom tags attached to spans | `tags in ["production", "canary"]` |
| **Models** | LLM models used in the trace | `models in ["gpt-4o"]` |
| **Providers** | LLM providers called | `providers in ["openai"]` |
| **Services** | OpenTelemetry service names | `serviceNames in ["api-server"]` |
| **Cost** | Estimated total trace cost | Cost greater than `$1` |
| **Duration** | End-to-end trace duration | Duration greater than `5s` |
| **TTFT** | Time to first token | TTFT greater than `1s` |
| **Span Count** | Number of spans in the trace | `spanCount gte 10` |
| **Error Count** | Number of errored spans | `errorCount gte 1` |
| **Tokens Input** | Total input tokens across LLM calls | `tokensInput gte 1000` |
| **Tokens Output** | Total output tokens across LLM calls | `tokensOutput gte 500` |
| **Metadata** | Custom key-value metadata your application sends | `metadata.env eq "production"` |
## Operators
Filters support 10 operators:
| Operator | Meaning | Works With |
| ------------- | ---------------------------------- | ----------------------------------------- |
| `eq` | Equals | All fields |
| `neq` | Not equals | All fields |
| `gt` | Greater than | Numeric fields |
| `gte` | Greater than or equal | Numeric fields |
| `lt` | Less than | Numeric fields |
| `lte` | Less than or equal | Numeric fields |
| `in` | Value is in set | Status, tags, models, providers, services |
| `notIn` | Value is not in set | Status, tags, models, providers, services |
| `contains` | Substring match (case-insensitive) | Text fields, metadata |
| `notContains` | Substring does not match | Text fields, metadata |
## Custom Metadata Filters
Your application can send structured metadata with its telemetry. Filter on any metadata field using dot-notation:
* `metadata.env`: top-level key
* `metadata.runtime.region`: nested key
Metadata filters support exact matches, ranges, set membership, and substring searches on your custom fields.
## Combining Filters
All active filters combine with **AND** logic. Common combinations:
* **Status** = ERROR **and** **Cost** > \$1: find expensive failures
* **Models** = gpt-4o **and** **Duration** > 5s: find slow GPT-4o traces
* **Metadata** `environment` = `production` **and** **Error Count** > 0: find production errors
* **Tags** in `["canary"]` **and** **Tokens Output** > 2000: find verbose canary responses
## Where Filters Are Used
| Feature | How Filters Are Used |
| ---------------------------------------------- | ------------------------------------------------------------------ |
| **Trace dashboard** | Interactive filtering from the toolbar |
| **[Search](../search/overview)** | Narrow a search query to a specific subset of traces |
| **[Saved searches](../search/saved-searches)** | The filter set is part of what gets bookmarked alongside the query |
| **Evaluation triggers** | Define which traces an evaluation monitors |
| **Score analytics** | Narrow analytics dashboards to specific trace subsets |
When you configure an evaluation trigger or save a search, you're building a filter using this same system. An empty filter means "match all traces."
## Next Steps
* [Traces](./traces): Browse and filter your traces
* [Search](../search/overview): Combine filters with hybrid search
* [Saved Searches](../search/saved-searches): Persist a query plus filter set as a named bookmark
* [Evaluation Triggers](../evaluations/triggers): Use filters to control evaluation scope
# Build a dashboard from Latitude data
Source: https://docs.latitude.so/observability/guides/build-a-dashboard
Give an agent arbitrarily sliceable analytics and let it render a self-contained HTML dashboard.
Latitude doesn't ship a dashboard builder. Instead it exposes one composable analytics tool,
`queryAnalytics`, over the [MCP connection](/getting-started/mcp). You ask your agent for a dashboard;
the agent makes a handful of `queryAnalytics` calls and renders a **self-contained HTML page** from the
results. Latitude provides the data; the agent renders it.
## The shape of every widget
Every chart is the same three-part query over a filtered stream:
* **Metric** — the number each row reduces to (`count`, `errorRate`, `percentile(duration)`, `avg(cost)`, …).
* **Breakdown** — the dimension to split across, one row per value (by `model`, `tool`, `signalId`, …).
* **Time bucket** — optional granularity (`hour` / `day` / `week`) that turns a number into a trend.
So "error rate by model, weekly" = metric `errorRate` × breakdown `model` × bucket `week`. Swap any
axis for the next chart — no new endpoint.
## Streams and metrics
| Stream | What it measures | Metrics | Break down by |
| ------------------------------- | ----------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------- |
| `traces` / `sessions` / `spans` | requests, conversations, operations | `count`, `errorRate`, `cacheHitRate`, `sum`/`min`/`max`/`avg`/`median` of `duration`\|`cost`\|`tokens`, plus `percentile` (`{ kind: "percentile", field, p }`, `p` 1–99) | `model`, `provider`, `service`, `tool`, `tag`, `status` (+ `name`/`userId` on traces, `operation` on spans) |
| `scores` (signals) | scored occurrences | `count`, `passRate`, `errorRate`, `avg`/`min`/`max`/`median` of `value` | `signalId`, `source`, `model`, `provider`, `service`, `tool`, `tag` |
| `behaviors` | clustered behaviors | `count`, `avg`/`min`/`max`/`median` of `confidence` | `cluster`, `session`, `method` |
| `moments` | labeled conversation moments | `count`, `avg`/`min`/`max`/`median` of `confidence`\|`coherence` | `kind`, `actor`, `session` |
Values are returned in human units: `duration` in **seconds**, `cost` in **dollars**, and rates
(`errorRate`, `cacheHitRate`, `passRate`) as a **0–1 fraction**.
## Example calls
```jsonc theme={"theme":{"light":"github-light","dark":"github-dark"}}
// Error rate by model, weekly, last 30 days
queryAnalytics({
stream: "traces", metric: { kind: "errorRate" }, breakdown: "model",
timeBucket: { unit: "week" },
range: { fromIso: "2026-05-31T00:00:00Z", toIso: "2026-06-30T00:00:00Z" }
})
// 95th-percentile latency by tool
queryAnalytics({
stream: "traces", metric: { kind: "percentile", field: "duration", p: 95 }, breakdown: "tool",
range: { fromIso: "2026-06-23T00:00:00Z", toIso: "2026-06-30T00:00:00Z" }
})
// Cost by provider
queryAnalytics({
stream: "traces", metric: { kind: "sum", field: "cost" }, breakdown: "provider",
range: { fromIso: "2026-06-23T00:00:00Z", toIso: "2026-06-30T00:00:00Z" }
})
// Conversation fallout: moment count by kind
queryAnalytics({
stream: "moments", metric: { kind: "count" }, breakdown: "kind",
range: { fromIso: "2026-06-23T00:00:00Z", toIso: "2026-06-30T00:00:00Z" }
})
```
Each returns a tidy series — `[{ key?, bucketStart?, value }]` — small enough to embed inline no matter
how many traces it spans.
## Row-level widgets
For a "top 10 slowest tool calls" style table (rows, not an aggregate), use `querySpans` to list the
individual spans across traces:
```jsonc theme={"theme":{"light":"github-light","dark":"github-dark"}}
querySpans({
filters: { operation: [{ op: "eq", value: "execute_tool" }] },
range: { fromIso: "2026-06-23T00:00:00Z", toIso: "2026-06-30T00:00:00Z" },
limit: 10
})
```
## Rendering
The agent embeds the returned series directly into a **self-contained HTML file** (an inline chart
library, data baked in) and commits it to your repo or hosts it wherever you like. Nothing is rendered
by Latitude — which keeps the data portable and the setup dependency-free.
A good prompt is simply: *"Build a self-contained HTML dashboard of this project's LLM reliability and
cost over the last 30 days."* The agent discovers the right `queryAnalytics` calls on its own.
See [Agent dispatch](/agent-dispatch/overview#investigate-a-signal) for the investigation counterpart —
how an agent uses these same tools to root-cause a signal.
# Group traces by project
Source: https://docs.latitude.so/observability/guides/group-traces-by-project
Keep signal detection scoped to the right agent by routing traces into separate Latitude projects.
# Group traces by project
Latitude runs signal detection across all traces inside a project. That is the right default when one project represents one agent, application, or product surface.
If the same workspace receives traces from multiple unrelated agents, their failure modes can mix together. For example, a customer-support agent and a code-review agent should usually not share signal discovery, because frustration, tool failures, refusals, or hallucinations mean different things in each context.
The best way to keep signal discovery clean is to **create one Latitude project per agent or product surface** and route each trace to the right project.
## When to split traces into different projects
Use separate projects when agents have different:
* prompts, tools, retrieval systems, or model configurations
* users, workflows, or product surfaces
* quality standards or failure definitions
* owners or triage workflows
* signal detection and evaluation needs
Keep traces in the same project when they belong to the same agent experience and should share signal discovery, search, scores, evaluations, and signal trends.
## How Latitude resolves the project
Each ingested span resolves to a Latitude project from the first available source:
1. **Span attribute**: `latitude.project`
2. **OTEL resource attribute**: `latitude.project`
3. **Export header**: `X-Latitude-Project`
Use the export header or SDK constructor when a process sends all traces to one project. Use the span attribute when one process emits traces for multiple agents.
## SDK examples
### TypeScript: one project per process
If the service only runs one agent, set the project when initializing Latitude.
```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
import { Latitude } from "@latitude-data/telemetry"
import { createOpenAIInstrumentation } from "@latitude-data/telemetry/instrumentations/openai"
import OpenAI from "openai"
const latitude = new Latitude({
apiKey: process.env.LATITUDE_API_KEY!,
project: "support-agent",
instrumentations: [createOpenAIInstrumentation(OpenAI)],
})
await latitude.ready
```
### TypeScript: multiple agents in one process
If one process runs multiple agents, set `project` on `capture()` so each trace is routed by agent.
```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
import { Latitude, capture } from "@latitude-data/telemetry"
import { createOpenAIInstrumentation } from "@latitude-data/telemetry/instrumentations/openai"
import OpenAI from "openai"
const latitude = new Latitude({
apiKey: process.env.LATITUDE_API_KEY!,
instrumentations: [createOpenAIInstrumentation(OpenAI)],
})
await latitude.ready
await capture(
"support-agent-turn",
async () => runSupportAgent(userMessage),
{ project: "support-agent", sessionId: supportConversationId },
)
await capture(
"billing-agent-turn",
async () => runBillingAgent(userMessage),
{ project: "billing-agent", sessionId: billingConversationId },
)
```
### Python: one project per process
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
import openai
from latitude_telemetry import Latitude
latitude = Latitude(
api_key="your-api-key",
project="support-agent",
instrumentations={"openai": openai},
)
```
### Python: multiple agents in one process
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
from latitude_telemetry import Latitude, capture
latitude = Latitude(api_key="your-api-key")
capture(
"support-agent-turn",
lambda: run_support_agent(user_message),
{"project": "support-agent", "session_id": support_conversation_id},
)
capture(
"billing-agent-turn",
lambda: run_billing_agent(user_message),
{"project": "billing-agent", "session_id": billing_conversation_id},
)
```
## OpenTelemetry examples
### One project per exporter with `X-Latitude-Project`
Use the export header when every span exported by the process belongs to the same Latitude project.
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
export OTEL_EXPORTER_OTLP_TRACES_ENDPOINT="https://ingest.latitude.so/v1/traces"
export OTEL_EXPORTER_OTLP_TRACES_HEADERS="Authorization=Bearer $LATITUDE_API_KEY,X-Latitude-Project=support-agent"
```
### One project per process with a resource attribute
Use the resource attribute when your OpenTelemetry setup prefers routing metadata on the resource instead of headers.
```go theme={"theme":{"light":"github-light","dark":"github-dark"}}
import (
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/sdk/resource"
)
res, err := resource.New(ctx,
resource.WithAttributes(
attribute.String("service.name", "agent-service"),
attribute.String("latitude.project", "support-agent"),
),
)
```
### Multiple agents in one process with a span attribute
Use the span attribute when a single process emits traces for multiple agents. The span-level value wins over the resource attribute and export header.
```go theme={"theme":{"light":"github-light","dark":"github-dark"}}
ctx, span := tracer.Start(ctx, "support-agent-turn")
defer span.End()
span.SetAttributes(
attribute.String("latitude.project", "support-agent"),
attribute.String("session.id", supportConversationID),
)
```
## Recommended pattern
Start with one Latitude project per production agent. If one runtime hosts several agents, keep one exporter but set `latitude.project` per agent run through the SDK `capture()` option or an OTEL span attribute.
That keeps search, scores, evaluations, and signal discovery focused on the traces that belong together.
# Memory
Source: https://docs.latitude.so/observability/memory
Watch how your agents' persistent memory evolves, with per-record history, diffs, and the trace behind every change.
# Memory
If your agents keep persistent memory, Latitude records every memory operation they run and treats each write like a commit. The **Memory** page shows the current contents of every store, the full history of each record, who read and wrote it, and a diff for every change, each one linked back to the session that caused it.
Not emitting memory operations yet? See [Memory tracing](../telemetry/memory)
to instrument your agent, then come back here to explore the data.
## How it works
Memory operations arrive as ordinary spans through your existing tracing setup. Shortly after a trace completes, Latitude materializes them into a versioned ledger: each write stores the record's full new body, versions are ordered by span end time, and the most recent write wins. Diffs and token counts are derived from those versions, so you get history and diffs without your memory provider having to support them.
Record bodies, diffs, and token counts appear when your instrumentation captures content. With content capture off, Latitude still records each operation with its store, record, and counts, but not the bodies or diffs.
## The Memory page
The **Memory** page lists every store, one row per store, with its record count, total tokens, when it was last updated and last read, the number of sessions that wrote to it, and the number of distinct users who accessed it.
Open a store to browse it like a repository. The left sidebar shows its records as a tree, with record ids split on `/` to form folders, the center pane shows the selected record's current body, rendered as JSON when it parses as JSON, and the header lists the users who accessed the store, each linking to their user page.
### Record activity
Below a record's content, the **Record Activity** panel shows everything that happened to it, across three tabs:
* **Changes**: the record's write history. Each row shows when it was created, updated, or removed, the token delta of that change, and the user behind it. Clicking a row opens the change's diff, and each row links to the span, and to the session or trace it belongs to.
* **Reads**: every time the record was retrieved, with the search query that matched it, the tokens returned, and the user.
* **Users**: a per-user roll-up of reads and writes on this record, each row linking to the user's page.
### Change diffs
Selecting a change on the **Changes** tab swaps the content pane for a GitHub-style unified diff of that version against the previous one, with per-word highlighting and a `+added −removed` token summary. Arrow controls step to newer and older changes, and the selected change is shareable by URL, so you can link a teammate straight to the write that corrupted a record.
## Memory on traces and sessions
Trace and session detail views include a **Memory** row summarizing the interaction's memory footprint: tokens read, added, and removed. Hovering it expands a per-record breakdown grouped by store. The added and removed numbers compare each record's body before and after the interaction, so a record written twice in the same session counts once, by its net change.
A collapsible **Memory changes** section below lists the per-record diffs the interaction produced, grouped by store.
## Memory spans on the Spans tab
Memory operations are first-class spans. They get their own color in the trace waterfall, a **Memory** filter on the Spans tab, and a detail panel showing the operation's store, query, and records. Search results include each record's relevance score. When content capture is off, the panel shows record ids and counts with a `Content not captured` note in place of the bodies.
## Memory on user pages
Each end user's page includes a **Memory stores** section listing the stores that user read or wrote, with the last access time, each linking into the Memory page. It answers "whose sessions have been writing to this store" from either direction.
## Related
* [Memory tracing](../telemetry/memory): Instrument your agent to emit memory operations
* [Spans](./spans): The span model memory operations ride on
* [Sessions](./sessions): The sessions memory changes link back to
* [Users](./users): Per-user activity, including memory stores accessed
# Observability overview
Source: https://docs.latitude.so/observability/overview
Inspect agent traces, spans, sessions, and telemetry metadata in Latitude.
# Observability overview
Latitude observability shows what your agent did in production. It captures the execution path for each interaction, including LLM calls, tool calls, retrieval steps, metadata, timing, token usage, cost, and errors.
For the complete product vocabulary, see [Core Concepts](../getting-started/concepts). This page focuses on the telemetry data you inspect in the Observability area.
## Telemetry model
Latitude uses three telemetry levels:
* **Spans**: individual operations such as LLM calls, tool invocations, retrieval steps, HTTP requests, or custom work.
* **Traces**: complete interactions composed of one or more spans. Traces are the main unit for debugging, search, annotations, scores, evaluations, and signals.
* **Sessions**: optional groups of related traces, usually a multi-turn conversation or workflow identified by a stable session id.
```text theme={"theme":{"light":"github-light","dark":"github-dark"}}
Session
└─ Trace: user turn 1
├─ Span: retrieve context
├─ Span: LLM call
└─ Span: tool call
└─ Trace: user turn 2
├─ Span: LLM call
└─ Span: tool call
```
If your app does not send a session id, Latitude still captures spans and traces. You can add session grouping later with `capture()` in the TypeScript or Python SDK.
## What you can inspect
Observability views help you answer questions such as:
* What happened during this agent interaction?
* Which model, provider, tool, or service was involved?
* Where did latency, cost, or errors come from?
* Which user, session, release, environment, tags, or metadata are attached?
* What annotations, scores, or signals are connected to this trace?
## Trace completion
Latitude waits for a trace to stop receiving new spans before treating it as complete. This prevents downstream features from running on partial agent executions.
Once a trace is complete, Latitude can:
* make the conversation available in [Search](../search/overview)
* run matching [Evaluations](../evaluations/overview)
* apply enabled [Flaggers](../annotations/flaggers)
* update related [Scores](../scores/overview) and [Signals](../signals/overview)
## Next steps
* [Core Concepts](../getting-started/concepts)
* [Start tracing](../telemetry/start-tracing)
* [Traces](./traces)
* [Sessions](./sessions)
* [Memory](./memory)
* [Filters](./filters)
# Sessions
Source: https://docs.latitude.so/observability/sessions
Group related traces into multi-turn conversations.
# Sessions
A session is an optional grouping of related traces into a multi-turn conversation. Traces always exist; sessions only exist when your application sends a session id with telemetry.
## How sessions work
All traces with the same session id are grouped together chronologically.
A session might look like:
1. **Trace 1**: User asks about an order status
2. **Trace 2**: User asks a follow-up question
3. **Trace 3**: User asks to change the order
Each turn is still its own trace with its own spans, scores, annotations, and metadata. The session ties those traces together so you can review the full conversation arc.
## Send a session id
Pass a session id through `capture()` at the request, conversation turn, or agent entrypoint.
```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
import { capture } from "@latitude-data/telemetry"
await capture(
"support-agent-turn",
async () => {
return agent.run(userMessage)
},
{
sessionId: conversation.id,
userId: user.id,
},
)
```
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
from latitude_telemetry import capture
capture(
"support-agent-turn",
lambda: agent.run(user_message),
{
"session_id": conversation.id,
"user_id": user.id,
},
)
```
Every trace captured with the same session id appears in the same session.
## Viewing sessions
Switch between **Sessions** and **Traces** with the tab toggle at the top of the **Traces** page. Sessions is the default view.
The sessions view shows one row per session, with columns for:
* session name (the root span's name, falling back to the session id)
* last activity
* tags
* duration (active execution time)
* time to first token
* cost
* session id
* user id, when provided
* models used
* span count
* error and annotation indicators
Expand a row to see the traces inside the session, or click it to open a detail panel with the full conversation, the span tree, annotations, and any linked signals.
Each trace within a session keeps its own metrics, so you can identify which turn had high latency, high cost, errors, or unusual behaviour.
## Sessions in search and review
Sessions are an aggregation on top of traces. Search, annotations, scores, and signals still operate primarily on traces.
You can:
* filter traces by session id
* search within a specific session
* open traces from a session and annotate them
* use session context to understand failures that unfold across turns
## Related
* [Traces](./traces): The individual interactions that make up a session
* [User tracking](./features/user-tracking): Associate traces and sessions with users
* [Search](../search/overview): Find traces within session-scoped cohorts
* [Scores](../scores/overview): How scores attach to traces
# Spans
Source: https://docs.latitude.so/observability/spans
A span is the smallest unit of captured work inside a trace, such as an LLM call, a tool call, or a retrieval step.
# Spans
A **span** is the smallest unit of captured work in Latitude. Each span represents one operation inside an agent run: an LLM call, a tool call, a retrieval step, an HTTP request, or any custom operation you instrument. One or more spans make up a [trace](./traces).
## What a span captures
A span can record:
* input and output content, when available
* start time, end time, and duration
* success or error status
* tags and metadata
* provider, model, token usage, and cost for LLM calls
## Common span kinds
* **LLM call**: a request to a model, with its prompt, response, token usage, and cost.
* **Tool call**: a function or tool the agent invoked, with its input arguments and result. See [Tool calls](./tool-calls).
* **Retrieval**: a lookup against a vector store, database, or search index.
* **HTTP request**: an outbound call to an external service.
* **Custom**: any operation you instrument yourself.
## How spans form a trace
Spans nest to show the execution path of a single interaction, and a trace groups them into one complete interaction:
```text theme={"theme":{"light":"github-light","dark":"github-dark"}}
Trace: user turn
├─ Span: retrieve context
├─ Span: LLM call
└─ Span: tool call
```
Open any trace to see its spans as a tree, then expand a span to inspect its input, output, timing, and status. Use spans when you need to understand exactly what happened inside a trace, such as where latency, cost, or an error came from.
## Next steps
* [Traces](./traces): the complete interaction a span belongs to
* [Tool calls](./tool-calls): inspect the tools the agent called
* [Sessions](./sessions): group related traces into a conversation
* [Core Concepts](../getting-started/concepts): the full product vocabulary
# Tool calls
Source: https://docs.latitude.so/observability/tool-calls
Inspect the tools your agent invoked, including their inputs, outputs, and errors, as spans inside a trace.
# Tool calls
A **tool call** is a [span](./spans) that records a function or tool your agent invoked while handling a request. Tool-call spans let you see which tools ran, what arguments they received, what they returned, and whether they failed.
## What a tool-call span captures
* the **tool name** that was invoked
* the **input** arguments the agent passed
* the **output** the tool returned
* **error status** when the call failed
* timing and duration
Latitude renders tool input and output as formatted JSON when it can, and as a plain code block otherwise, so you can read structured arguments and results directly.
## Inspecting tool calls
Open a trace and find the tool-call spans in its span tree. Expanding one shows the arguments the agent sent and the result it received. This is the fastest way to answer questions such as:
* Did the agent call the right tool for the request?
* Were the arguments well formed?
* Did the tool return what the agent expected, or did it error?
* Is the agent looping between tools instead of making progress?
## Next steps
* [Spans](./spans): the building block a tool call is recorded as
* [Traces](./traces): the full interaction that contains the tool calls
* [Search](../search/overview): find traces by tool behaviour and other signals
# Tools
Source: https://docs.latitude.so/observability/tools
Monitor every tool your agents define and call, with usage, failure, and latency metrics.
# Tools
The Tools page lists every tool your agents define and call, aggregated from your telemetry. Use it to spot tools that are failing, slow, or never used — no extra instrumentation needed beyond the tool calls and definitions already present in your traces.
## The tools dashboard
The dashboard opens with aggregate metrics for the selected time range — tool count, total calls, error rate, the share of traces and sessions using tools, and how many tools went unused — above a histogram of successful and failed calls with the error rate overlaid.
Below, the table shows one row per tool:
* **Trend**: a per-tool sparkline of successful and failed calls
* **Calls**: call volume, with a column-wide SUM rollup
* **% of traces**: the share and count of traces that called the tool
* **Calls per offer**: how often the model calls the tool across the chat turns that offered it. Can exceed 100% when a single turn calls it multiple times, and requires tool definitions on chat spans.
* **Error rate**: the rate and count of failed calls
* **Duration**: p50 / p95 call duration
* **Last called**: most recent call in the period
Tools also carry status badges: **Unused** (defined and offered to the model, but never called), **Failing** (error rate above the warning threshold), and **No definition** (called, but no chat span in the window carried its definition). The All / Unused / Failing tabs filter the list to each state, and the search box matches tool names.
## Tool detail
Open a tool to see its usage stats, calls over time, and latency over time. The parameters panel shows the most frequent values per parameter across recent calls — useful for spotting malformed or repeated inputs — and the context panel shows which tools and tags co-occur in the same traces. Recent calls list each invocation with its status, duration, input, output, and a link to the trace it belongs to.
## Error view
Toggle the error view to scope the whole page to failed calls: failed calls over time, common errors grouped by message and error type, the parameter values seen on recent failures, the tools it fails alongside, and the recent failed calls with their error output.
## Related
* [Traces](./traces): Review individual interactions
* [Sessions](./sessions): Group traces into conversations
* [Users](./users): Review activity by end user
# Traces
Source: https://docs.latitude.so/observability/traces
Understand the trace model, lifecycle, and how to work with traces in Latitude
# Traces
A trace represents one complete interaction between a user and your agent. It's the primary unit that Latitude's reliability features operate on: evaluations, annotations, and signals all reference traces.
## What's in a Trace
A trace is composed of one or more **spans**: the individual operations your agent performed. A typical trace might include:
* An incoming user message
* One or more LLM calls
* Tool invocations (search, database queries, API calls)
* Retrieval operations
* The final agent response
Traces are defined by your telemetry client and sent to Latitude with their spans already grouped. Latitude computes aggregate metrics from those spans, giving you:
* **Root span name**: The top-level operation name
* **Overall status**: OK or error, derived from constituent spans
* **Total duration**: End-to-end timing
* **Aggregated tokens**: Total input and output tokens across all LLM calls
* **Aggregated cost**: Total cost across all provider calls
* **Span count**: How many individual operations were involved
* **Error count**: How many spans errored
* **Models and providers**: Which LLMs and providers were used
* **Tags and metadata**: Custom labels and structured metadata
## Trace completion
Latitude waits until a trace has finished before using it in downstream workflows. This keeps search, evaluations, flaggers, and signal discovery focused on complete interactions rather than partial agent runs.
Once a trace is complete, Latitude can:
* make it available in [Search](../search/overview)
* run matching [Evaluations](../evaluations/overview)
* apply enabled [Flaggers](../annotations/flaggers)
* connect resulting [Scores](../scores/overview) to [Signals](../signals/overview)
## Viewing Traces
The **Traces** page in your project shows a table of all traces, newest first. Each row displays key metadata at a glance: name, status, duration, cost, tokens, and timestamp.
Click on a trace to open the detail view, which shows:
* **The full conversation**: All messages exchanged between user and agent
* **The span tree**: A hierarchical view of every operation your agent performed
* **Scores**: Any evaluation, annotation, or custom scores attached to this trace
* **Metadata**: Custom fields, timing details, and resource usage
## Searching and Filtering Traces
The project **Search** page lets you search trace conversations in three ways. You can combine them in one query:
* **Semantic search**: write normal text without quotes. Latitude searches by meaning and ranks matching traces first. Example: `customer wants a refund` can find traces where the user says "I need my money back" even if the exact words differ.
* **Literal match**: wrap text in double quotes. Latitude looks for that exact case-sensitive text in the searchable conversation. Example: `"handOffToHuman: true"` matches that exact string.
* **Phrase match**: wrap text in backticks. Latitude searches for those words next to each other and in order, while ignoring case and punctuation between them. Example: `` `human-annotation` `` can match `human-annotation`, `Human annotation`, or `"human": "annotation"`, but not `human something annotation`.
For example:
```txt theme={"theme":{"light":"github-light","dark":"github-dark"}}
refund "handOffToHuman: true" `human-annotation`
```
This finds traces that contain the literal text `handOffToHuman: true`, contain the adjacent phrase tokens `human` and `annotation`, and ranks the remaining matches by how semantically close they are to `refund`.
Click the **Filters** button in the toolbar to open the filter panel. You can combine any number of filters with search to narrow your trace view.
### Available Filters
Filter traces by their completion status. Select one or more:
* **OK**: Trace completed without errors
* **ERROR**: At least one span in the trace errored
* **UNSET**: Status was not reported
Filter by the root span name (the `path` you set in `capture()`). Type a name to match.
Filter traces belonging to a specific session. Useful for viewing the full history of a multi-turn conversation.
Filter traces generated by a specific simulation run.
Filter traces by end-user identifier, if your application passes one via telemetry metadata.
Filter by custom tags attached to traces. Search and select from the list of tags seen in your project.
Filter by the LLM model(s) used in the trace (e.g. `gpt-4o`, `claude-3.5-sonnet`). Search and select from models seen in your project.
Filter by the LLM provider(s) used (e.g. OpenAI, Anthropic, Azure). Search and select from providers seen in your project.
Filter by the service name reported by OpenTelemetry instrumentation.
Set a min/max range to filter traces by total duration.
Set a min/max range to filter by time to first token. This is useful for identifying slow-starting responses.
Set a min/max range to filter by estimated total cost. Useful for finding expensive traces.
Set a min/max range for the number of spans in a trace. Higher span counts typically indicate more complex agent operations.
Set a min/max range for the number of errored spans within a trace.
Set a min/max range for total input tokens across all LLM calls in the trace.
Set a min/max range for total output tokens across all LLM calls in the trace.
Filter by custom key-value metadata. Add one or more conditions in the form `key = value`. This matches against the structured metadata your application sends with its telemetry.
### Combining Filters
All active filters are combined with **AND** logic. Traces must match every active filter to appear. For example:
* **Status** = ERROR **AND** **Cost** > \$1: find expensive failures
* **Models** = gpt-4o **AND** **Duration** > 5s: find slow GPT-4o traces
* **Metadata** `environment` = `production` **AND** **Error Count** > 0: find production errors
## Traces and Sessions
When your application provides a `session_id` with its telemetry, Latitude groups related traces into sessions. You can filter traces by session ID to see the full conversation history, or switch to the Sessions tab on the Traces page for a session-focused view.
## Next Steps
* [Sessions](./sessions): Session-level conversations and aggregation
* [Search](../search/overview): Find traces by meaning, not just metadata
* [Scores](../scores/overview): How scores attach to traces
* [Annotations](../annotations/inline-annotations): Annotating directly from trace views
# Users
Source: https://docs.latitude.so/observability/users
Review activity, sessions, and errors by end user.
# Users
The Users page groups telemetry by the end-user identifier you send with each trace. Use it to see who is using your product, when they were last active, and which traces or sessions are associated with them.
## Send user identifiers
Attach a stable user id when you capture a trace. You can also send a user email when it is useful for support workflows.
```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
import { capture } from "@latitude-data/telemetry"
await capture(
"support-agent-turn",
async () => {
return agent.run(userMessage)
},
{
userId: user.id,
userEmail: user.email,
},
)
```
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
from latitude_telemetry import capture
capture(
"support-agent-turn",
lambda: agent.run(user_message),
{
"user_id": user.id,
"user_email": user.email,
},
)
```
## The users dashboard
The dashboard opens with aggregate metrics for the selected time range — unique users, new users, the share of traces carrying a user id, and traces per user — above an activity histogram. Drag across the histogram to zoom into a narrower period.
Below, the table shows one row per user:
* **Seen at**: last and first activity within the selected period
* **Trend**: a per-user activity sparkline
* **Sessions**: distinct sessions in the period
* **Errors**: the rate and count of sessions with at least one errored trace
* **Cost**: per-user spend, with a column rollup you can cycle between SUM, AVG, and MED
Use the time range, column selector, and search controls to focus on a subset of users. Search matches user ids and emails, and every column with a sort icon is sortable.
## User detail
Open a user to see their lifetime profile: traces, sessions, errored sessions, cost, tokens, average trace duration, and active days, alongside a 30-day activity chart. The page also surfaces the [signals](../signals/overview) affecting this user and the behaviours they triggered, so you can connect one user's complaints to known problems.
Further down, usage breakdowns show which models, providers, and tools this user's traces relied on, followed by their sessions. Toggle the error view to focus on the sessions that failed.
## Related
* [User tracking](./features/user-tracking): How user ids are attached to traces
* [Sessions](./sessions): Group traces into conversations
* [Traces](./traces): Review individual interactions
# Score Analytics
Source: https://docs.latitude.so/scores/analytics
Visualize score trends and quality metrics across your project
# Score Analytics
Score analytics show quality trends across your project: whether quality is improving, which evaluations catch the most failures, and when signals occur.
## Project-Level Dashboard
The project overview shows:
* **Pass/fail distribution**: How many scores passed or failed over time
* **Failure rate trend**: The percentage of failing scores over days or weeks
* **Score volume**: Total scores, broken down by source
Use these metrics for a high-level view of your agent's quality trajectory.
## Evaluation-Level Analytics
Each evaluation has its own analytics page with:
* **Pass/fail trend**: How results change over time
* **Value distribution**: A histogram of score values
* **Volume**: How many traces the evaluation has scored
* **Alignment**: Whether the evaluation agrees with human review when annotations exist for the same traces
Use evaluation analytics to spot regressions, improvements after a fix, or drift from human judgment.
## Signal-Level Analytics
Each signal tracks:
* **Occurrence count**: How many times the signal has been detected
* **Lifecycle state**: Whether the signal is new, escalating, or ongoing
* **Affected users**: The share of users the signal has impacted
## Score-Aware Trace Filtering
Traces and sessions can be filtered by score-derived properties:
* **Score state**: Failing scores, passing scores, or draft annotations
* **Value thresholds**: Scores below a quality threshold
* **Signal linkage**: Traces associated with a specific signal
* **Score source**: A specific evaluation, annotation source, or custom source
This bridges observability and reliability: you can move from a failed evaluation or signal directly to the underlying conversations.
## Filtering Analytics
Analytics dashboards use the same [filter system](../observability/filters) as trace views. Narrow analytics by time range, model, provider, score source, or custom metadata to answer targeted questions such as: "What is the failure rate for GPT-4 traces in production this week?"
## Next Steps
* [Scores Overview](./overview): How the score model works
* [Evaluations](../evaluations/overview): How automated evaluations produce scores
* [Signals](../signals/overview): How failure patterns are discovered from scores
# Scores Overview
Source: https://docs.latitude.so/scores/overview
Understand how scores work as the universal measurement unit in Latitude
# Scores
Scores are Latitude's common measurement unit. Every verdict on an agent interaction—from an evaluation, annotation, flagger, or your own code—is stored as a score. Signals, evaluation dashboards, annotation workflows, and analytics all build on this model.
## What Is a Score
A score is a verdict attached to a trace. Every score has:
| Field | Description |
| --------------- | ------------------------------------------------------------ |
| **Value** | A number between 0 and 1 |
| **Pass / Fail** | Whether the interaction met expectations |
| **Feedback** | Text explaining the verdict |
| **Source** | Where the score came from: evaluation, annotation, or custom |
Scores can also carry resource fields such as duration, token count, and cost.
A score is always associated with a **trace**. It can also be associated with a **span**, **session**, or **signal**.
## Score Sources
### Evaluation Scores
Automated monitors create evaluation scores when a trace matches an evaluation's trigger configuration. These scores power continuous monitoring and show how quality changes over time.
### Annotation Scores
Human reviewers and built-in [flaggers](../annotations/flaggers) create annotation scores. They serve as ground truth for [evaluation alignment](../evaluations/alignment) and provide feedback for signal discovery.
### Custom Scores
Your own code can submit custom scores through the [Latitude API](./api). Use them for domain-specific signals such as satisfaction ratings, task completion, conversion, resolution rate, or downstream validation.
## Drafts and Finalized Scores
Human annotations can start as drafts while you edit them. Drafts are visible in the trace's annotation panel but do not feed analytics, signal discovery, or alignment until they are finalized.
Once finalized, a score becomes part of Latitude's reliability workflows.
## How Scores Flow Through Latitude
Finalized scores feed into:
1. **Signal discovery**: Failed scores can become named, trackable [signals](../signals/overview).
2. **Evaluation generation**: Signals can generate monitors that produce more scores on live traffic.
3. **Alignment**: Annotation scores are compared with evaluation scores on the same traces.
4. **Analytics**: Score dashboards show quality trends across your project.
## Next Steps
* [Annotations](../annotations/overview): How human reviewers create scores
* [Evaluations](../evaluations/overview): How automated monitors create scores
* [Signals](../signals/overview): How failed scores become trackable failure patterns
* [Analytics](./analytics): Visualize score trends
* [Scores API](./api): Submit custom scores programmatically
# Search and review effectively
Source: https://docs.latitude.so/search/guides/search-and-review-effectively
Practical habits for writing queries, saving useful searches, and keeping them useful over time.
# Search and review effectively
Search finds traces; **Save search** turns a query plus filters into a [saved search](../saved-searches) you can revisit, assign, and track. The habits below help your team find the right cohorts and keep the resulting saved searches valuable as your agent changes.
For how search itself works, see [Search Overview](../overview) and [Saved Searches](../saved-searches). Once a cohort is scoped, see [Annotate traces effectively](../../annotations/guides/annotate-effectively) for review habits.
## Pick the right surface for what you want to find
When both query and filters are active, a trace must match both to appear.
| You want to find… | Use… | Example |
| ---------------------------------------------------------- | -------------------------------------- | ------------------------------- |
| Conversations that *sound like* something | Semantic query | `user frustrated about billing` |
| Traces that contain a specific phrase | Exact text query (`"..."`) | `"401 Unauthorized"` |
| Traces in a flow or environment | Filters / metadata | `metadata.flow = "checkout"` |
| Traces your app already tagged | Tags or status | `status = error`, tag `refund` |
| Known failure categories (jailbreak, refusal, tool errors) | [Flaggers](../../annotations/flaggers) | (automatic, no query needed) |
Empty results? Drop quoted text first, widen the time window, then loosen
filters. Over-specific `"exact strings"` are the most common miss.
## Keep saved searches small enough to finish
Before you click **Save search**, check that you could actually work through the matches and that they're varied enough to learn from.
* **Small enough to finish.** If the result set is in the thousands and you're reviewing by hand, tighten filters or shorten the time range until a week of review feels realistic. You can always broaden later.
* **Varied enough to learn.** Twenty different traces teach you more about your agent than two hundred near-identical ones. If every match looks the same, add a filter (model, metadata, span count) or tweak the query for edge cases.
## Name saved searches for what's in them
A good name describes the traces in the cohort, not the query syntax.
| Less useful | More useful |
| ------------------- | ------------------------------------ |
| `q: payment errors` | `Failed payments last 7 days` |
| `search v2` | `Checkout flows over 5 steps` |
| `jailbreak test` | `Jailbreak attempts without refusal` |
When you save:
1. Run the query and filters until the result set looks right.
2. **Save search** with a name a teammate can understand without opening it.
3. Open matches from the trace detail view and annotate as you work through them.
## Investigation vs review vs regression watch
The same feature serves three intents:
**Investigation** (may stay unsaved)
* Narrow time window, specific query.
* Delete the saved search when done, or **Save as new** if you want a permanent cohort derived from what you learned.
**Review** (bounded work)
* Stable query and filters so the cohort stays consistent between sessions.
* Work matches until the team agrees you're through it.
* Leave the saved search in place if you might need to re-sample later.
**Regression watch** (ongoing)
* Filters on metadata or tags that won't break when wording changes (e.g. `metadata.flow = "checkout"`, not a one-off phrase from a single bad trace).
* For anything you want to keep an eye on, point a [monitor](../../monitors/overview) at the saved search so Latitude alerts you when matching traces arrive again, instead of reopening it by hand.
* Update or delete watches when the product changes. Stale saved searches just get in the way.
Saved searches don't send notifications on their own. To get alerted when
new matches show up, point a [monitor](../../monitors/overview) at the saved
search.
## Update vs save as new
When a loaded saved search drifts from what you want:
* **Update saved search**: same intent, refined scope (e.g. extend from 7 to 30 days, add a model filter everyone agrees on).
* **Save as new search**: a related cohort (e.g. same checkout flow but errors only vs all outcomes).
Use **Save as new** when two teams need similar but different views. Use **Update** when everyone shares one definition.
## Flaggers vs saved searches
[Flaggers](../../annotations/flaggers) and saved searches both find traces, but for different jobs.
| | **Flaggers** | **Saved searches** |
| --------------------- | -------------------------------------------------------------- | ------------------------------------------------------------------ |
| **Who finds matches** | Latitude, on every completed trace | You, when you run or reopen the search |
| **Best for** | Known failure categories (jailbreak, frustration, tool errors) | Product-specific cohorts that flaggers don't cover |
| **Output** | Automatic annotations | A bookmarked working set you annotate, export, or inspect manually |
| **Configuration** | Project settings (enable, sampling) | Query + filters + name |
Use flaggers for the built-in failure types. Use saved searches for flows, metadata combinations, and regressions only your product can name. Most teams use both.
## Send metadata and tags your future self will use
The easiest searches start in your app: fields and tags you'll still recognize in a few months.
* **Stable keys**: `metadata.flow`, `metadata.environment`, `metadata.feature`, not one-off debug strings.
* **Values you will filter on**: If you care about "refunds over \$100", emit `metadata.refund_tier = "high"` (or a numeric field) rather than hoping the dollar amount appears in user messages.
* **Tags for cross-cutting flags**: `production`, `canary`, `beta-user`, all easy filter targets alongside semantic search.
You don't need a perfect schema on day one. Add fields when you find yourself re-running the same awkward query twice.
## Keep saved searches up to date
Saved searches go stale when the product, model, or prompts change.
* **Reopen watches you still care about monthly** and skim recent matches. No new matches for months often means update or delete.
* **Avoid duplicates**: two names for the same cohort confuse the team and waste review effort.
* **Watch filter-only saves**: filters with no query and no time limit can grow forever, so long-lived watches should lean on metadata that stays meaningful.
## Common pitfalls
* **Searching for what only lives in tool results.** Use metadata filters instead.
* **Over-literal quoting.** `"the user wants a refund because the order was damaged"` must appear exactly; use semantic search plus a metadata filter if you have one.
* **Saving before the result set looks right.** A saved search is only useful if its query and filters are correct, so check the matches before you save.
* **Giant unbounded saved searches.** A cohort of 5,000 traces won't get reviewed by hand; tighten it first.
* **Confusing flaggers with search.** Flaggers annotate automatically; saved searches are for cohorts you scope and review yourself.
* **Expecting alerts.** A saved search won't email you when a new trace matches; point a [monitor](../../monitors/overview) at it for that.
## What teams often do
* **A clear owner in practice for each saved search under active review**: one person works the cohort, even though everyone can see and open it.
* **Saved searches named for the area that knows them**: checkout with payments, support flows with the team that ships them.
* **A quick pass on new matches**: skim recent hits on the searches you watch before weekly planning.
* **Save as new instead of arguing**: when two squads need slightly different views of the same flow, don't overwrite a shared search.
## Recommended pattern
Start with one or two saved searches per failure mode your team already cares about. Work matches through the trace detail view, and reopen them as part of a weekly habit. For the ones worth watching continuously, point a [monitor](../../monitors/overview) at them so you're alerted automatically; delete the searches that stop being useful.
# Search Overview
Source: https://docs.latitude.so/search/overview
Find traces by meaning or exact text, combine search with filters, and save the searches your team uses repeatedly
# Search
Search lives on the **Traces** page. Use the search bar to find behaviours across production traces by meaning, exact text, or both, then narrow results with filters.
Every trace is searchable: 100% of ingested traces are available for semantic and text search within your project's retention window.
## Types of search
Latitude supports two complementary search modes.
### Semantic search
Semantic search is the default. Type a plain-language description, and Latitude returns traces whose conversations are closest in meaning.
Examples:
* *users complaining about billing*
* *agent gets stuck calling tools*
* *refund requests that were not resolved*
* *assistant gave a vague answer*
Semantic search works even when a trace does not contain those exact words. For example, *user is frustrated* can find traces where the user says *"this is the third time I've explained this"* or *"you're not helping"*.
### Exact text search
Use quotes around words or phrases that must appear exactly in the trace.
```text theme={"theme":{"light":"github-light","dark":"github-dark"}}
"checkout" users abandoning cart
```
This requires *checkout* to appear, then ranks matches by semantic relevance to *users abandoning cart*. Use exact text search when a product name, tool name, error message, or policy phrase matters.
## Combine search with filters
Search finds relevant conversation content; filters narrow it by trace metadata such as status, model, provider, service, tags, user, session, cost, latency, token usage, or custom fields.
For example, search for *agent loops between tools* and filter to production traces from a specific model, service, or customer segment. A trace must match both the query and filters to appear.
## Running a search
1. Type a query in the search bar and press Enter.
2. Add filters to narrow the cohort.
3. Pick a time range if you only care about a recent window.
4. Open any matching trace to inspect the conversation, spans, scores, and annotations.
When you search by meaning, results are ordered by relevance. When you only use exact quoted text, results use the normal trace ordering and table controls.
## Saving a search
When a query, filters, or both become useful enough to revisit, click **Save search** and give it a clear name, such as *Failed payments* or *Tool loops in production*.
Saved searches then appear in the **Saved searches** dropdown next to the search bar. See [Saved Searches](./saved-searches) for the full lifecycle.
## Empty results
If a search returns nothing:
* Reword the query. Semantic search works best with natural descriptions.
* Remove quotes unless an exact phrase is required.
* Widen the time range.
* Loosen filters that may be excluding relevant traces.
## Search and the rest of Latitude
Search is the discovery layer for behaviours Latitude should track. A typical workflow is:
1. Search for a user or agent behaviour you care about.
2. Open representative traces.
3. Annotate traces that show a real failure mode.
4. Latitude turns failed annotations and related scores into [signals](../signals/overview).
5. Generate evaluations from important signals to monitor them automatically on incoming traces.
| Feature | Relationship |
| ----------------------------------------------------------- | -------------------------------------------------------------------------------- |
| **[Saved searches](./saved-searches)** | Revisit useful behavioural cohorts |
| **[Inline annotations](../annotations/inline-annotations)** | Label traces from search as good or bad behaviour |
| **[Signals](../signals/overview)** | Turn failed annotations and scores into trackable production signals |
| **[Flaggers](../annotations/flaggers)** | Automatically detect common categories; use search for product-specific patterns |
## Next steps
* [Saved Searches](./saved-searches): Save behavioural cohorts for repeated review
* [Inline Annotations](../annotations/inline-annotations): Label traces you find through search
* [Signals](../signals/overview): Track recurring failure modes
* [Filters](../observability/filters): Narrow searches with trace metadata
# Saved Searches
Source: https://docs.latitude.so/search/saved-searches
Bookmark useful search queries and filter sets, then reopen them from the Saved searches dropdown
# Saved Searches
A saved search is a reusable search query, filter set, or both. Save it once, then return to the same cohort from the **Saved searches** dropdown on the Traces page.
## What a Saved Search Stores
Each saved search stores:
* **A query**: The text from the search bar, if any
* **A filter set**: The filters active when you saved, if any
* **A name**: A human-readable label
A saved search must include a query, filters, or both. *"All errors in production this week"* might use only filters; *"jailbreak attempts"* might use only a query.
Saved searches are project-scoped. Two projects can have saved searches with the same name; they're independent.
## The Saved searches dropdown
Saved searches live in the **Saved searches** dropdown, on the left of the search bar on the Traces page. Open it to see every saved search in the project. Each row shows:
| Element | What it shows |
| ----------- | --------------------------------------------------------- |
| **Name** | The label you gave the saved search |
| **Query** | A preview of the search text, if the saved search has one |
| **Filters** | How many filters are part of the saved search |
Use the filter box at the top of the dropdown to find a saved search by name. Click a row to reopen the saved search with its query and filters restored. Hovering a row also reveals actions to create or view a [monitor](../monitors/overview), rename, and delete (see below).
## Creating a Saved Search
1. On the **Traces** page, run a query, apply filters, or both.
2. Click **Save search**, or open the **Saved searches** dropdown and click **Save current search** at the bottom.
3. Give it a descriptive name and confirm.
The saved search becomes active right away, and the new entry appears in the **Saved searches** dropdown.
Good names describe what's in the cohort, not how you searched for it. *"Failed payments last week"* is more useful at a glance than *"q: payment errors filter: status=error"*.
## Renaming and Deleting
Hover a row in the **Saved searches** dropdown to reveal its actions, then rename or delete the saved search. Deleting removes only the bookmark; the underlying traces are not affected. Deleting a saved search also removes any [monitor](../monitors/overview) alerts watching it.
## Editing a Saved Search
When you change the query or filters on a loaded saved search, Latitude shows two actions:
* **Update saved search**: Overwrite the saved search with the current query and filters.
* **Save as new search**: Keep the original and create a new saved search from the current state.
Use **Update saved search** for changes you want to keep, such as expanding the time range from 7 to 30 days. Use **Save as new search** when you want a sibling search, such as one that adds a model filter.
The update action is disabled until there's a change to save.
## Workflows
### Investigate a specific cohort
You're investigating reports that the agent gets confused during multi-turn checkout flows.
1. Run a search like `"checkout"` and add a filter for `metadata.flow = "checkout"` and `spanCount >= 5`.
2. Save it as *"Checkout flows over 5 steps"*.
3. Work through the matches, opening each trace to inspect and annotate it.
4. Reopen the saved search whenever you want to return to the same cohort.
### Watch for a recurring issue
You've resolved a tool-retry issue and want to make sure it doesn't return.
1. Run a search filtered to the relevant tag or metadata.
2. Save it as *"Tool retry regressions"*.
3. Reopen it whenever you want to check, or point a [monitor](../monitors/overview) at it to be alerted automatically when matching traces arrive.
### Share a cohort with a teammate
You've found interesting jailbreak attempts and want a teammate to review them.
1. Save the search with a clear name.
2. Tell your teammate the name.
3. Saved searches are shared across the project, so they'll find it in the **Saved searches** dropdown.
## What Saved Searches Don't Do
Saved searches are bookmarks, not subscriptions. On their own they don't notify you, generate evaluation scores, or run background processing when a new trace matches. To get **alerted** when matching traces start arriving, spike, or stay elevated, point a [monitor](../monitors/overview) at the saved search. For automated detection that produces scores and feeds signal discovery, see [Flaggers](../annotations/flaggers).
## Next Steps
* [Monitors](../monitors/overview): Alert on a saved search when matching traces arrive, spike, or stay elevated
* [Search Overview](./overview): How search itself works
* [Flaggers](../annotations/flaggers): Automatic annotators for common failure categories
* [Inline Annotations](../annotations/inline-annotations): Leave feedback on traces from your saved search
* [Filters](../observability/filters): The shared filter system used inside searches
# GDPR
Source: https://docs.latitude.so/security/compliance/gdpr
Latitude's GDPR readiness, AWS eu-central-1 data boundary, and customer data protection practices.
# GDPR
Latitude is built with European data protection expectations in mind. Hosted Latitude stores and processes customer data and runs Latitude-managed inference in AWS eu-central-1 in Frankfurt, Germany. SDK and internal safeguards reduce PII exposure in telemetry.
Latitude's GDPR readiness review is in progress and is expected to be completed by the end of May 2026.
GDPR is a regulatory framework, not a point-in-time certification like ISO 27001 or a SOC II audit report. Latitude's GDPR work covers documentation, operational practices, data handling, and customer-facing process readiness.
## Status
| Area | Status |
| --------------------- | --------------- |
| GDPR readiness review | In progress |
| Expected completion | End of May 2026 |
## Data protection approach
Latitude's GDPR readiness work centers on:
* AWS eu-central-1 boundary for hosted customer data and Latitude-managed inference
* organization and project scoping for customer data
* SDK redaction for common security-sensitive fields
* configurable redaction for customer-specific PII patterns
* internal controls that minimize unnecessary sensitive-data exposure
See [Data protection](../data-protection) and [PII redaction](../pii-redaction) for the technical controls available today.
## Customer responsibilities
Customers control what telemetry they send to Latitude. To support GDPR-aligned usage, customers should:
* avoid sending unnecessary personal data in prompts, tool outputs, metadata, or annotations
* configure custom redaction for application-specific PII
* scope agents and products into separate projects when their data should not mix
* maintain an internal legal basis and retention policy for the telemetry they collect
## Availability
GDPR documentation and process details will be updated as the readiness review is completed.
# ISO 27001
Source: https://docs.latitude.so/security/compliance/iso-27001
Latitude's ISO 27001 audit status and information security management program.
# ISO 27001
Latitude is undergoing ISO 27001 audit work for its information security management system. Completion is expected by the end of May 2026.
Latitude does not claim ISO 27001 certification until the audit is finalized.
## Status
| Area | Status |
| ----------------------- | ------------------------ |
| ISO 27001 audit | In progress |
| Expected completion | End of May 2026 |
| ISO 27001 certification | Pending audit completion |
## Scope
ISO 27001 focuses on the information security management system behind Latitude's hosted platform.
Latitude's audit work covers areas such as:
* risk management and control ownership
* access control and least-privilege workflows
* infrastructure and operational security
* secure product development and change management
* incident response and continuity planning
* vendor and subprocessor management
* data protection for customer telemetry and derived reliability data
## AWS eu-central-1 operating boundary
Hosted Latitude stores and processes customer data and runs Latitude-managed inference in AWS eu-central-1 in Frankfurt, Germany. This boundary applies to product data such as traces, spans, sessions, annotations, scores, signals, evaluations, and search indexes.
See [Data protection](../data-protection) for more detail.
## Availability
ISO 27001 documentation and certification evidence will be available once the audit is complete.
# SOC II
Source: https://docs.latitude.so/security/compliance/soc2
Latitude's SOC II certification and security controls.
# SOC II
Latitude is SOC II certified as part of its security and compliance program.
## Status
| Area | Status |
| -------------------- | -------------------- |
| SOC II audit | Complete |
| SOC II certification | Certified |
| SOC II report | Available on request |
## Scope
Latitude's SOC II controls cover operating an AI observability platform that handles production telemetry and reliability workflows.
Relevant control areas include:
* access control and authentication
* tenant isolation across organizations and projects
* operational monitoring and incident handling
* change management for product and infrastructure updates
* data protection for traces, sessions, scores, signals, and evaluations
* vendor and infrastructure risk management
## Data processing boundary
Hosted Latitude stores and processes customer data and runs Latitude-managed inference in AWS eu-central-1 in Frankfurt, Germany.
See [Data protection](../data-protection) and [PII redaction](../pii-redaction) for the product safeguards that support this control environment.
## Availability
SOC II documentation and audit artifacts are available to customers and prospects on request. Contact [support@latitude.so](mailto:support@latitude.so) to request the report.
# Data protection
Source: https://docs.latitude.so/security/data-protection
How Latitude keeps hosted customer data and Latitude-managed inference within AWS eu-central-1.
# Data protection
Hosted Latitude is designed for teams that need AI observability while keeping production agent data within a defined European region. It stores and processes customer data and runs Latitude-managed inference in AWS eu-central-1 in Frankfurt, Germany.
This covers data Latitude receives or creates while operating the platform, including traces, spans, sessions, annotations, scores, signals, evaluations, search indexes, and related metadata.
## European data and inference boundary
Latitude-managed inference includes the internal model calls that power product workflows such as:
* signal discovery and clustering
* signal names and descriptions
* flagger review for supported failure categories
* evaluation generation and alignment workflows
* search, scoring, and reliability analysis
Your application can call any model provider you choose. This boundary applies only to hosted Latitude and its managed inference.
## Tenant and project boundaries
Latitude scopes data by organization and project:
* **Organizations** are the top-level access boundary.
* **Projects** scope traces, search, scores, signals, evaluations, and related workflows.
* Signal detection runs within a project, so teams can keep unrelated agents separate by routing them to separate projects.
See [Group traces by project](../observability/guides/group-traces-by-project) for recommended project scoping patterns.
## Data minimization
Latitude reduces sensitive-data exposure throughout the telemetry pipeline:
* SDKs redact common security-sensitive attributes before export by default.
* Customers can configure additional SDK redaction patterns for application-specific PII.
* Internal systems avoid exposing raw payloads when summaries, derived labels, scores, or references are enough.
* Customer-facing workflows use scoped project data instead of broad organization-wide processing.
See [PII redaction](./pii-redaction) for implementation details and examples.
## Compliance status
Latitude is SOC II certified. See [SOC II](./compliance/soc2) for scope and report availability.
ISO 27001 audit work and GDPR readiness review are in progress and expected to be completed by the end of May 2026. Latitude does not claim ISO 27001 certification or a finalized GDPR compliance review until those reviews are complete.
# PII redaction
Source: https://docs.latitude.so/security/pii-redaction
How Latitude reduces PII exposure with ingest-side redaction, SDK attribute masking, and internal data handling safeguards.
# PII redaction
Latitude reduces exposure of personally identifiable information (PII) and other sensitive values in telemetry. There are two independent controls: **ingest redaction**, which Latitude applies to span content before storing it, and **SDK attribute masking**, which your application applies before exporting.
Ingest redaction covers built-in categories such as email addresses and card numbers, plus [custom rules](#custom-rules) for identifiers specific to your business.
Redaction is best effort. Customers should avoid sending unnecessary regulated or highly sensitive data to Latitude and configure custom redaction patterns for application-specific identifiers.
## Ingest PII redaction
Opt-in, per project. When enabled, Latitude scans span content for the categories you configure and replaces matches with a labeled placeholder before the span is persisted.
Redaction applies only to spans ingested after you enable it, and redacted content cannot be recovered. Existing spans are never rewritten, and there is no way to restore a redacted value.
### What it does and does not catch
Detection is pattern based. It reliably catches structured identifiers, and it does **not** catch names, addresses, or free-form personal detail — those need semantic understanding that a deterministic matcher cannot provide.
| Category | Default | What it matches |
| -------------------------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Email addresses | On | Addresses with a domain and a two-or-more character suffix, including non-ASCII names and percent-encoded addresses in URLs |
| Phone numbers | On | International numbers written with or without separators (`+44 20 7183 8750`, `+1-415-555-2671`) and separated North American forms |
| Credit card numbers | On | 13–19 digits passing the Luhn checksum with a known issuer prefix, compact or grouped with spaces, dashes or slashes |
| IBANs | On | Bank account numbers passing the mod-97 checksum, in any case, compact or grouped |
| US Social Security numbers | On | Separated `NNN-NN-NNNN` in a valid range, and ITINs |
| API keys and secrets | On | Recognizable provider key formats, private key blocks, connection-string passwords, and values assigned to a credential-shaped key such as `DATABASE_PASSWORD` or `api_key` |
| IP addresses | Off | IPv4 and IPv6 addresses |
IP addresses are off by default because a version string such as `1.2.3.4` and a dotted quad are the same string. Every category can be toggled individually.
### Known limits
Deterministic detection has edges, and it is more useful to know where they are than to discover them. As of the current version:
* **Names, street addresses, dates of birth, and health or insurance identifiers are not detected at all.** These need semantic understanding.
* **Numeric JSON values are never scanned.** A card number sent as `{"pan": 4111111111111111}` — a JSON number rather than a string — is stored as-is, because scanning number literals would corrupt the numeric identifiers that fill tool output. Send values that may contain personal data as strings.
* **A bare ten-digit phone number or nine-digit SSN is not matched.** Without separators they are indistinguishable from the numeric identifiers in tool output.
* **National phone formats with no country code are not matched**, such as `07700 900123` or `06 12 34 56 78`, and neither is the parenthesised international form `+44 (20) 7183 8750`.
* **A credential under a key named only `*_KEY`** is not matched unless the name also contains `api`, `secret`, `private`, `access`, `auth`, `client`, `encryption` or `signing`. Keys such as `idempotency_key`, `partition_key` and `cache_key` are far too common in tool output to treat as credentials.
* **Credential detection reads the key next to the value**, so it works in a stringified payload or a log line. In already-parsed message content the key and value are separate fields, and only the value is scanned.
* **A 16-digit numeric identifier beginning with 4 has roughly a one-in-ten chance** of passing the card checks and being redacted, and one beginning with 5 roughly one in twenty. That is inherent to Luhn plus an issuer prefix.
* **Some numeric sequences read as phone numbers.** Three groups of 3, 3 and 4 digits, such as latency percentiles printed as `250 300 1000`, cannot be told apart from a phone number without reading the surrounding prose.
When a value is redacted that should not have been, the fix is to turn off the category that matched it: the placeholder names the category, so it tells you which one.
### Enabling it
**Dashboard** — **Settings → Privacy**, per project. Organization owners can also set an organization-wide policy that applies to every project, and lock it so projects cannot weaken it. Changing a project policy needs the owner or admin role; changing the organization policy needs owner.
**API** — `settings.redaction` on `PATCH /v1/projects/{projectSlug}`, also available through the SDKs and the `latitude` CLI.
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl -X PATCH https://gateway.latitude.so/v1/projects/my-project \
-H "Authorization: Bearer $LATITUDE_API_KEY" \
-H "Content-Type: application/json" \
-d '{"settings":{"redaction":{"mode":"enforce","entities":["email","credit_card"]}}}'
```
A change takes effect within a minute.
### Scope
Scanned: message content, tool call arguments and results, reasoning, span attributes holding text or numbers, span events, and resource attributes. Boolean attributes are not scanned — no detector can match `true` or `false`.
Redaction replaces values; it never removes an attribute. Every attribute your exporter sends is stored, so the attribute view stays a faithful record of the span with sensitive values marked.
Not scanned by default: the metadata map and tags. These are usually operational, and redacting them removes values you filter and group by — enable the metadata scope if you put personal data there.
Never scanned: span names, service names, model identifiers, and other enum or identifier columns, plus binary payloads in file and image parts.
### User identifiers
`userId` and `userEmail` can be kept as-is or replaced with a stable pseudonym. Pseudonyms are deterministic and scoped to your organization, so filtering, grouping, and per-user analytics keep working while the underlying identity is no longer stored. Self-hosted deployments with no pseudonym secret configured remove the identifier entirely rather than pseudonymizing it.
The pseudonym replaces the identifier everywhere it appears on the span, not only in the user column: the attribute it was read from, resource attributes, metadata, and tags all get the same value. Metadata and tags are covered here even when the metadata scope is off.
### What redacted content looks like
Redacted values appear as a labeled marker in place of the original — the dashboard renders them as a small tag naming the category. This is intentional: you need to be able to tell the difference between content that was removed and content that was never sent.
You may also see an oversized-field marker. Fields above 1 MB are removed whole rather than stored unscanned, so nothing in them was necessarily personal data.
A numeric attribute that matches — a card number sent as an integer, for example — moves to the span's text attributes as a single marker, because a numeric field cannot hold one. Its key does not change, so you can still find it where you expect.
### Custom rules
The built-in categories cover structured identifiers that look the same for everyone. Anything shaped like *your* data — an internal account format, a list of known customer references, a vendor attribute Latitude does not recognise — needs a rule you define. Rules are part of the redaction policy, so they follow the same **Set by** control as everything else on the card, and come in three kinds.
**Attribute key.** Replaces the whole value of a span attribute you name, wherever it appears: the attribute map, resource attributes, and metadata, whatever type the attribute arrived as. A number or boolean moves to the text attributes as the marker, since a typed field cannot hold one. The key itself is kept, like every other redaction, so a removed value never looks like an attribute you failed to send. Nothing is scanned, so this kind cannot match the wrong value. Name a key exactly (`acme.customer.tax_id`) or by prefix (`acme.customer.*`). This is the server-side counterpart to SDK attribute masking below, and unlike that one it takes effect without redeploying your application.
**Exact terms.** Removes a list of literal strings — known account numbers, internal codenames, a set of employee addresses. Matches only what you list. Whole-word matching is on by default, so `ACME` does not match inside `ACMEXYZ`, and matching is case-insensitive unless you say otherwise.
**Pattern.** Removes anything matching a regular expression. The most capable kind and the only one that can remove values you did not intend, so it is checked before it can be saved.
Each rule carries a label, which is what appears in stored content: a rule labelled `ACCOUNT_NUMBER` leaves `[REDACTED_ACCOUNT_NUMBER]` behind. Labels cannot reuse a built-in category name.
Custom rules are configured in the dashboard only. They are not part of `settings.redaction` on the API yet, so a `PATCH` that changes other redaction fields leaves your rules exactly as they were. Sending `rules: []` is the only way to clear them, and the API cannot express that.
#### How a pattern is checked
Latitude refuses to save a pattern that would be unsafe to run: one that cannot compile, matches the empty string, uses a backreference, or backtracks catastrophically — the last checked both by inspecting the expression and by timing it against generated input.
What it does **not** judge is whether a pattern is too broad. Only your data can answer that, so nothing stops you saving `\d{4,}`; the check below is what tells you it would eat every long number in your tool output.
#### Check before you enable
The editor tells you whether a rule is safe to run. To see whether it removes the right things, use **Check against recent spans** on the Privacy page. It runs the policy you have on screen against spans already stored and reports how many values each category and rule would remove — including the ones that matched nothing, which is how you tell a new rule is not doing what you meant — followed by each distinct change with the number of spans carrying it. Nothing is written, and no policy is saved.
This is worth doing every time. Redaction is not retroactive and cannot be undone, so the first time you enable a rule for real is otherwise the first time you find out what it removes.
#### How organization and project rules combine
Rules follow the card's **Set by** control like the rest of the policy. A project set to *This project* carries its own rule list, which **replaces** the organization default rather than adding to it — the same way the categories behave. Switching to *This project* seeds the list from the organization default, so taking ownership does not lose anything. Locking the organization default stops projects changing rules at all.
### Things to know before enabling
* **The raw payload is buffered before redaction runs.** Latitude acknowledges your export first and redacts in a background worker, so the unredacted payload exists briefly in the ingestion queue and, for large batches, in object storage. Queue entries are bounded and object-storage buffers are deleted after processing, with a one-day lifecycle rule as a backstop. Redaction is a control over what Latitude *stores*, not a guarantee that unredacted content never reaches our infrastructure.
* **Search matches redacted text.** Full-text search indexes are built from the stored content, so a redacted value is no longer findable.
* **Deduplication does not span a policy change.** Identical messages hash differently before and after redaction, so cross-trace message deduplication treats them as distinct.
## SDK attribute masking
### Masked by default
The TypeScript and Python SDKs mask common security-sensitive attributes before export.
Redacted by default:
* HTTP authorization headers
* HTTP cookies
* HTTP API key headers such as `x-api-key`
* database statements, which may contain sensitive values
The default mask is `******`.
### Custom attribute patterns
Add custom attribute patterns when your app places PII or secrets in known attributes, metadata fields, headers, or prompt variables.
#### TypeScript
```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
import { Latitude } from "@latitude-data/telemetry"
import { createOpenAIInstrumentation } from "@latitude-data/telemetry/instrumentations/openai"
import OpenAI from "openai"
const latitude = new Latitude({
apiKey: process.env.LATITUDE_API_KEY!,
project: process.env.LATITUDE_PROJECT_SLUG!,
instrumentations: [createOpenAIInstrumentation(OpenAI)],
redact: {
attributes: [/^password$/i, /email/i, /phone/i, /secret/i],
mask: () => "[REDACTED]",
},
})
await latitude.ready
```
#### Python
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
import re
import openai
from latitude_telemetry import Latitude, RedactSpanProcessorOptions
latitude = Latitude(
api_key="your-api-key",
project="your-project-slug",
instrumentations={"openai": openai},
redact=RedactSpanProcessorOptions(
attributes=[
re.compile(r"^password$", re.IGNORECASE),
re.compile(r"email", re.IGNORECASE),
re.compile(r"phone", re.IGNORECASE),
re.compile(r"secret", re.IGNORECASE),
],
mask=lambda attr, value: "[REDACTED]",
),
)
```
## Internal redaction and data handling
Latitude's internal systems are designed to avoid unnecessary sensitive-data exposure:
* Internal AI workflows receive only the trace context needed for the task.
* Product workflows prefer summaries, derived labels, scores, and signal examples over raw payloads.
* Sensitive implementation details and omitted payloads are not exposed in generated customer-facing explanations.
* Latitude-managed inference stays within the hosted data boundary described in [Data protection](./data-protection).
## Recommended customer controls
For strongest protection:
1. Do not send fields your team does not need for debugging, search, scoring, or signal discovery. Nothing beats not collecting it.
2. Add custom SDK attribute patterns for app-specific PII fields, so those values never leave your infrastructure.
3. Enable ingest redaction as a backstop for the structured identifiers that slip through anyway.
4. Add [custom rules](#custom-rules) for the identifier formats specific to your business, which no built-in category can know about. Check each one against recent spans before enabling it.
5. Use project boundaries to avoid mixing traces from unrelated agents or products.
6. Review metadata before adding it to spans, sessions, scores, or annotations.
7. Keep API keys and secrets out of prompts, tool outputs, and trace metadata whenever possible.
# Create a signal
Source: https://docs.latitude.so/signals/create
Define a signal yourself when you already know the behavior you want to track, using the New signal builder.
Signals usually come from [discovery](./overview): Latitude groups similar failed scores into named patterns without you defining them first. You can also create a signal yourself when you already know the behavior you want to track. This page covers that path.
Discovery finds what you didn't know to look for. Creating a signal manually tracks what you already know matters: a specific failure mode, a policy you need to enforce, or a behavior you want measured from day one.
## When to create one manually
* You already know a failure mode and want it tracked and counted from now on.
* You want to enforce a known requirement, such as a format, a policy, or task success.
* The behavior is specific enough that you can describe it or write a rule for it.
If you are exploring rather than tracking something specific, start with [Search](../search/overview) and [Behaviours](../behaviours/overview) instead, and let discovery surface patterns.
## Open the builder
On the Signals page, select **New signal**. The builder opens on a short intro with two ways forward:
* **Generate signal**: describe the behavior and let Latitude build the whole signal.
* **Configure manually**: build it yourself step by step.
Both paths end the same way: a signal with a detector that checks every new matching session and adds the ones that exhibit the behavior. That detector is an [evaluation](../evaluations/overview).
## Describe it and let Latitude build it
In the "What do you want to track?" field, describe the behavior in plain language. For example:
> Sessions where the ticket-cancellation tool fails and the user gets frustrated.
Select **Generate signal**. Latitude reads your description, drafts a detector, chooses a scope and sampling rate, tests it against recent sessions, names it, and creates it. When it finishes, you land on the new signal's detail page, where you can review it and edit anything.
This is a good starting point even when you plan to adjust the result. You can reopen the builder from the signal to change the detector, scope, or sampling. AI generation is rate limited, so if you generate several in a row you may need to wait before the next one.
## Configure it manually
Choosing **Configure manually** runs a four-step builder.
Pick how Latitude decides whether a session matches, using one of three [detection methods](../evaluations/detection-methods): a set of conditions, an LLM judge, or a custom script. This check runs automatically on every new matching session, and a session that passes joins the signal.
Choose which sessions get checked and how many of them. See [Scope](#scope) below.
Try the detector on recent sessions before saving. See [Test](#test) below.
Give the signal a **name** and a **description**. Both are required. The name shows up in the signals list, so pick something your team will recognize.
### Scope
By default a detector runs on every session in your project. Narrow it with filters on **Tags**, **Services**, **Models**, **Providers**, or **Metadata**. With filters set, only matching sessions run through the detector and everything else is ignored.
The sampling slider controls how many of those sessions get checked, from 0 to 100 percent. It defaults to 10 percent.
* A set of conditions is free and instant, so checking 100 percent is usually fine.
* An LLM judge or a script that calls an LLM costs money and time per check. On high traffic, sampling a slice still catches the pattern for much less.
* Setting sampling to 0 pauses the signal: no sessions are checked.
See [Triggers](../evaluations/triggers) for more on scope and sampling.
### Test
The Test step runs your detector against recent sessions from your project and shows how it scored each one. Nothing is saved. Each session gets a verdict: **match**, **no match**, **skipped** (for example, not embedded yet), or **errored**. If the verdicts look off, go back, adjust the detector or scope, and run the preview again.
## After you create a signal
A new signal starts empty and collects matches from new sessions onward. It does not scan your history.
From there it behaves like any signal. It appears in your Signals list, its occurrences and trends build up as matching sessions arrive, and you can triage, mute, monitor, or delete it from its detail page. See [Signal management](./management).
You can edit the detector, scope, and sampling of a signal you created at any time, and switch detection methods freely. Edits apply going forward. A detector you define runs exactly as you wrote it. Unlike a detector Latitude generates from a discovered signal, it is not automatically realigned to human annotations, so it keeps doing what you specified. See [Alignment](../evaluations/alignment).
## Related pages
* [Detection methods](../evaluations/detection-methods): the three ways to define a detector
* [Custom scripts](../evaluations/custom-scripts): the scripting reference
* [Signals overview](./overview): how discovery finds signals for you
* [Signal management](./management): triage, monitor, mute, and manage signals
# Signal management
Source: https://docs.latitude.so/signals/management
Triage signals, monitor them with evaluations, resolve what you fix, ignore noise, and keep your signal list focused.
# Signal management
Once a signal exists, whether Latitude [discovered](./overview) it or you [created](./create) it, you work with it from its detail page. This page covers its states and the actions you can take.
## Signal states
A signal carries one or more states:
* New: discovered or created in the last 7 days.
* Escalating: occurrences are rising faster than the normal pattern for this time of week. Escalation is detected automatically and opens an incident.
* Ongoing: the steady state, once no other state applies.
* Resolved: you marked the problem as fixed. The signal is archived but keeps watching for recurrences.
* Regressed: a resolved signal started occurring again and was reopened.
* Ignored: you marked the signal as noise. It is archived, monitoring stops, and notifications are muted.
A signal can carry several states at once, for example New and Escalating. Separately, a signal with an active evaluation shows an Evaluated marker.
The Signals page has two tabs, Active and Archived. Resolved and ignored signals live in Archived; everything else is Active.
## Triage
When a signal appears:
1. Read its description to understand the pattern.
2. Open example sessions to see where it happened.
3. Judge how much it matters: a safety risk, a quality problem, or a rare edge case.
4. Act on it: assign an owner, set a priority, start monitoring, resolve it once fixed, ignore it as noise, or delete it.
Assign a signal to an org member and set its priority (Urgent, High, Medium, Low, or none) so the right person picks up the right thing first. You can filter the list by assignee.
## Monitor a signal
Monitoring attaches an evaluation that scores new sessions for the same behavior. For a discovered signal, open it and choose Generate an evaluation: Latitude builds a detector from the signal's examples, annotations, and scores, then keeps it aligned to human judgment. A signal you created already has the detector you defined.
From the signal you can:
* Change the sampling rate to trade coverage for cost.
* Realign a generated evaluation after adding annotations.
* Remove the evaluation to stop scoring new sessions.
Unresolve or unignore a signal before you generate an evaluation for it — archived signals can't start monitoring.
See [Evaluations](../evaluations/overview) for how detectors work, and [Create a signal](./create) to define one yourself.
## Investigate
For a signal worth digging into, review several example sessions and ask:
* What user inputs lead to it?
* Is the agent consistently wrong, or is the failure intermittent?
* Are there shared patterns in context, tools, retrieval, model, or prompt behavior?
* If an evaluation is attached, is it too strict, too lenient, or drifting from human review?
## Resolve and ignore
Resolve a signal once you've fixed the underlying problem. Resolving moves it to the Archived tab and, by default, keeps its evaluations running so Latitude notices if the problem comes back. Turn off "Keep evaluating" in the resolve dialog if you also want monitoring to stop; the default for that switch is a project setting.
Ignore a signal that isn't worth acting on. Ignoring moves it to Archived, archives its evaluations so monitoring stops, and mutes its notifications. Occurrences that Latitude discovers from annotations still attach to the ignored signal, so the noise keeps flowing into one place instead of spawning new signals.
Both actions are reversible: unresolve reopens a signal without marking it as regressed, and unignore returns it to the active list with notifications re-enabled.
## Mute notifications
Muting is a pure notification switch, separate from resolving and ignoring. A muted signal keeps collecting occurrences and still opens incidents when it escalates — you just don't get notified. Use mute when you want to keep watching a signal in the list without the pings. Ignoring a signal mutes it automatically; resolving or unignoring one re-enables its notifications.
## Delete a signal you created
A signal you created can be renamed or deleted from its detail page. Deleting also archives its evaluation and can't be undone. Existing scores stay in analytics. Discovered signals can't be deleted; ignore them instead.
## Catching regressions
After you ship a fix, resolve the signal and leave "Keep evaluating" on. Latitude keeps scoring new sessions; the first occurrence after the resolve reopens the signal, marks it as Regressed, and notifies you (the assignee when one is set, otherwise the project's members). You don't have to watch for it by hand. If [agent dispatch](/agent-dispatch/overview) is enabled with the "Regressed signal" trigger, Latitude also wakes your coding agent to investigate the regression.
## Keep signals manageable
As your project matures:
* Assign and prioritize signals so the important ones stand out.
* Monitor the signals you need ongoing coverage for.
* Resolve fixed problems and ignore noise so real problems stay visible.
* Keep descriptions clear enough for the next teammate to understand.
## Next steps
* [Signals overview](./overview): how discovery finds signals
* [Create a signal](./create): define one yourself
* [Monitors](../monitors/overview): watch saved searches, tools, users, and sessions
* [Evaluations](../evaluations/overview): monitor a signal on live traffic
# Signal discovery
Source: https://docs.latitude.so/signals/overview
Understand how Latitude turns annotations, scores, evaluations, and flaggers into trackable production signals.
# Signal discovery
Signals are recurring behavior patterns in your agent's production traffic, usually failures. Latitude discovers most of them for you by grouping similar failed scores, and gives each signal a name, examples, trends, and a lifecycle your team can act on.
The loop is the same whether a signal is discovered or defined: find the behavior, explain it, monitor it, fix it, and watch for it coming back.
Most signals are discovered for you. You can also [create one yourself](./create) when you already know the behavior to track: a specific failure, a policy to enforce, or a check you want from day one.
## How signal discovery works
```text theme={"theme":{"light":"github-light","dark":"github-dark"}}
Traces
↓
Annotations, flaggers, evaluations, and custom checks
↓
Scores
↓
Signal discovery
↓
Signals with examples, trends, status, and linked evaluations
```
### 1. Traces capture real behavior
Your telemetry sends real user and agent interactions into Latitude as traces. These traces are the raw material for signal discovery.
### 2. Signals produce scores
Latitude uses several signal sources to decide whether a trace represents good or bad behavior:
* **Annotations**: Human feedback left on traces during review.
* **Flaggers**: Built-in automatic annotators for categories such as frustration, refusal, jailbreaking, tool errors, and empty responses.
* **Evaluations**: Automated monitors that track signal patterns across incoming traces.
* **Custom scores**: Domain-specific verdicts you submit from your own systems.
All of these produce [scores](../scores/overview): Latitude's common unit for verdicts, feedback, analytics, and signal discovery.
### 3. Failed scores become signal candidates
When a score fails, Latitude compares its feedback and trace context against existing signals.
* If it matches an existing signal, the score becomes a new occurrence of that signal.
* If it does not match, Latitude can create a new signal with a generated name, description, and example traces.
You do not need to predefine every failure category. Latitude learns signal clusters from the failures that appear in your traffic.
### 4. Signals become monitors
Important signals can generate [evaluations](../evaluations/overview). These monitors watch live traffic for the same failure pattern, measure it over time, and detect regressions after you fix it.
As new annotations, flagger matches, and scores arrive, Latitude can realign the evaluation so it stays calibrated to the latest signal.
## The Signals page
The Signals page shows discovered failure patterns for your project. Summary cards highlight regressions, escalating signals, active signals, new signals, and total events. A histogram shows signal occurrences over time.
The table lists each signal with:
* **Signal name**: The generated name for the failure pattern
* **Status**: The current lifecycle state
* **Trend**: Recent occurrence activity
* **Total events**: How many times the signal has been detected
* **Affected users**: The percentage of users impacted
* **Evaluations**: Linked monitoring evaluations and their status
Use the **Active / Inactive** tabs to switch between current signals and resolved or ignored ones.
## Signal detail
Click any signal to open its detail page:
The signal page shows the signal description, lifecycle state, impact (affected traces, sessions, users, and cost), assignee and priority, a trend chart, recurring patterns, linked evaluations, an examples carousel, and the recent traces where the signal was detected. Open example traces to understand what triggered the signal and what the agent did wrong.
## Signal lifecycle
A signal's status is shown as chips on the signal:
| State | Meaning |
| ---------- | --------------------------------------------------------------------------- |
| New | Discovered or created within the last 7 days |
| Escalating | Occurrences are rising faster than the normal pattern for this time of week |
| Ongoing | The steady state, once no other state applies |
| Resolved | Marked as fixed; archived but still watching for recurrences |
| Regressed | A resolved signal started occurring again and was reopened |
| Ignored | Marked as noise; archived, unmonitored, and muted |
A signal can hold more than one state at once. A signal created this week that is also spiking shows as both New and Escalating.
Two more markers appear alongside the state:
* Evaluated: the signal has an active evaluation checking new sessions. See [Evaluations](../evaluations/overview).
* Muted: notifications are silenced; the signal keeps tracking occurrences and opening incidents. See [Signal management](./management).
Resolved and ignored signals live in the Archived tab; everything else is Active.
## Common workflows
### Investigate sessions
Use the signal's example sessions to understand common user intents, missing context, tool failures, retrieval problems, prompt gaps, or model behavior that needs to change.
### Generate an evaluation
Choose Generate an evaluation to attach an automated detector to the signal. It scores new sessions and tracks whether the behavior is still happening. See [Signal management](./management).
### Triage, resolve, and ignore
Assign an owner and a priority so the right person picks it up. Resolve a signal once the underlying problem is fixed — Latitude reopens it and alerts you if it comes back. Ignore a signal that isn't worth acting on: it moves to Archived, monitoring stops, and notifications are muted. Mute a signal you want to keep watching without the pings. For a signal you created that you no longer need, delete it.
## Related pages
* [Create a signal](./create): Define a signal yourself
* [Signal management](./management): Triage, monitor, resolve, ignore, and manage signals
* [Monitors](../monitors/overview): Watch saved searches, tools, users, and sessions
* [Annotations](../annotations/overview): Leave human feedback on traces
* [Flaggers](../annotations/flaggers): Detect common failure categories automatically
* [Evaluations](../evaluations/overview): Monitor known failure patterns
* [Scores](../scores/overview): Understand Latitude's common measurement unit
# Claude code
Source: https://docs.latitude.so/telemetry/claude-code
# Claude Code telemetry
Stream Claude Code conversations into Latitude as traces. After setup, Claude Code turns appear in your project's **Traces** view with prompts, responses, tool calls, and tool results.
## Prerequisites
* A [Latitude account](https://console.latitude.so/login) with a project
* Claude Code installed locally
* Node.js available on your `PATH`
This integration works with local Claude Code surfaces that support hooks, including the CLI, desktop app, and IDE extensions. It does not run inside the hosted Claude web app.
## Install
1. In Latitude, copy your project slug from the project sidebar.
2. Create or copy an API key from **Settings → API Keys**.
3. Run the installer:
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
npx -y @latitude-data/claude-code-telemetry@latest install
```
The installer prompts for your API key and project slug, then configures Claude Code to export telemetry after each turn.
You can also pass values directly:
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
npx -y @latitude-data/claude-code-telemetry@latest install \
--api-key=lat_xxx \
--project=your-project-slug \
--yes
```
## Restart and verify
Fully quit and relaunch Claude Code, then run any prompt. Open your Latitude project and go to **Traces**. The new trace should appear within a few seconds.
## Disable or uninstall
To pause telemetry temporarily:
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
export LATITUDE_CLAUDE_CODE_ENABLED=0
```
Restart Claude Code for the change to take effect.
To remove the integration:
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
npx -y @latitude-data/claude-code-telemetry@latest uninstall
```
## Manual configuration
If you manage Claude Code settings yourself, add the telemetry command to `~/.claude/settings.json`:
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
"env": {
"LATITUDE_API_KEY": "lat_xxx",
"LATITUDE_PROJECT": "your-project-slug"
},
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "npx -y @latitude-data/claude-code-telemetry@latest",
"async": true
}
]
}
]
}
}
```
Restart Claude Code after saving.
## Captured data and privacy
Treat this as full-fidelity telemetry. Latitude receives the content needed to reconstruct Claude Code turns, including prompts, responses, tool input/output, and system context when available.
* Telemetry runs for each turn until disabled or uninstalled.
* Disable telemetry before working with sensitive material you do not want sent to Latitude.
## Custom redaction
If you want to keep telemetry enabled but mask specific span attributes before they leave your machine, set `LATITUDE_REDACT_ATTRIBUTES` in your Claude Code environment. Redaction happens locally, after the content gate and before the OTLP export.
`LATITUDE_REDACT_ATTRIBUTES` accepts a JSON array (or comma-separated list) of patterns. Each pattern can be:
* An **exact attribute name** — `"gen_ai.tool.call.arguments"`
* A **regex source string** — `"^gen_ai\\.(input|output)\\.messages$"` (anchored match)
* A **`/pattern/flags` string** — `"/^gen_ai\\.tool\\.call\\.(arguments|result)$/i"`
`LATITUDE_REDACT_MASK` sets the replacement value (default: `******`). Set it to `[]` to replace message arrays with an empty array instead of a string.
### Examples
Redact all prompt and response messages, plus tool arguments and results:
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
LATITUDE_REDACT_ATTRIBUTES='["/^gen_ai\\.(input|output)\\.messages$/", "/^gen_ai\\.tool\\.call\\.(arguments|result)$/"]' \
LATITUDE_REDACT_MASK='[]' \
claude
```
Redact only a specific custom attribute by exact name:
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
LATITUDE_REDACT_ATTRIBUTES='["user_prompt"]' \
claude
```
To persist redaction settings, add them to the `env` block in `~/.claude/settings.json`:
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
"env": {
"LATITUDE_API_KEY": "lat_xxx",
"LATITUDE_PROJECT": "your-project-slug",
"LATITUDE_REDACT_ATTRIBUTES": "[\"/^gen_ai\\\\.(input|output)\\\\.messages$/\"]",
"LATITUDE_REDACT_MASK": "[]"
}
}
```
## Troubleshooting
**No traces appear.** Fully quit and relaunch Claude Code, then run a new prompt. Confirm the API key and project slug are correct.
**Need more diagnostics.** Set `LATITUDE_DEBUG=1` before running Claude Code. The telemetry command prints diagnostic output when uploads fail.
**Wrong project receives traces.** Re-run the installer with the correct project slug, or uninstall and install again.
# Cloudflare AI Gateway
Source: https://docs.latitude.so/telemetry/frameworks/cloudflare-ai-gateway
Connect Cloudflare AI Gateway to Latitude for observability.
## Overview
[Cloudflare AI Gateway](https://developers.cloudflare.com/ai-gateway/) proxies requests to your
LLM providers and can export an OpenTelemetry span for every request it handles. Those spans
follow the OpenTelemetry [GenAI semantic conventions](https://opentelemetry.io/docs/specs/semconv/gen-ai/),
so Latitude ingests them directly over OTLP — no SDK or code change in your application.
You configure the exporter once in the AI Gateway dashboard, pointing it at Latitude's OTLP
endpoint. Every model call routed through the gateway then shows up in Latitude with its model,
token usage, cost, and prompt/response messages.
***
## Requirements
* A Latitude API key
* A Latitude project slug
* A Cloudflare AI Gateway with OpenTelemetry export available in its settings
***
## Setup
In the Cloudflare dashboard, go to **AI → AI Gateway**, select your gateway, and open
**Settings → OpenTelemetry**.
Click **Add Otel Destination** and fill in the dialog:
| Field | Value |
| ------------------------ | -------------------------------------- |
| **OTLP Traces Endpoint** | `https://ingest.latitude.so/v1/traces` |
| **Content Type** | `JSON` (OTLP/protobuf also works) |
Then add two **Custom Headers**:
| Header name | Value |
| -------------------- | --------------------------- |
| `x-latitude-project` | `` |
| `Authorization` | `Bearer ` |
Replace `` with a Latitude API key (Settings → API Keys) and
`` with your project's slug.
To keep the API key out of the destination config, store the full `Bearer `
value in Cloudflare [Secrets Store](https://developers.cloudflare.com/secrets-store/) and
reference it from the `Authorization` header — Cloudflare injects the secret as the entire
header value, so it must include the `Bearer` prefix.
Route a request through your gateway. The trace appears in Latitude within a few seconds.
***
## What gets captured
Latitude resolves the standard GenAI attributes AI Gateway emits:
* **Provider** — `gen_ai.provider.name`
* **Model** — `gen_ai.request.model`
* **Token usage** — `gen_ai.usage.input_tokens`, `gen_ai.usage.output_tokens`
* **Cost** — `gen_ai.usage.cost`
* **Messages** — `gen_ai.input.messages` / `gen_ai.output.messages`. AI Gateway puts the raw
request body and the upstream provider's native response in these fields; Latitude unwraps
them into a normal conversation view.
AI Gateway reports `gen_ai.operation.name` as `chat` for every request, including embeddings.
Latitude reclassifies embedding requests to `embeddings` automatically, so they're counted and
displayed correctly.
***
## Linking gateway spans to your app traces
If your application already emits OpenTelemetry traces, you can stitch the gateway span into
them by passing trace context on the request:
* `cf-aig-otel-trace-id` — a 32-character hex trace ID
* `cf-aig-otel-parent-span-id` — a 16-character hex parent span ID
The gateway span is then created as a child of the span you identify.
***
## Seeing your traces
Once connected, traces appear automatically in Latitude:
1. Open your project in the Latitude dashboard
2. Send a request through your AI Gateway
3. The request appears with its model, messages, latency, token usage, and cost
# Cloudflare Think
Source: https://docs.latitude.so/telemetry/frameworks/cloudflare-think
Connect Cloudflare Think agents to Latitude for observability.
## Overview
Cloudflare Think uses the Vercel AI SDK internally. Think owns the `streamText`
call, so add Latitude telemetry in `beforeTurn()` instead of at a model call
site.
Start with the basic setup. Add context only if you need traces to include
user, session, tags, or metadata from your app.
***
## Requirements
* A Latitude API key
* A Latitude project slug
* A Cloudflare Workers project using `@cloudflare/think`
* The `nodejs_compat` compatibility flag enabled in your Worker
* `LATITUDE_API_KEY` and `LATITUDE_PROJECT_SLUG` configured as Worker secrets or variables
***
## Basic Telemetry
This records Think model calls, tools, latency, tokens, prompts, and responses.
```bash npm theme={"theme":{"light":"github-light","dark":"github-dark"}}
npm install @latitude-data/telemetry
```
```bash pnpm theme={"theme":{"light":"github-light","dark":"github-dark"}}
pnpm add @latitude-data/telemetry
```
```bash yarn theme={"theme":{"light":"github-light","dark":"github-dark"}}
yarn add @latitude-data/telemetry
```
```bash bun theme={"theme":{"light":"github-light","dark":"github-dark"}}
bun add @latitude-data/telemetry
```
```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
import { Think, type TurnConfig } from "@cloudflare/think"
import { Latitude } from "@latitude-data/telemetry"
import { routeAgentRequest } from "agents"
import { createWorkersAI } from "workers-ai-provider"
type Env = {
AI: Ai
LATITUDE_API_KEY: string
LATITUDE_PROJECT_SLUG: string
}
const latitude = new Latitude({
apiKey: process.env.LATITUDE_API_KEY!,
project: process.env.LATITUDE_PROJECT_SLUG!,
serviceName: "cloudflare-think-agent",
})
export class MyAgent extends Think {
getModel() {
return createWorkersAI({ binding: this.env.AI })("@cf/meta/llama-4-scout-17b-16e-instruct")
}
beforeTurn(): TurnConfig {
return {
experimental_telemetry: {
isEnabled: true,
tracer: latitude.getTracer("cloudflare-think"),
functionId: "think-turn",
metadata: { framework: "cloudflare-think" },
},
}
}
async onChatResponse() {
await latitude.flush()
}
onChatError(error: unknown) {
this.ctx.waitUntil(latitude.flush())
return error
}
}
export default {
async fetch(request: Request, env: Env) {
const response =
(await routeAgentRequest(request, env)) ??
new Response("Not found", { status: 404 })
return response
},
} satisfies ExportedHandler
```
If your Worker exposes secrets through `process.env`, this is all you need.
Most Workers receive secrets through `env` bindings instead. In that case,
keep one `Latitude` instance per Worker isolate:
```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
let latitude: Latitude | undefined
function getLatitude(env: Env) {
latitude ??= new Latitude({
apiKey: env.LATITUDE_API_KEY,
project: env.LATITUDE_PROJECT_SLUG,
serviceName: "cloudflare-think-agent",
})
return latitude
}
```
Then use `getLatitude(this.env).getTracer("cloudflare-think")` in
`beforeTurn()` and `getLatitude(this.env).flush()` in `onChatResponse()` /
`onChatError()`. Do not create `new Latitude()` inside `beforeTurn()`.
***
## Add App Context
If your app uses Think's default WebSocket chat entrypoint, pass the same context
you would normally pass to `capture()` through `useAgentChat({ body })`.
```tsx theme={"theme":{"light":"github-light","dark":"github-dark"}}
import { useAgentChat } from "@cloudflare/think/react"
const { messages, sendMessage } = useAgentChat({
agent,
body: {
userId: currentUser.id,
sessionId: session.id,
tags: ["cloudflare-think"],
metadata: { plan: currentUser.plan },
},
})
```
Then read that context in `beforeTurn()` and pass it to `getTracer()`:
```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
import { Think, type TurnConfig, type TurnContext } from "@cloudflare/think"
import type { ContextOptions } from "@latitude-data/telemetry"
export class MyAgent extends Think {
beforeTurn(ctx: TurnContext): TurnConfig {
const context = (ctx.body ?? {}) as ContextOptions
return {
experimental_telemetry: {
isEnabled: true,
tracer: latitude.getTracer("cloudflare-think", context),
functionId: "think-turn",
},
}
}
}
```
This is the recommended setup for most Think apps.
***
## Codemode Internal Tools
Requires `@latitude-data/telemetry` 3.6.0 or newer.
Think's codemode `execute` tool appears as a normal AI SDK tool span. Tools
called from inside codemode run outside the active AI SDK tool-call context, so
wrap both the internal tool set and the outer `execute` tool with
`createCodemodeTelemetry()`.
The helper records each internal codemode tool as an `ai.toolCall ` child
span under `execute`, stamps the same Latitude context as the turn, records AI
SDK and GenAI tool attributes, and marks failed tools with exception details.
When concurrent `execute` calls cannot be correlated after crossing the codemode
sandbox boundary, ambiguous internal spans are omitted instead of attaching them
to the wrong trace.
```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
import { Think, type TurnConfig, type TurnContext } from "@cloudflare/think"
import { createExecuteTool } from "@cloudflare/think/tools/execute"
import type { ContextOptions } from "@latitude-data/telemetry"
import { createCodemodeTelemetry } from "@latitude-data/telemetry/cloudflare"
import { tool } from "ai"
import { z } from "zod"
const travelTools = {
getWeather: tool({
description: "Get the current weather for a city.",
inputSchema: z.object({ city: z.string() }),
execute: async ({ city }) => ({ city, temperatureC: 21, conditions: "sunny" }),
}),
estimateTripBudget: tool({
description: "Estimate a trip budget for a city.",
inputSchema: z.object({
city: z.string(),
days: z.number().int().positive(),
travelers: z.number().int().positive().default(1),
}),
execute: async ({ city, days, travelers }) => ({ city, estimatedEur: days * travelers * 95 }),
}),
}
export class MyAgent extends Think {
private latitudeContext: ContextOptions | undefined
getTools() {
const codemode = createCodemodeTelemetry({
latitude: getLatitude(this.env),
scope: "cloudflare-think-codemode",
context: () => this.latitudeContext,
})
return {
execute: codemode.wrapExecuteTool(
createExecuteTool(this, {
tools: codemode.traceToolSet(travelTools),
}),
),
}
}
beforeTurn(ctx: TurnContext): TurnConfig {
this.latitudeContext = (ctx.body ?? {}) as ContextOptions
return {
experimental_telemetry: {
isEnabled: true,
tracer: getLatitude(this.env).getTracer("cloudflare-think", this.latitudeContext),
functionId: "think-turn",
},
}
}
}
```
This produces one trace waterfall with the codemode internals nested under the
outer tool call:
```text theme={"theme":{"light":"github-light","dark":"github-dark"}}
ai.streamText
ai.toolCall execute
ai.toolCall getWeather
ai.toolCall estimateTripBudget
```
By default, tool inputs and outputs are captured as span attributes. Disable
capture or redact values if codemode tools handle sensitive data:
```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
const codemode = createCodemodeTelemetry({
latitude: getLatitude(this.env),
context: () => this.latitudeContext,
capture: { inputs: true, outputs: false },
redact: (value, info) => (info.toolName === "lookupCustomer" ? "[redacted]" : value),
})
```
***
## Programmatic Turns
If your app starts Think turns with your own `runTurn()` call, wrap that call
with `capture()`.
```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
import { capture } from "@latitude-data/telemetry"
export class MyAgent extends Think {
async runSupportTurn(input: string, userId: string, sessionId: string) {
return capture(
"cloudflare-think-turn",
() => this.runTurn({ input }),
{
userId,
sessionId,
tags: ["cloudflare-think"],
metadata: { framework: "cloudflare-think" },
},
)
}
}
```
Keep `getTracer()` in `beforeTurn()`:
```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
beforeTurn(): TurnConfig {
return {
experimental_telemetry: {
isEnabled: true,
tracer: latitude.getTracer("cloudflare-think"),
functionId: "think-turn",
},
}
}
```
Do not use `capture.start()` / `scope.end()` on Cloudflare Workers. Use
`capture()` as a callback wrapper instead.
***
## If You Cannot Wrap The Turn
If you cannot wrap the turn, pass context directly to the tracer from
`beforeTurn()`. The context can come from agent state, Durable Object storage,
auth state, or any place your agent can read during the turn.
```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
async beforeTurn(): Promise {
const userId = await this.ctx.storage.get("userId")
const sessionId = await this.ctx.storage.get("sessionId")
return {
experimental_telemetry: {
isEnabled: true,
tracer: latitude.getTracer("cloudflare-think", {
userId,
sessionId,
tags: ["cloudflare-think"],
}),
functionId: "think-turn",
},
}
}
```
This records the model and tool spans with the right context. It does not add a
separate parent `cloudflare-think-turn` span.
***
## Runnable Example
The Latitude repository includes a runnable Think example at
[`examples/cloudflare-think-app`](https://github.com/latitude-dev/latitude-llm/tree/development/packages/telemetry/typescript/examples/cloudflare-think-app).
It includes an `execute` codemode tool backed by several demo tools, a small QA
page, and a local verifier that checks model spans, codemode tool spans,
`userId`, and `sessionId` against local Latitude.
***
## Seeing Your Traces
Once connected, traces appear automatically in Latitude:
1. Open your project in the Latitude dashboard
2. Send a message to your Think agent
3. The turn appears with model calls, tool calls, messages, latency, token usage, and errors
# CrewAI
Source: https://docs.latitude.so/telemetry/frameworks/crewai
Connect your CrewAI multi-agent application to Latitude for observability.
## Overview
This guide shows you how to integrate **Latitude Telemetry** into an application built with **CrewAI** (`crewai`).
Latitude includes dedicated instrumentation for CrewAI, so crew kickoffs, agent steps, model generations, and tool calls appear as traces.
You'll keep building crews exactly as you do today. Telemetry observes agent
steps, model calls, and tool calls as they happen.
CrewAI instrumentation is available in the **Python** SDK only.
***
## Requirements
* A **Latitude account** and **API key**
* A **Latitude project slug**
* A project that uses **CrewAI** (`crewai`)
* A model provider key — CrewAI uses **OpenAI** by default (`OPENAI_API_KEY`)
***
## Steps
```bash pip theme={"theme":{"light":"github-light","dark":"github-dark"}}
pip install latitude-telemetry crewai
```
```bash uv theme={"theme":{"light":"github-light","dark":"github-dark"}}
uv add latitude-telemetry crewai
```
```bash poetry theme={"theme":{"light":"github-light","dark":"github-dark"}}
poetry add latitude-telemetry crewai
```
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
import crewai
from crewai import Agent, Crew, Task
from latitude_telemetry import Latitude, capture
latitude = Latitude(
api_key="your-api-key",
project="your-project-slug",
instrumentations={"crewai": crewai},
)
def crew_run():
researcher = Agent(
role="Researcher",
goal="Summarize topics concisely",
backstory="You provide brief, accurate summaries.",
llm="gpt-4o-mini",
)
task = Task(
description="Explain what OpenTelemetry is in one sentence.",
expected_output="A single sentence.",
agent=researcher,
)
return Crew(agents=[researcher], tasks=[task]).kickoff().raw
capture("crew-run", crew_run)
latitude.shutdown()
```
***
## What you get
Each crew kickoff shows up as a trace with nested spans:
* **Crew / agent spans** — agent roles, goals, and configured tools
* **Generation spans** — model, input/output messages, and token usage
* **Tool spans** — tool calls with input arguments and output
Wrap a request or job with `capture()` to attach a `user_id`, `session_id`, `tags`, or `metadata` to every span produced inside.
***
## Seeing Your Traces
Once connected, traces appear automatically in Latitude:
1. Open your **project** in the Latitude dashboard
2. Each crew run shows the full hierarchy of agent → generation → tool calls
3. Token usage and latency are aggregated at every level
# DSPy
Source: https://docs.latitude.so/telemetry/frameworks/dspy
Connect your DSPy program to Latitude for observability.
## Overview
This guide shows you how to integrate **Latitude Telemetry** into a program built with **DSPy**.
DSPy has no dedicated instrumentor — it routes every language-model call through **LiteLLM**. Instrumenting LiteLLM therefore captures all of DSPy's model calls, including those issued by modules like `Predict` and `ReAct`.
You'll keep writing DSPy modules exactly as you do today. Telemetry observes
the model calls DSPy makes under the hood.
DSPy instrumentation is available in the **Python** SDK only, via the LiteLLM
integration.
***
## Requirements
* A **Latitude account** and **API key**
* A **Latitude project slug**
* A project that uses **DSPy** (`dspy`, which depends on `litellm`)
* A key for whichever provider your `dspy.LM` targets (e.g. `OPENAI_API_KEY`)
***
## Steps
```bash pip theme={"theme":{"light":"github-light","dark":"github-dark"}}
pip install latitude-telemetry dspy litellm
```
```bash uv theme={"theme":{"light":"github-light","dark":"github-dark"}}
uv add latitude-telemetry dspy litellm
```
```bash poetry theme={"theme":{"light":"github-light","dark":"github-dark"}}
poetry add latitude-telemetry dspy litellm
```
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
import dspy
import litellm
from latitude_telemetry import Latitude, capture
# DSPy routes every LM call through litellm, so instrumenting litellm
# captures DSPy's model calls.
latitude = Latitude(
api_key="your-api-key",
project="your-project-slug",
instrumentations={"litellm": litellm},
)
dspy.configure(lm=dspy.LM("openai/gpt-4o-mini"))
def dspy_qa():
qa = dspy.Predict("question -> answer")
return qa(question="Hello").answer
capture("dspy-qa", dspy_qa)
latitude.shutdown()
```
***
## Seeing Your Traces
Once connected, traces appear automatically in Latitude:
1. Open your **project** in the Latitude dashboard
2. Each execution shows input/output messages, model, token usage, latency, and errors
3. Wrap a request with `capture()` to group every model call DSPy makes into one trace
# ElevenLabs Agents
Source: https://docs.latitude.so/telemetry/frameworks/elevenlabs
Connect your ElevenLabs voice agents to Latitude for LLM observability.
## Overview
This guide shows you how to get LLM observability for [**ElevenLabs Agents**](https://elevenlabs.io/docs/eleven-agents/overview) in Latitude.
ElevenLabs Agents is a fully hosted **voice platform**: speech-to-text, the LLM loop, and text-to-speech all run on ElevenLabs' infrastructure. ElevenLabs does not export STT or TTS traces to third parties — only the LLM step is observable, and only when you route it through infrastructure you control.
The way in is ElevenLabs' [**Custom LLM**](https://elevenlabs.io/docs/eleven-agents/customization/llm/custom-llm) feature: point your agent at an OpenAI-compatible server **you** run, and instrument that server with Latitude. Your server receives the real LLM traffic — system prompt, full conversation history, and tool definitions — so Latitude captures every turn with actual prompts, completions, token usage, and latency.
Your server is a thin streaming proxy in front of any OpenAI-compatible
provider. The agent keeps running on ElevenLabs exactly as before — only the
LLM calls route through code you can observe.
Using ElevenLabs only as the **TTS/STT plugin inside LiveKit Agents**? You
don't need this guide — instrument the LiveKit side instead. See
[LiveKit Agents](/telemetry/frameworks/livekit).
***
## Requirements
* A **Latitude account** and **API key**
* A **Latitude project slug**
* An **ElevenLabs agent** and an API key for an OpenAI-compatible LLM provider
* A **publicly reachable URL** for your server (use a tunnel like ngrok during development)
***
## Steps
```bash pip theme={"theme":{"light":"github-light","dark":"github-dark"}}
pip install latitude-telemetry openai fastapi uvicorn
```
```bash uv theme={"theme":{"light":"github-light","dark":"github-dark"}}
uv add latitude-telemetry openai fastapi uvicorn
```
```bash poetry theme={"theme":{"light":"github-light","dark":"github-dark"}}
poetry add latitude-telemetry openai fastapi uvicorn
```
```bash npm theme={"theme":{"light":"github-light","dark":"github-dark"}}
npm install @latitude-data/telemetry openai express
```
```bash pnpm theme={"theme":{"light":"github-light","dark":"github-dark"}}
pnpm add @latitude-data/telemetry openai express
```
```bash yarn theme={"theme":{"light":"github-light","dark":"github-dark"}}
yarn add @latitude-data/telemetry openai express
```
Expose a `/v1/chat/completions` endpoint that forwards requests to your LLM provider and streams the response back as Server-Sent Events. Initializing `Latitude` with the OpenAI instrumentation is all it takes for every forwarded call to be traced.
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
import os
import openai
from fastapi import FastAPI, Request
from fastapi.responses import StreamingResponse
from openai import AsyncOpenAI
from latitude_telemetry import Latitude
latitude = Latitude(
api_key=os.environ["LATITUDE_API_KEY"],
project=os.environ["LATITUDE_PROJECT_SLUG"],
instrumentations={"openai": openai},
)
app = FastAPI()
client = AsyncOpenAI()
@app.post("/v1/chat/completions")
async def chat_completions(request: Request):
body = await request.json()
body.pop("elevenlabs_extra_body", None)
body["stream"] = True
async def stream():
response = await client.chat.completions.create(**body)
async for chunk in response:
yield f"data: {chunk.model_dump_json()}\n\n"
yield "data: [DONE]\n\n"
return StreamingResponse(stream(), media_type="text/event-stream")
```
Run it with `uvicorn server:app --port 8013`.
```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
import express from "express"
import OpenAI from "openai"
import { createOpenAIInstrumentation } from "@latitude-data/telemetry/instrumentations/openai"
import { Latitude } from "@latitude-data/telemetry"
const latitude = new Latitude({
apiKey: process.env.LATITUDE_API_KEY!,
project: process.env.LATITUDE_PROJECT_SLUG!,
instrumentations: [createOpenAIInstrumentation(OpenAI)],
})
await latitude.ready
const app = express()
app.use(express.json())
const client = new OpenAI()
app.post("/v1/chat/completions", async (req, res) => {
const { elevenlabs_extra_body: _extra, ...body } = req.body
res.setHeader("Content-Type", "text/event-stream")
const stream = await client.chat.completions.create({ ...body, stream: true })
for await (const chunk of stream) {
res.write(`data: ${JSON.stringify(chunk)}\n\n`)
}
res.write("data: [DONE]\n\n")
res.end()
})
app.listen(8013)
```
ElevenLabs requires streaming responses (`Content-Type: text/event-stream`),
and your provider must support OpenAI-style **function calling** if your agent
uses tools — ElevenLabs system tools (`end_call`, `transfer_to_agent`, etc.)
arrive in the standard `tools` parameter.
In the [ElevenLabs dashboard](https://elevenlabs.io/app/agents), open your agent's settings:
1. In the **LLM** dropdown, select **Custom LLM**
2. Enter your **Server URL** (e.g. `https://your-server.example.com/v1`) and the **Model ID** your provider expects
3. Under **API key**, create a secret with your LLM provider's key — ElevenLabs forwards it as the `Authorization` header
4. **Publish** the agent
Start a conversation with your agent — each turn now flows through your instrumented server.
***
## STT → LLM → TTS
ElevenLabs Agents runs the full voice loop on its own infrastructure:
```mermaid theme={"theme":{"light":"github-light","dark":"github-dark"}}
sequenceDiagram
participant User
participant ElevenLabs as ElevenLabs Agents
participant STT as ElevenLabs STT
participant LLM as Custom LLM proxy
participant TTS as ElevenLabs TTS
User->>ElevenLabs: audio
ElevenLabs->>STT: transcribe
STT-->>ElevenLabs: transcript
ElevenLabs->>LLM: chat/completions
LLM-->>ElevenLabs: reply text
ElevenLabs->>TTS: synthesize
TTS-->>ElevenLabs: audio
ElevenLabs-->>User: audio
```
### What gets traced
| Stage | Visible in Latitude | How |
| ------- | ------------------- | --------------------------------------- |
| **STT** | No | Runs on ElevenLabs — not exported |
| **LLM** | Yes | Via your Custom LLM proxy (steps below) |
| **TTS** | No | Runs on ElevenLabs — not exported |
Latitude only sees the **LLM step** — the `/v1/chat/completions` calls ElevenLabs forwards to your instrumented server. STT and TTS stay inside ElevenLabs and never reach your proxy.
For full **STT → LLM → TTS** tracing in Latitude, use [LiveKit Agents](/telemetry/frameworks/livekit) (native spans for all three stages) or a self-hosted [Vercel AI SDK v7](/telemetry/frameworks/vercel-ai-sdk-v7) pipeline with manual STT/TTS spans.
Using ElevenLabs only as the **TTS/STT plugin inside LiveKit Agents**? Instrument
[LiveKit](/telemetry/frameworks/livekit) instead — LiveKit exports STT and TTS spans
when the smart filter is disabled.
***
## What you get
Because your server receives the exact requests ElevenLabs builds for the model, Latitude shows each conversation turn as a real LLM call:
* **Input messages** — the agent's system prompt and the full conversation history so far
* **Output messages** — the assistant response text and any tool calls the model emitted
* **Tool definitions** — your agent tools and ElevenLabs system tools, as sent in the request
* **Model, token usage, and latency** — from the provider's streamed response
***
## Grouping turns into conversations
Each agent turn is a separate LLM call, so by default turns appear as separate traces. To group them, enable **Custom LLM extra body** in your agent's LLM settings and pass identifiers (e.g. a conversation id) from your client as overrides — they arrive on the request as `elevenlabs_extra_body`. Use them with `capture()` to set `session_id` and `user_id` on the spans; see the [Python SDK](/telemetry/python) or [TypeScript SDK](/telemetry/typescript) guides.
When using `capture()` with streaming, consume the entire stream inside the
`capture()` callback so the full LLM call stays within the active context.
***
## Seeing Your Traces
Once connected, traces appear automatically in Latitude:
1. Open your **project** in the Latitude dashboard
2. Each agent turn shows the LLM call with its input/output conversation
3. Token usage and latency are aggregated at every level
ElevenLabs-managed LLMs (where you don't bring your own endpoint) cannot be
traced this way — the calls never leave ElevenLabs' infrastructure. For those
agents, conversation transcripts are available via ElevenLabs'
[post-call webhooks](https://elevenlabs.io/docs/eleven-agents/workflows/post-call-webhooks)
and Conversations API.
# Eve
Source: https://docs.latitude.so/telemetry/frameworks/eve
Connect your Eve-powered agent to Latitude for observability.
## Overview
This guide shows you how to send traces from an agent built with **[Eve](https://eve.dev)** to Latitude.
Eve is built on the **Vercel AI SDK** and exports standard OpenTelemetry spans (`ai.streamText`, `ai.toolCall`, …) through whatever exporter you register in `agent/instrumentation.ts`. Because Latitude already understands Vercel AI SDK spans, you can point Eve's OTel exporter straight at Latitude's OTLP endpoint — no Latitude SDK required.
You'll keep building with Eve exactly as you do today. The exporter simply
sends your traces to Latitude alongside any other observability backend.
The Eve integration is **TypeScript only**.
***
## Requirements
* A **Latitude account** and **API key**
* A **Latitude project slug**
* An **Eve** project (with an `agent/instrumentation.ts` file)
***
## Steps
```bash npm theme={"theme":{"light":"github-light","dark":"github-dark"}}
npm install @vercel/otel @opentelemetry/exporter-trace-otlp-http
```
```bash pnpm theme={"theme":{"light":"github-light","dark":"github-dark"}}
pnpm add @vercel/otel @opentelemetry/exporter-trace-otlp-http
```
```bash yarn theme={"theme":{"light":"github-light","dark":"github-dark"}}
yarn add @vercel/otel @opentelemetry/exporter-trace-otlp-http
```
```bash bun theme={"theme":{"light":"github-light","dark":"github-dark"}}
bun add @vercel/otel @opentelemetry/exporter-trace-otlp-http
```
In `agent/instrumentation.ts`, register an OTLP exporter that targets Latitude's ingestion endpoint. Eve auto-discovers this file and runs it at startup, which implicitly enables telemetry.
```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
import { defineInstrumentation } from "eve/instrumentation"
import { registerOTel } from "@vercel/otel"
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http"
export default defineInstrumentation({
setup: ({ agentName }) =>
registerOTel({
serviceName: agentName,
traceExporter: new OTLPTraceExporter({
url: "https://ingest.latitude.so/v1/traces",
headers: {
Authorization: `Bearer ${process.env.LATITUDE_API_KEY!}`,
"X-Latitude-Project": process.env.LATITUDE_PROJECT_SLUG!,
},
}),
}),
})
```
Eve records full message history and model outputs on spans by default
(`recordInputs` / `recordOutputs`). Set them to `false` for sensitive or
regulated data.
Eve tags every turn with `eve.session.id`, which Latitude reads automatically to group related traces into a session — no extra work needed.
To also associate traces with an end user, return a `user.id` attribute from the `step.started` event so it lands on the spans:
```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
export default defineInstrumentation({
setup: ({ agentName }) => registerOTel({ /* … */ }),
events: {
"step.started"(input) {
return {
runtimeContext: {
"user.id": input.channel.metadata.triggeringUserId ?? "",
},
}
},
},
})
```
***
## Seeing Your Traces
Once connected, traces appear automatically in Latitude:
1. Open your **project** in the Latitude dashboard
2. Each turn shows input/output messages, model, token usage, latency, and errors
3. Eve turns, model calls, and tool executions appear as nested spans, grouped by session
# Flue
Source: https://docs.latitude.so/telemetry/frameworks/flue
Connect your Flue workflows and agents to Latitude for observability.
## Overview
This guide shows you how to send traces from **[Flue](https://flueframework.com)** to Latitude.
Flue exposes an OpenTelemetry observer through `@flue/opentelemetry`. The observer converts Flue workflow runs, operations, model turns, tool calls, delegated tasks, compactions, and logs into standard OpenTelemetry spans. Latitude ingests those spans directly and understands Flue's `flue.*` attributes and OpenTelemetry GenAI `gen_ai.*` model metadata.
You'll keep building Flue workflows exactly as you do today. Latitude only
adds an OpenTelemetry export path for the spans Flue already emits.
Flue support is **TypeScript only**.
***
## Requirements
* A **Latitude account** and **API key**
* A **Latitude project slug**
* A Flue project that uses `@flue/runtime`
***
## Steps
Install Latitude Telemetry and Flue's OpenTelemetry observer:
```bash npm theme={"theme":{"light":"github-light","dark":"github-dark"}}
npm install @latitude-data/telemetry @flue/opentelemetry @opentelemetry/api
```
```bash pnpm theme={"theme":{"light":"github-light","dark":"github-dark"}}
pnpm add @latitude-data/telemetry @flue/opentelemetry @opentelemetry/api
```
```bash yarn theme={"theme":{"light":"github-light","dark":"github-dark"}}
yarn add @latitude-data/telemetry @flue/opentelemetry @opentelemetry/api
```
```bash bun theme={"theme":{"light":"github-light","dark":"github-dark"}}
bun add @latitude-data/telemetry @flue/opentelemetry @opentelemetry/api
```
Initialize Latitude once in your application entrypoint, before the first Flue workflow or operation runs. You do not need a provider-specific `instrumentations` entry for Flue, because Flue emits OpenTelemetry spans itself.
```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
import { Latitude } from "@latitude-data/telemetry"
export const latitude = new Latitude({
apiKey: process.env.LATITUDE_API_KEY!,
project: process.env.LATITUDE_PROJECT_SLUG!,
serviceName: "flue-app",
})
await latitude.ready
```
If your app already initializes Sentry, Datadog, New Relic, Honeycomb, or a
custom OpenTelemetry SDK, initialize that first and then construct
`new Latitude(...)`. Latitude will attach its span processor to the existing
provider when possible.
Register Flue's OpenTelemetry observer once, after the OpenTelemetry SDK is configured by Latitude:
```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
import { createOpenTelemetryObserver } from "@flue/opentelemetry"
import { observe } from "@flue/runtime"
import { latitude } from "./latitude"
observe(createOpenTelemetryObserver())
```
Flue model-turn spans include `gen_ai.*` attributes for model, provider, token usage, and finish metadata. Flue workflow, operation, tool, task, compaction, and log spans include `flue.*` correlation attributes so the full hierarchy appears in Latitude.
Wrap the request, job, or agent-turn boundary with `capture()` when you have user, session, tag, or metadata context to attach to every Flue span created inside the callback.
```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
import { capture } from "@latitude-data/telemetry"
await capture(
"flue-support-workflow",
async () => {
return runSupportWorkflow({ message })
},
{
userId: user.id,
sessionId: conversation.id,
tags: ["flue", "support"],
metadata: { workflowName: "support" },
},
)
```
***
## Export content safely
Flue's OpenTelemetry observer omits sensitive content by default. Workflow payloads and results, detailed errors, model input and output, tool values, task content, and log content are not exported unless you explicitly opt in with `exportContent()`.
Use `exportContent()` only after sanitizing the fields you want Latitude to receive:
```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
import { createOpenTelemetryObserver } from "@flue/opentelemetry";
import { observe } from "@flue/runtime";
const redactLogMessage = (message: string) =>
message.replace(/[\w.-]+@[\w.-]+/g, "[REDACTED_EMAIL]");
const redactLogAttributes = (attributes: Record | undefined) =>
Object.fromEntries(
Object.entries(attributes ?? {}).filter(
([key]) => !key.toLowerCase().includes("secret"),
),
);
observe(
createOpenTelemetryObserver({
exportContent(event) {
if (event.type !== "log") return undefined;
return {
...event,
message: redactLogMessage(event.message),
attributes: redactLogAttributes(event.attributes),
};
},
}),
);
```
Returning the original event from `exportContent()` can send prompts,
completions, tool arguments, tool results, logs, and application-owned values
to Latitude. Only enable content export for data your organization is allowed
to store in your observability backend.
***
## Cloudflare and generic OpenTelemetry runtimes
For runtimes where the TypeScript SDK's Node OpenTelemetry stack is not appropriate, configure your runtime's OTLP HTTP exporter directly and still register Flue's observer:
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
export OTEL_EXPORTER_OTLP_TRACES_ENDPOINT="https://ingest.latitude.so/v1/traces"
export OTEL_EXPORTER_OTLP_TRACES_HEADERS="Authorization=Bearer YOUR_API_KEY,X-Latitude-Project=YOUR_PROJECT_SLUG"
```
```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
import { createOpenTelemetryObserver } from "@flue/opentelemetry";
import { observe } from "@flue/runtime";
observe(createOpenTelemetryObserver());
```
Make sure your OpenTelemetry SDK, exporter, sampling, and shutdown or flush behavior are compatible with the deployed runtime.
***
## Seeing Your Traces
Once connected, traces appear automatically in Latitude:
1. Open your **project** in the Latitude dashboard
2. Run a Flue workflow with at least one model turn or tool call
3. Confirm the workflow, operation, model turn, tool, task, and compaction spans appear with Flue correlation attributes, model metadata, token usage, latency, and errors
# Google ADK
Source: https://docs.latitude.so/telemetry/frameworks/google-adk
Connect your Google Agent Development Kit (ADK) application to Latitude for observability.
## Overview
This guide shows you how to integrate **Latitude Telemetry** into an application that uses the **Google Agent Development Kit (ADK)** (`google-adk` for Python).
Latitude includes dedicated instrumentation for Google ADK, so agent runs, model generations, and tool calls appear as traces.
You'll keep calling Google ADK exactly as you do today. Telemetry observes
agent runs, model calls, and tool calls as they happen.
Google ADK instrumentation is available in the **Python** SDK only.
***
## Requirements
* A **Latitude account** and **API key**
* A **Latitude project slug**
* A project that uses **Google ADK** (`google-adk`)
* A **Gemini API key** (set as `GOOGLE_API_KEY`)
***
## Steps
```bash pip theme={"theme":{"light":"github-light","dark":"github-dark"}}
pip install latitude-telemetry google-adk
```
```bash uv theme={"theme":{"light":"github-light","dark":"github-dark"}}
uv add latitude-telemetry google-adk
```
```bash poetry theme={"theme":{"light":"github-light","dark":"github-dark"}}
poetry add latitude-telemetry google-adk
```
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
import asyncio
import google.adk
from google.adk.agents import Agent
from google.adk.runners import InMemoryRunner
from google.genai import types
from latitude_telemetry import Latitude, capture
latitude = Latitude(
api_key="your-api-key",
project="your-project-slug",
instrumentations={"google_adk": google.adk},
)
def get_weather(city: str) -> dict:
"""Returns the current weather for a city."""
return {"status": "success", "report": f"The weather in {city} is sunny."}
agent = Agent(
name="weather_agent",
model="gemini-2.5-flash",
description="Agent that answers weather questions using tools.",
instruction="Answer weather questions using get_weather.",
tools=[get_weather],
)
async def weather_agent_run():
runner = InMemoryRunner(agent=agent, app_name="weather_app")
await runner.session_service.create_session(
app_name="weather_app",
user_id="user_123",
session_id="session_abc",
)
async for event in runner.run_async(
user_id="user_123",
session_id="session_abc",
new_message=types.Content(
role="user",
parts=[types.Part(text="What's the weather in Barcelona?")],
),
):
if event.is_final_response() and event.content and event.content.parts:
return event.content.parts[0].text
capture("weather-agent-run", lambda: asyncio.run(weather_agent_run()))
latitude.shutdown()
```
***
## What you get
Each agent run shows up as a trace with nested spans:
* **Agent spans** — agent name, instructions, and configured tools
* **Generation spans** — model, input/output messages, and token usage
* **Tool spans** — tool calls with input arguments and output
Wrap a request or job with `capture()` to attach a `user_id`, `session_id`, `tags`, or `metadata` to every span produced inside.
***
## Seeing Your Traces
Once connected, traces appear automatically in Latitude:
1. Open your **project** in the Latitude dashboard
2. Each agent run shows the full hierarchy of agent → generation → tool calls
3. Token usage and latency are aggregated at every level
# Haystack
Source: https://docs.latitude.so/telemetry/frameworks/haystack
Connect your Haystack application to Latitude for observability.
## Overview
This guide shows you how to integrate **Latitude Telemetry** into an application built with **Haystack** (`haystack-ai`).
Latitude includes dedicated instrumentation for Haystack, so pipeline runs, generator calls, and tool invocations appear as traces.
You'll keep building pipelines exactly as you do today. Telemetry observes
components, model calls, and tool calls as they happen.
Haystack instrumentation is available in the **Python** SDK only.
***
## Requirements
* A **Latitude account** and **API key**
* A **Latitude project slug**
* A project that uses **Haystack** (`haystack-ai`)
* A model provider key — the example below uses **OpenAI** (`OPENAI_API_KEY`)
***
## Steps
```bash pip theme={"theme":{"light":"github-light","dark":"github-dark"}}
pip install latitude-telemetry haystack-ai
```
```bash uv theme={"theme":{"light":"github-light","dark":"github-dark"}}
uv add latitude-telemetry haystack-ai
```
```bash poetry theme={"theme":{"light":"github-light","dark":"github-dark"}}
poetry add latitude-telemetry haystack-ai
```
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
import haystack
from haystack.components.generators.chat import OpenAIChatGenerator
from haystack.dataclasses import ChatMessage
from latitude_telemetry import Latitude, capture
latitude = Latitude(
api_key="your-api-key",
project="your-project-slug",
instrumentations={"haystack": haystack},
)
def chat():
generator = OpenAIChatGenerator(model="gpt-4o-mini")
result = generator.run(messages=[ChatMessage.from_user("Hello")])
return result["replies"][0].text
capture("haystack-chat", chat)
latitude.shutdown()
```
***
## What you get
Each pipeline run shows up as a trace with nested spans:
* **Pipeline / component spans** — the components that ran and their configuration
* **Generation spans** — model, input/output messages, and token usage
* **Tool spans** — tool calls with input arguments and output
Wrap a request or job with `capture()` to attach a `user_id`, `session_id`, `tags`, or `metadata` to every span produced inside.
***
## Seeing Your Traces
Once connected, traces appear automatically in Latitude:
1. Open your **project** in the Latitude dashboard
2. Each run shows the full hierarchy of component → generation → tool calls
3. Token usage and latency are aggregated at every level
# LangChain
Source: https://docs.latitude.so/telemetry/frameworks/langchain
Connect your LangChain-powered application to Latitude for observability.
## Overview
This guide shows you how to integrate **Latitude Telemetry** into an application that uses **LangChain**.
You'll keep calling LangChain exactly as you do today. Telemetry simply
observes and enriches those calls.
***
## Requirements
* A **Latitude account** and **API key**
* A **Latitude project slug**
* A project that uses **LangChain**
***
## Steps
```bash npm theme={"theme":{"light":"github-light","dark":"github-dark"}}
npm install @latitude-data/telemetry @langchain/openai @langchain/core
```
```bash pnpm theme={"theme":{"light":"github-light","dark":"github-dark"}}
pnpm add @latitude-data/telemetry @langchain/openai @langchain/core
```
```bash yarn theme={"theme":{"light":"github-light","dark":"github-dark"}}
yarn add @latitude-data/telemetry @langchain/openai @langchain/core
```
```bash bun theme={"theme":{"light":"github-light","dark":"github-dark"}}
bun add @latitude-data/telemetry @langchain/openai @langchain/core
```
```bash pip theme={"theme":{"light":"github-light","dark":"github-dark"}}
pip install latitude-telemetry
```
```bash uv theme={"theme":{"light":"github-light","dark":"github-dark"}}
uv add latitude-telemetry
```
```bash poetry theme={"theme":{"light":"github-light","dark":"github-dark"}}
poetry add latitude-telemetry
```
```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
import { createLangChainInstrumentation } from "@latitude-data/telemetry/instrumentations/langchain"
import { Latitude, capture } from "@latitude-data/telemetry"
import * as CallbackManagerModule from "@langchain/core/callbacks/manager"
import { HumanMessage } from "@langchain/core/messages"
import { ChatOpenAI } from "@langchain/openai"
const latitude = new Latitude({
apiKey: process.env.LATITUDE_API_KEY!,
project: process.env.LATITUDE_PROJECT_SLUG!,
instrumentations: [createLangChainInstrumentation(CallbackManagerModule)],
})
await latitude.ready
const llm = new ChatOpenAI({ modelName: "gpt-4o" })
await capture("langchain-query", async () => {
const response = await llm.invoke([new HumanMessage("Hello")])
return response.content
})
await latitude.shutdown()
```
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
from latitude_telemetry import Latitude, capture
import langchain_core
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage
latitude = Latitude(
api_key="your-api-key",
project="your-project-slug",
instrumentations={"langchain": langchain_core},
)
llm = ChatOpenAI(model="gpt-4o")
def langchain_query():
response = llm.invoke([HumanMessage(content="Hello")])
return response.content
capture("langchain-query", langchain_query)
latitude.shutdown()
```
***
## Seeing Your Traces
Once connected, traces appear automatically in Latitude:
1. Open your **project** in the Latitude dashboard
2. Each execution shows input/output messages, model, token usage, latency, and errors
3. LangChain chain steps appear as child spans
# LiteLLM
Source: https://docs.latitude.so/telemetry/frameworks/litellm
Connect your LiteLLM-powered application to Latitude for observability.
## Overview
This guide shows you how to integrate **Latitude Telemetry** into an application that uses **LiteLLM** to call models across providers behind a single API.
Latitude wires LiteLLM's built-in OpenTelemetry callback, so every `litellm.completion` call — whatever the underlying provider — is captured with messages, model, and token usage.
You'll keep calling LiteLLM exactly as you do today. Telemetry simply
observes and enriches those calls.
LiteLLM instrumentation is available in the **Python** SDK only.
***
## Requirements
* A **Latitude account** and **API key**
* A **Latitude project slug**
* A project that uses **LiteLLM** (`litellm`)
* A key for whichever provider you route to (e.g. `OPENAI_API_KEY`)
***
## Steps
```bash pip theme={"theme":{"light":"github-light","dark":"github-dark"}}
pip install latitude-telemetry litellm
```
```bash uv theme={"theme":{"light":"github-light","dark":"github-dark"}}
uv add latitude-telemetry litellm
```
```bash poetry theme={"theme":{"light":"github-light","dark":"github-dark"}}
poetry add latitude-telemetry litellm
```
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
import litellm
from latitude_telemetry import Latitude, capture
latitude = Latitude(
api_key="your-api-key",
project="your-project-slug",
instrumentations={"litellm": litellm},
)
def generate_reply():
response = litellm.completion(
model="gpt-4o-mini",
messages=[{"role": "user", "content": "Hello"}],
)
return response.choices[0].message.content
capture("generate-reply", generate_reply)
latitude.shutdown()
```
***
## Seeing Your Traces
Once connected, traces appear automatically in Latitude:
1. Open your **project** in the Latitude dashboard
2. Each execution shows input/output messages, model, token usage, latency, and errors
# LiveKit Agents
Source: https://docs.latitude.so/telemetry/frameworks/livekit
Connect your LiveKit voice AI agents to Latitude for observability.
## Overview
This guide shows you how to send traces from a [**LiveKit Agents**](https://docs.livekit.io/agents/) application to Latitude.
LiveKit Agents ships with built-in OpenTelemetry support and owns its own tracer provider. Instead of bootstrapping telemetry with the `Latitude` class, you attach a **`LatitudeSpanProcessor`** to LiveKit's provider — Latitude then receives the same spans LiveKit emits for LLM, agent, and tool activity. This works the same way for the **Python** (`livekit-agents`) and **Node.js** (`@livekit/agents`) SDKs.
You'll keep building your LiveKit agent exactly as you do today. Latitude
observes the LLM spans the framework already produces.
***
## Requirements
* A **Latitude account** and **API key**
* A **Latitude project slug**
* A project that uses **LiveKit Agents** (`livekit-agents` or `@livekit/agents`)
***
## Steps
```bash pip theme={"theme":{"light":"github-light","dark":"github-dark"}}
pip install latitude-telemetry "livekit-agents[openai]"
```
```bash uv theme={"theme":{"light":"github-light","dark":"github-dark"}}
uv add latitude-telemetry "livekit-agents[openai]"
```
```bash poetry theme={"theme":{"light":"github-light","dark":"github-dark"}}
poetry add latitude-telemetry "livekit-agents[openai]"
```
```bash npm theme={"theme":{"light":"github-light","dark":"github-dark"}}
npm install @latitude-data/telemetry @opentelemetry/sdk-trace-node
```
```bash pnpm theme={"theme":{"light":"github-light","dark":"github-dark"}}
pnpm add @latitude-data/telemetry @opentelemetry/sdk-trace-node
```
```bash yarn theme={"theme":{"light":"github-light","dark":"github-dark"}}
yarn add @latitude-data/telemetry @opentelemetry/sdk-trace-node
```
Build a tracer provider, add the `LatitudeSpanProcessor`, and hand the provider to LiveKit inside your entrypoint.
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
import os
from livekit.agents import AgentSession, JobContext
from livekit.agents.telemetry import set_tracer_provider
from opentelemetry.sdk.trace import TracerProvider
from latitude_telemetry import LatitudeSpanProcessor
def setup_latitude_telemetry():
provider = TracerProvider()
provider.add_span_processor(
LatitudeSpanProcessor(
os.environ["LATITUDE_API_KEY"],
os.environ["LATITUDE_PROJECT_SLUG"],
)
)
set_tracer_provider(provider)
async def entrypoint(ctx: JobContext):
setup_latitude_telemetry()
session = AgentSession(
# ... your STT / LLM / TTS configuration
)
# ... start your agent as usual
```
```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
import { telemetry, type JobContext } from "@livekit/agents"
import { LatitudeSpanProcessor } from "@latitude-data/telemetry"
import { NodeTracerProvider } from "@opentelemetry/sdk-trace-node"
function setupLatitudeTelemetry() {
const provider = new NodeTracerProvider({
spanProcessors: [
new LatitudeSpanProcessor(
process.env.LATITUDE_API_KEY!,
process.env.LATITUDE_PROJECT_SLUG!,
),
],
})
telemetry.setTracerProvider(provider)
}
export default async function entrypoint(ctx: JobContext) {
setupLatitudeTelemetry()
// ... build and start your AgentSession as usual
}
```
***
## STT → LLM → TTS
LiveKit Agents runs the full voice pipeline — speech-to-text, an LLM turn, and text-to-speech — inside `AgentSession`. LiveKit emits OpenTelemetry spans for each stage; Latitude receives them through `LatitudeSpanProcessor`.
```mermaid theme={"theme":{"light":"github-light","dark":"github-dark"}}
sequenceDiagram
participant User
participant LiveKit as LiveKit AgentSession
participant STT as STT plugin
participant LLM as LLM
participant TTS as TTS plugin
User->>LiveKit: audio
LiveKit->>STT: transcribe
STT-->>LiveKit: transcript
LiveKit->>LLM: generate
LLM-->>LiveKit: reply text
LiveKit->>TTS: synthesize
TTS-->>LiveKit: audio
LiveKit-->>User: audio
```
### What gets traced
| Stage | Traced by default | Content in Latitude |
| ------- | ----------------- | ------------------------------------------------------------- |
| **STT** | No (smart filter) | Enable with `disableSmartFilter` |
| **LLM** | Yes | Prompts, completions, tools, tokens via `gen_ai.*` and `lk.*` |
| **TTS** | No (smart filter) | Enable with `disableSmartFilter` |
By default, Latitude's smart filter forwards **LLM spans only**. To include STT, TTS, and VAD spans in every trace, disable the filter when creating the processor:
```python Python theme={"theme":{"light":"github-light","dark":"github-dark"}}
from latitude_telemetry import LatitudeSpanProcessor, LatitudeSpanProcessorOptions
LatitudeSpanProcessor(
api_key,
project,
LatitudeSpanProcessorOptions(disable_smart_filter=True),
)
```
```ts TypeScript theme={"theme":{"light":"github-light","dark":"github-dark"}}
new LatitudeSpanProcessor(apiKey, project, { disableSmartFilter: true })
```
LiveKit serializes conversation content in `lk.*` attributes. Latitude parses those alongside `gen_ai.*` metadata — STT transcripts in `audio_content` items are normalized to text parts.
Using ElevenLabs as the TTS/STT plugin inside LiveKit? Instrument LiveKit on
this page — not the [ElevenLabs Agents](/telemetry/frameworks/elevenlabs) guide.
***
## What you get
### LLM turns (default)
LiveKit's LLM spans carry both `gen_ai.*` metadata and the conversation content in custom `lk.*` attributes. Latitude parses both, so each LLM turn shows up with:
* **Model and provider** — from `gen_ai.request.model` / `gen_ai.provider.name`
* **Token usage and latency** — input/output tokens, time-to-first-token
* **Input messages** — the chat context (system prompt, user turns, prior tool calls and results)
* **Output messages** — the assistant response text and any tool calls the model emitted
* **Tool definitions** — the function tools available to the agent
Audio in the chat context (e.g. STT transcripts in `audio_content` items) is normalized to text parts in Latitude.
***
## Seeing Your Traces
Once connected, traces appear automatically in Latitude:
1. Open your **project** in the Latitude dashboard
2. Each agent turn shows the LLM call with its input/output conversation
3. Token usage and latency are aggregated at every level
# LlamaIndex
Source: https://docs.latitude.so/telemetry/frameworks/llamaindex
Connect your LlamaIndex-powered application to Latitude for observability.
## Overview
This guide shows you how to integrate **Latitude Telemetry** into an application that uses **LlamaIndex**.
You'll keep calling LlamaIndex exactly as you do today. Telemetry simply
observes and enriches those calls.
***
## Requirements
* A **Latitude account** and **API key**
* A **Latitude project slug**
* A project that uses **LlamaIndex**
***
## Steps
```bash npm theme={"theme":{"light":"github-light","dark":"github-dark"}}
npm install @latitude-data/telemetry llamaindex @llamaindex/openai @llamaindex/workflow
```
```bash pnpm theme={"theme":{"light":"github-light","dark":"github-dark"}}
pnpm add @latitude-data/telemetry llamaindex @llamaindex/openai @llamaindex/workflow
```
```bash yarn theme={"theme":{"light":"github-light","dark":"github-dark"}}
yarn add @latitude-data/telemetry llamaindex @llamaindex/openai @llamaindex/workflow
```
```bash bun theme={"theme":{"light":"github-light","dark":"github-dark"}}
bun add @latitude-data/telemetry llamaindex @llamaindex/openai @llamaindex/workflow
```
```bash pip theme={"theme":{"light":"github-light","dark":"github-dark"}}
pip install latitude-telemetry
```
```bash uv theme={"theme":{"light":"github-light","dark":"github-dark"}}
uv add latitude-telemetry
```
```bash poetry theme={"theme":{"light":"github-light","dark":"github-dark"}}
poetry add latitude-telemetry
```
```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
import { createLlamaIndexInstrumentation } from "@latitude-data/telemetry/instrumentations/llamaindex"
import { Latitude, capture } from "@latitude-data/telemetry"
import { Settings } from "llamaindex"
import * as LlamaIndex from "llamaindex"
import { openai } from "@llamaindex/openai"
import { agent } from "@llamaindex/workflow"
const latitude = new Latitude({
apiKey: process.env.LATITUDE_API_KEY!,
project: process.env.LATITUDE_PROJECT_SLUG!,
instrumentations: [createLlamaIndexInstrumentation(LlamaIndex)],
})
await latitude.ready
Settings.llm = openai({ model: "gpt-4o" })
const myAgent = agent({ tools: [] })
await capture("llamaindex-query", async () => {
const response = await myAgent.run("Hello")
return response
})
await latitude.shutdown()
```
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
import llama_index
from llama_index.core import VectorStoreIndex, SimpleDirectoryReader
from latitude_telemetry import Latitude, capture
latitude = Latitude(
api_key="your-api-key",
project="your-project-slug",
instrumentations={"llamaindex": llama_index},
)
documents = SimpleDirectoryReader("data").load_data()
index = VectorStoreIndex.from_documents(documents)
query_engine = index.as_query_engine()
def llamaindex_query():
response = query_engine.query("What is this document about?")
return str(response)
capture("llamaindex-query", llamaindex_query)
latitude.shutdown()
```
***
## Seeing Your Traces
Once connected, traces appear automatically in Latitude:
1. Open your **project** in the Latitude dashboard
2. Each execution shows input/output messages, model, token usage, latency, and errors
3. LlamaIndex retrieval and synthesis steps appear as child spans
# Mastra
Source: https://docs.latitude.so/telemetry/frameworks/mastra
Connect your Mastra-powered application to Latitude for observability.
## Overview
This guide shows you how to integrate **Latitude Telemetry** into an application that uses **Mastra**.
Mastra has built-in observability support via `@mastra/observability` and `@mastra/otel-exporter`. It emits standard OpenTelemetry `gen_ai.*` spans, so you can point Mastra's OTel exporter directly at Latitude's OTLP ingestion endpoint — no Latitude SDK required.
You'll keep using Mastra exactly as you do today. The exporter simply
sends your traces to Latitude alongside any other observability backend.
The Mastra integration is **TypeScript only**.
***
## Requirements
* A **Latitude account** and **API key**
* A **Latitude project slug**
* A Node.js project that uses **Mastra** (`@mastra/core`)
***
## Steps
```bash npm theme={"theme":{"light":"github-light","dark":"github-dark"}}
npm install @mastra/observability @mastra/otel-exporter @opentelemetry/exporter-trace-otlp-proto
```
```bash pnpm theme={"theme":{"light":"github-light","dark":"github-dark"}}
pnpm add @mastra/observability @mastra/otel-exporter @opentelemetry/exporter-trace-otlp-proto
```
```bash yarn theme={"theme":{"light":"github-light","dark":"github-dark"}}
yarn add @mastra/observability @mastra/otel-exporter @opentelemetry/exporter-trace-otlp-proto
```
```bash bun theme={"theme":{"light":"github-light","dark":"github-dark"}}
bun add @mastra/observability @mastra/otel-exporter @opentelemetry/exporter-trace-otlp-proto
```
Create an `OtelExporter` with a `custom` provider pointed at Latitude's OTLP endpoint, then pass it to Mastra's `Observability` config:
```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
import { Mastra } from "@mastra/core"
import { Agent } from "@mastra/core/agent"
import { Observability } from "@mastra/observability"
import { OtelExporter } from "@mastra/otel-exporter"
const otelExporter = new OtelExporter({
provider: {
custom: {
endpoint: "https://ingest.latitude.so/v1/traces",
protocol: "http/protobuf",
headers: {
Authorization: `Bearer ${process.env.LATITUDE_API_KEY!}`,
"X-Latitude-Project": process.env.LATITUDE_PROJECT_SLUG!,
},
},
},
})
const agent = new Agent({
id: "my-agent",
name: "My Agent",
model: { provider: "OPEN_AI", name: "gpt-4o" },
instructions: "You are a helpful assistant.",
})
const mastra = new Mastra({
agents: { "my-agent": agent },
observability: new Observability({
configs: {
otel: {
serviceName: "my-mastra-app",
exporters: [otelExporter],
},
},
}),
})
```
***
## Seeing Your Traces
Once connected, traces appear automatically in Latitude:
1. Open your **project** in the Latitude dashboard
2. Each execution shows input/output messages, model, token usage, latency, and errors
3. Mastra agent runs, tool executions, and workflow steps appear as child spans
# OpenAI Agents SDK
Source: https://docs.latitude.so/telemetry/frameworks/openai-agents
Connect your OpenAI Agents SDK application to Latitude for observability.
## Overview
This guide shows you how to integrate **Latitude Telemetry** into an application that uses the **OpenAI Agents SDK** (`@openai/agents` for TypeScript, `openai-agents` for Python).
Latitude includes dedicated instrumentation for the OpenAI Agents SDK, so agent runs, generations, function calls, handoffs, and guardrails appear as traces.
You'll keep calling the Agents SDK exactly as you do today. Telemetry observes
agent runs, tool calls, and handoffs as they happen.
***
## Requirements
* A **Latitude account** and **API key**
* A **Latitude project slug**
* A project that uses the **OpenAI Agents SDK**
***
## Steps
```bash npm theme={"theme":{"light":"github-light","dark":"github-dark"}}
npm install @latitude-data/telemetry @openai/agents
```
```bash pnpm theme={"theme":{"light":"github-light","dark":"github-dark"}}
pnpm add @latitude-data/telemetry @openai/agents
```
```bash yarn theme={"theme":{"light":"github-light","dark":"github-dark"}}
yarn add @latitude-data/telemetry @openai/agents
```
```bash bun theme={"theme":{"light":"github-light","dark":"github-dark"}}
bun add @latitude-data/telemetry @openai/agents
```
```bash pip theme={"theme":{"light":"github-light","dark":"github-dark"}}
pip install latitude-telemetry openai-agents
```
```bash uv theme={"theme":{"light":"github-light","dark":"github-dark"}}
uv add latitude-telemetry openai-agents
```
```bash poetry theme={"theme":{"light":"github-light","dark":"github-dark"}}
poetry add latitude-telemetry openai-agents
```
```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
import { createOpenAIAgentsInstrumentation } from "@latitude-data/telemetry/instrumentations/openai-agents"
import { Latitude, capture } from "@latitude-data/telemetry"
import { Agent, run, tool } from "@openai/agents"
import * as OpenAIAgentsSDK from "@openai/agents"
import { z } from "zod"
const latitude = new Latitude({
apiKey: process.env.LATITUDE_API_KEY!,
project: process.env.LATITUDE_PROJECT_SLUG!,
instrumentations: [createOpenAIAgentsInstrumentation(OpenAIAgentsSDK)],
})
await latitude.ready
const getWeather = tool({
name: "get_weather",
description: "Returns the current weather for a city.",
parameters: z.object({ city: z.string() }),
execute: async ({ city }) => `The weather in ${city} is sunny.`,
})
const agent = new Agent({
name: "Weather agent",
instructions: "Answer weather questions using get_weather.",
tools: [getWeather],
model: "gpt-4o-mini",
})
await capture("weather-agent-run", () =>
run(agent, "What's the weather in Barcelona?"),
)
await latitude.shutdown()
```
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
import agents
from agents import Agent, Runner
from latitude_telemetry import Latitude, capture
latitude = Latitude(
api_key="your-api-key",
project="your-project-slug",
instrumentations={"openai-agents": agents},
)
agent = Agent(
name="Weather agent",
instructions="Answer weather questions concisely.",
model="gpt-4o-mini",
)
async def weather_agent_run():
return await Runner.run(agent, "What's the weather in Barcelona?")
capture("weather-agent-run", weather_agent_run)
latitude.shutdown()
```
***
## What you get
Each agent run shows up as a trace with nested spans:
* **Agent spans** — agent name, configured tools, handoff targets, output type
* **Generation / Response spans** — model, input/output messages, token usage, response id
* **Function spans** — tool calls with input arguments and output
* **Handoff spans** — `from_agent` → `to_agent`
* **Guardrail spans** — guardrail name and whether it triggered
* **MCP spans** — listed tools per server
Wrap a request or job with `capture()` to attach a `userId`, `sessionId`, `tags`, or `metadata` to every span produced inside.
***
## Seeing Your Traces
Once connected, traces appear automatically in Latitude:
1. Open your **project** in the Latitude dashboard
2. Each agent run shows the full hierarchy of agent → generation/response → tool calls and handoffs
3. Token usage and latency are aggregated at every level
# Pydantic AI
Source: https://docs.latitude.so/telemetry/frameworks/pydantic-ai
Connect your Pydantic AI agent to Latitude for observability.
## Overview
This guide shows you how to send traces from a **[Pydantic AI](https://ai.pydantic.dev)** agent to Latitude.
Pydantic AI has OpenTelemetry support built in. Calling `Agent.instrument_all()` makes it emit standard `gen_ai.*` spans for every agent run, model call, and tool execution. Latitude's Python SDK registers the global OpenTelemetry provider those spans flow into, so you get full traces without a dedicated instrumentor — Latitude reads Pydantic AI's native spans directly.
You'll keep building Pydantic AI agents exactly as you do today. Latitude only
adds the export path for the spans Pydantic AI already emits.
The Pydantic AI integration is **Python only**.
***
## Requirements
* A **Latitude account** and **API key**
* A **Latitude project slug**
* A Python project that uses **Pydantic AI** (`pydantic-ai`)
***
## Steps
```bash pip theme={"theme":{"light":"github-light","dark":"github-dark"}}
pip install latitude-telemetry pydantic-ai
```
```bash uv theme={"theme":{"light":"github-light","dark":"github-dark"}}
uv add latitude-telemetry pydantic-ai
```
```bash poetry theme={"theme":{"light":"github-light","dark":"github-dark"}}
poetry add latitude-telemetry pydantic-ai
```
Initialize Latitude once at startup, then turn on Pydantic AI's OpenTelemetry instrumentation. You do not need an `instrumentations` entry for Pydantic AI, because it emits OpenTelemetry spans itself.
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
import os
from latitude_telemetry import Latitude, capture
from pydantic_ai import Agent
latitude = Latitude(
api_key=os.environ["LATITUDE_API_KEY"],
project=os.environ["LATITUDE_PROJECT_SLUG"],
)
# Pydantic AI self-instruments via OpenTelemetry onto the global provider
# Latitude just registered — no `instrumentations` entry required.
Agent.instrument_all()
agent = Agent("openai:gpt-4o", system_prompt="You are a helpful assistant.")
@capture("pydantic-ai-run", {"session_id": "example"})
def main():
return agent.run_sync("Summarize the key features of Pydantic AI.").output
if __name__ == "__main__":
print(main())
latitude.shutdown()
```
The SDK registers `atexit` and signal shutdown handlers automatically. The
explicit `latitude.shutdown()` is a safeguard for short-lived scripts and
notebooks, ensuring buffered spans flush before the process exits.
Wrap the boundary that runs your agent with `capture()` to attach user, session, tag, or metadata context to every span created inside the callback.
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
@capture(
"support-agent",
{
"user_id": user.id,
"session_id": conversation.id,
"tags": ["pydantic-ai", "support"],
},
)
def handle_request(message: str):
return agent.run_sync(message).output
```
***
## Bring your own OpenTelemetry
If your app already runs its own OpenTelemetry `TracerProvider`, skip the Latitude SDK and point that provider's OTLP exporter at Latitude, then still call `Agent.instrument_all()`:
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
export OTEL_EXPORTER_OTLP_ENDPOINT="https://ingest.latitude.so"
export OTEL_EXPORTER_OTLP_HEADERS="Authorization=Bearer YOUR_API_KEY,X-Latitude-Project=YOUR_PROJECT_SLUG"
```
`OTEL_EXPORTER_OTLP_ENDPOINT` is the **base URL** — the OpenTelemetry SDK
appends `/v1/traces` automatically. Do not include the path yourself or traces
will fail to reach Latitude. The full endpoint
`https://ingest.latitude.so/v1/traces` is what you use with `curl` or the
signal-specific `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT`.
On this path Latitude's smart filter does not apply — every exported span is stored — so export only the spans you want to keep. See [GenAI Span Attributes](/telemetry/otel-exporter#genai-span-attributes-llm-metadata) for the attributes Latitude expects.
***
## What you get
Each agent run shows up as a trace with nested spans:
* **Agent spans** — the agent run, its user message, and the final response
* **Model spans** — model invocations with input/output messages, model name, and token usage
* **Tool spans** — tool name, input arguments, and output result
Because Pydantic AI emits standard `gen_ai.*` attributes (OpenTelemetry GenAI semantic conventions), Latitude renders model name, token counts, and messages automatically.
***
## Seeing Your Traces
Once connected, traces appear automatically in Latitude:
1. Open your **project** in the Latitude dashboard
2. Run your agent with at least one model call or tool call
3. Confirm the agent → model → tool hierarchy appears with model metadata, token usage, and latency
# Strands Agents
Source: https://docs.latitude.so/telemetry/frameworks/strands
Connect your Strands Agents application to Latitude for observability.
## Overview
This guide shows you how to integrate **Latitude Telemetry** into an application that uses **Strands Agents** — AWS's open-source AI agents SDK.
Strands Agents has first-class OpenTelemetry support built in (`strands-agents[otel]`). It emits standard `gen_ai.*` spans for every agent run, LLM call, and tool execution, so you can point Strands' OTLP exporter directly at Latitude's ingestion endpoint — no Latitude SDK required.
You'll keep using Strands Agents exactly as you do today. The exporter simply
sends your traces to Latitude alongside any other observability backend.
The Strands Agents integration is **Python only**.
***
## Requirements
* A **Latitude account** and **API key**
* A **Latitude project slug**
* A Python project that uses **Strands Agents** (`strands-agents`)
***
## Steps
```bash pip theme={"theme":{"light":"github-light","dark":"github-dark"}}
pip install 'strands-agents[otel]'
```
```bash uv theme={"theme":{"light":"github-light","dark":"github-dark"}}
uv add 'strands-agents[otel]'
```
```bash poetry theme={"theme":{"light":"github-light","dark":"github-dark"}}
poetry add 'strands-agents[otel]'
```
Set the required environment variables so Strands sends traces to Latitude:
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
export OTEL_EXPORTER_OTLP_ENDPOINT="https://ingest.latitude.so"
export OTEL_EXPORTER_OTLP_HEADERS="Authorization=Bearer YOUR_API_KEY,X-Latitude-Project=YOUR_PROJECT_SLUG"
export OTEL_EXPORTER_OTLP_TRACES_PROTOCOL="http/protobuf"
export OTEL_SEMCONV_STABILITY_OPT_IN="gen_ai_latest_experimental"
```
`OTEL_EXPORTER_OTLP_ENDPOINT` is the **base URL** — the OTel SDK automatically appends `/v1/traces`. Do not include the path yourself or traces will fail to reach Latitude.
Then initialize telemetry before creating your agent:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
from strands import Agent
from strands.telemetry.config import StrandsTelemetry
# Initialize and configure the OTLP exporter (reads env vars above)
telemetry = StrandsTelemetry()
telemetry.setup_otlp_exporter()
# Your agent code is unchanged
agent = Agent(
system_prompt="You are a helpful assistant.",
)
result = agent("Summarize the key features of AWS Strands Agents.")
print(result)
```
Getting traces into Latitude is only half the job. For Latitude to display the model name, token counts, and input/output messages, your spans must carry the right `gen_ai.*` attributes in the format Latitude expects.
`OTEL_SEMCONV_STABILITY_OPT_IN=gen_ai_latest_experimental` (set in the previous step) tells Strands to emit these attributes automatically.
See [GenAI Span Attributes](/telemetry/otel-exporter#genai-span-attributes-llm-metadata) for the full list of required attributes and the exact message format Latitude expects.
***
## What you get
Each agent run shows up as a trace with nested spans:
* **Agent spans** — agent name, ID, description, user message, full response, total token usage
* **Cycle spans** — one span per event-loop cycle with `event_loop.cycle_id`
* **LLM spans** — model invocations with input/output messages, token counts (prompt, completion, cache), model parameters
* **Tool spans** — tool name, input arguments, output result, and `tool.status`
***
## Seeing Your Traces
Once connected, traces appear automatically in Latitude:
1. Open your **project** in the Latitude dashboard
2. Each agent run shows the full hierarchy: agent → LLM calls → tool executions
3. Token usage and latency are aggregated at every level
# Vercel AI SDK
Source: https://docs.latitude.so/telemetry/frameworks/vercel-ai-sdk
Connect your Vercel AI SDK-powered application to Latitude for observability.
## Overview
This guide shows you how to integrate **Latitude Telemetry** into an application that uses the **Vercel AI SDK**.
The Vercel AI SDK can emit OpenTelemetry spans through its `experimental_telemetry` flag. Latitude can ingest those spans without a provider-specific instrumentation entry.
This guide covers **Vercel AI SDK v6**. On **v7**, OpenTelemetry moved into the separate
`@ai-sdk/otel` package and became opt-out — see the
[Vercel AI SDK v7](/telemetry/frameworks/vercel-ai-sdk-v7) guide instead.
You'll keep calling the Vercel AI SDK exactly as you do today. Telemetry simply
observes and enriches those calls.
The Vercel AI SDK integration is **TypeScript only**.
***
## Requirements
* A **Latitude account** and **API key**
* A **Latitude project slug**
* A Node.js project that uses the **Vercel AI SDK** (`ai` package)
***
## Steps
```bash npm theme={"theme":{"light":"github-light","dark":"github-dark"}}
npm install @latitude-data/telemetry
```
```bash pnpm theme={"theme":{"light":"github-light","dark":"github-dark"}}
pnpm add @latitude-data/telemetry
```
```bash yarn theme={"theme":{"light":"github-light","dark":"github-dark"}}
yarn add @latitude-data/telemetry
```
```bash bun theme={"theme":{"light":"github-light","dark":"github-dark"}}
bun add @latitude-data/telemetry
```
Initialize Latitude **without** an `instrumentations` array. The Vercel AI SDK emits its own OpenTelemetry spans. Pass `latitude.getTracer("vercelai")` to the AI SDK telemetry config on each call.
```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
import { Latitude, capture } from "@latitude-data/telemetry"
import { generateText } from "ai"
import { openai } from "@ai-sdk/openai"
const latitude = new Latitude({
apiKey: process.env.LATITUDE_API_KEY!,
project: process.env.LATITUDE_PROJECT_SLUG!,
})
await latitude.ready
await capture("generate-support-reply", async () => {
const { text } = await generateText({
model: openai("gpt-4o"),
prompt: "Hello",
experimental_telemetry: {
isEnabled: true,
tracer: latitude.getTracer("vercelai"),
},
})
return text
})
await latitude.shutdown()
```
***
## Seeing Your Traces
Once connected, traces appear automatically in Latitude:
1. Open your **project** in the Latitude dashboard
2. Each execution shows input/output messages, model, token usage, latency, and errors
# Vercel AI SDK v7
Source: https://docs.latitude.so/telemetry/frameworks/vercel-ai-sdk-v7
Connect your Vercel AI SDK v7 powered application to Latitude for observability.
## Overview
This guide shows you how to integrate **Latitude Telemetry** into an application that uses **Vercel AI SDK v7**.
Using **Vercel AI SDK v6**? See the [Vercel AI SDK](/telemetry/frameworks/vercel-ai-sdk) guide
instead — its telemetry setup is different.
In v7, OpenTelemetry collection **moved out of the `ai` package** into the separate **`@ai-sdk/otel`** package, and it is now **opt-out**: once you register a telemetry integration, every AI SDK call emits telemetry by default — there is no per-call `experimental_telemetry` flag anymore. Latitude ingests those spans without a provider-specific instrumentation entry.
You'll keep calling the Vercel AI SDK exactly as you do today. Telemetry simply
observes and enriches those calls.
The Vercel AI SDK integration is **TypeScript only**.
***
## Requirements
* A **Latitude account** and **API key**
* A **Latitude project slug**
* A Node.js project that uses **Vercel AI SDK v7** (`ai@7`) and the matching **`@ai-sdk/otel`**
***
## Steps
Install Latitude Telemetry and the AI SDK v7 OpenTelemetry package.
```bash npm theme={"theme":{"light":"github-light","dark":"github-dark"}}
npm install @latitude-data/telemetry @ai-sdk/otel
```
```bash pnpm theme={"theme":{"light":"github-light","dark":"github-dark"}}
pnpm add @latitude-data/telemetry @ai-sdk/otel
```
```bash yarn theme={"theme":{"light":"github-light","dark":"github-dark"}}
yarn add @latitude-data/telemetry @ai-sdk/otel
```
```bash bun theme={"theme":{"light":"github-light","dark":"github-dark"}}
bun add @latitude-data/telemetry @ai-sdk/otel
```
Initialize Latitude **without** an `instrumentations` array. Then register the AI SDK
`OpenTelemetry` integration **once**, after constructing `Latitude` — it uses the global tracer
provider that Latitude registered, so spans flow to Latitude automatically. No per-call flag is
needed.
```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
import { Latitude, capture } from "@latitude-data/telemetry"
import { generateText, registerTelemetry } from "ai"
import { OpenTelemetry } from "@ai-sdk/otel"
import { openai } from "@ai-sdk/openai"
const latitude = new Latitude({
apiKey: process.env.LATITUDE_API_KEY!,
project: process.env.LATITUDE_PROJECT_SLUG!,
})
await latitude.ready
// Register once, after Latitude. All AI SDK calls now emit telemetry (opt-out).
registerTelemetry(new OpenTelemetry())
await capture("generate-support-reply", async () => {
const { text } = await generateText({
model: openai("gpt-4o"),
prompt: "Hello",
})
return text
})
await latitude.shutdown()
```
The recommended integration is `OpenTelemetry`, which emits standard OpenTelemetry GenAI
semantic-convention spans. `@ai-sdk/otel` also exports `LegacyOpenTelemetry`, which emits the
older `ai.*` spans (same as v6). Latitude ingests both.
**Pass your system prompt via the `instructions` field.** AI SDK v7's telemetry drops
`role: "system"` messages placed inside `messages`, so they won't appear in your traces. Use the
top-level `instructions` field instead — it's captured correctly.
```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
await generateText({
model: openai("gpt-4o"),
instructions: "You are a helpful assistant.", // ✅ shows up in traces
prompt: "Hello",
})
```
### Next.js
Register the integration in your `instrumentation.ts`, alongside your OpenTelemetry provider setup:
```ts instrumentation.ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
import { registerOTel } from "@vercel/otel"
import { registerTelemetry } from "ai"
import { OpenTelemetry } from "@ai-sdk/otel"
export function register() {
registerOTel({ serviceName: "my-ai-app" })
registerTelemetry(new OpenTelemetry())
}
```
### Opting out
Telemetry is opt-out. To disable it for a specific call, set `telemetry: { isEnabled: false }`. To disable it globally, don't register any telemetry integration.
***
## STT → LLM → TTS
AI SDK 7 ships stable [`transcribe()`](https://sdk.vercel.ai/docs/reference/ai-sdk-core/transcribe) and [`generateSpeech()`](https://sdk.vercel.ai/docs/reference/ai-sdk-core/generate-speech) on [AI Gateway](https://vercel.com/docs/ai-gateway). A typical voice turn chains all three:
1. **STT** — `transcribe()` converts incoming audio to text
2. **LLM** — `generateText()` (or `streamText()`) produces a reply
3. **TTS** — `generateSpeech()` synthesizes the reply as audio
`@ai-sdk/otel` traces the **LLM step automatically**. It does not yet emit spans for `transcribe()` or `generateSpeech()` — wrap those calls in manual OpenTelemetry spans inside a `capture()` boundary so all three stages appear in one trace.
```mermaid theme={"theme":{"light":"github-light","dark":"github-dark"}}
sequenceDiagram
participant Client
participant Server
participant STT as transcribe()
participant LLM as generateText()
participant TTS as generateSpeech()
Client->>Server: audio
Server->>STT: transcribe
STT-->>Server: transcript
Server->>LLM: generateText(prompt)
LLM-->>Server: reply text
Server->>TTS: generateSpeech(text)
TTS-->>Server: audio
Server-->>Client: audio
```
### One voice turn
```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
import { readFile } from "node:fs/promises"
import { trace } from "@opentelemetry/api"
import { openai } from "@ai-sdk/openai"
import { generateSpeech, generateText, transcribe } from "ai"
import { capture } from "@latitude-data/telemetry"
const tracer = trace.getTracer("voice.pipeline")
async function handleVoiceTurn(audioPath: string, sessionId: string) {
return capture(
"voice-turn",
async () => {
const audio = await readFile(audioPath)
const { text: transcript } = await tracer.startActiveSpan(
"transcribe whisper-1",
async (span) => {
span.setAttribute("gen_ai.operation.name", "transcribe")
span.setAttribute("gen_ai.provider.name", "openai")
span.setAttribute("gen_ai.request.model", "whisper-1")
const result = await transcribe({
model: openai.transcription("whisper-1"),
audio,
})
span.setAttribute(
"gen_ai.output.messages",
JSON.stringify([
{
role: "assistant",
parts: [{ type: "text", content: result.text }],
},
]),
)
span.end()
return result
},
)
const { text: reply } = await generateText({
model: openai("gpt-4o"),
instructions: "You are a concise voice assistant.",
prompt: transcript,
})
await tracer.startActiveSpan("speech tts-1", async (span) => {
span.setAttribute("gen_ai.operation.name", "speech")
span.setAttribute("gen_ai.provider.name", "openai")
span.setAttribute("gen_ai.request.model", "tts-1")
span.setAttribute(
"gen_ai.input.messages",
JSON.stringify([
{ role: "user", parts: [{ type: "text", content: reply }] },
]),
)
const { audio: speechAudio } = await generateSpeech({
model: openai.speech("tts-1"),
text: reply,
voice: "alloy",
})
span.setAttribute("voice.output.bytes", speechAudio.uint8Array.byteLength)
span.end()
return speechAudio
})
},
{ sessionId, tags: ["voice", "stt-llm-tts"] },
)
}
```
Pass a stable `sessionId` on every `capture()` call to group voice turns into a conversation in Latitude.
### What you see in Latitude
| Stage | Traced by | Span operation |
| ----- | -------------- | ---------------------------------------------- |
| STT | Manual span | `transcribe` — transcript in output messages |
| LLM | `@ai-sdk/otel` | `chat` — full input/output, tokens, latency |
| TTS | Manual span | `speech` — input text, model, audio byte count |
Models on AI Gateway (e.g. `openai/whisper-1`, `openai/tts-1`) resolve for cost tracking the same way as chat models.
`experimental_useRealtime` (browser WebSocket voice) does not produce
server-side `@ai-sdk/otel` spans. For realtime voice observability, use
[LiveKit Agents](/telemetry/frameworks/livekit) or wrap backend tool routes in
`capture()`.
***
## Seeing Your Traces
Once connected, traces appear automatically in Latitude:
1. Open your **project** in the Latitude dashboard
2. Each execution shows input/output messages, model, token usage, latency, and errors
# Hermes
Source: https://docs.latitude.so/telemetry/hermes
# Hermes telemetry
Stream [Hermes Agent](https://github.com/NousResearch/hermes-agent) (Nous Research's open-source agent harness) runs into Latitude as traces. After setup, each Hermes turn appears in your project's **Traces** view with user prompts, model turns, tool calls, tool results, token usage, timing, and the real system prompt that reached the model.
## Prerequisites
* A [Latitude account](https://console.latitude.so/login) with a project
* Hermes Agent installed locally
* `pip` (Hermes already runs on Python — the plugin uses only the standard library plus `certifi`, which Hermes already ships)
## Install
1. In Latitude, copy your project slug from the project sidebar.
2. Create or copy an API key from **Settings → API Keys**.
3. Install the plugin:
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
pip install latitude-telemetry-hermes
```
4. Enable it by adding `latitude` to the enabled-plugins list in `~/.hermes/config.yaml`:
```yaml theme={"theme":{"light":"github-light","dark":"github-dark"}}
plugins:
enabled:
- latitude
```
Hermes discovers the plugin through the `hermes_agent.plugins` entry point — there are no files to copy.
**Enable via `config.yaml`, not `hermes plugins enable latitude`.** Hermes's runtime loads
pip/entry-point plugins, but its `hermes plugins list`/`enable`/`disable` commands scan only
bundled and `~/.hermes/plugins/` directory plugins — so they report a pip-installed plugin as
**"not installed or bundled"** even though it loads fine ([hermes-agent#23802](https://github.com/NousResearch/hermes-agent/issues/23802)).
The `config.yaml` entry above is the reliable way to turn it on.
The plugin must be installed into the **same Python that runs Hermes**. The official installer
puts Hermes in its own venv (`~/.hermes/hermes-agent/venv`) that ignores your shell's Python, so
a plain `pip install` from another interpreter (system, pyenv, mise, …) won't be discovered.
Install into Hermes's venv instead:
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
~/.hermes/bin/uv pip install --python ~/.hermes/hermes-agent/venv/bin/python latitude-telemetry-hermes
```
5. Set your credentials in the environment, or add them to `~/.hermes/.env` (Hermes loads it at startup):
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
LATITUDE_API_KEY=lat_xxx
LATITUDE_PROJECT=your-project-slug
```
The plugin sends to Latitude Cloud (`https://ingest.latitude.so`) by default. If you
run a **self-hosted or local** Latitude, also set `LATITUDE_BASE_URL` to your ingest
**origin only** — for example `http://localhost:3002` on a local dev stack — with no
`/v1/traces` suffix (the plugin appends it). The API key and project slug must belong
to that same instance.
## Verify
Run Hermes and send a message to your agent, then open your Latitude project and go to **Traces**. The new trace should appear within a few seconds.
If nothing arrives, set `LATITUDE_DEBUG=true` in `~/.hermes/.env` and run again to see the plugin's logging. (`hermes plugins list` does **not** show pip-installed plugins — see the install note — so it can't be used to confirm the plugin is loaded.)
## Structural-only telemetry
If you want trace structure without prompt, response, or tool content, set:
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
LATITUDE_NO_CONTENT=true
```
Structural-only traces still include timing, model, token usage, and run structure. Message content and tool input/output are omitted.
## Disable or uninstall
To pause telemetry without removing anything, set the environment variable in `~/.hermes/.env`:
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
LATITUDE_HERMES_TELEMETRY_ENABLED=0
```
To stop Hermes loading the plugin at all, remove `latitude` from `plugins.enabled` in `~/.hermes/config.yaml`. (`hermes plugins disable latitude` doesn't work for pip-installed plugins — see the install note.)
To remove the integration entirely, drop it from `plugins.enabled` and uninstall the package:
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
pip uninstall latitude-telemetry-hermes
```
(Your `~/.hermes/.env` credentials are left in place so a re-install is one step.)
## Configuration
All configuration is read from environment variables (set them in your shell or `~/.hermes/.env`):
| Env | Default | Description |
| ------------------------------------------------------------------ | ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| `LATITUDE_API_KEY` | — | API key (required) |
| `LATITUDE_PROJECT` / `LATITUDE_PROJECT_SLUG` | — | Project slug (required) |
| `LATITUDE_BASE_URL` | `https://ingest.latitude.so` | Ingest origin (no path; the plugin appends `/v1/traces`). Set to your own ingest for self-hosted/local, e.g. `http://localhost:3002` |
| `LATITUDE_HERMES_TELEMETRY_ENABLED` / `LATITUDE_TELEMETRY_ENABLED` | `true` | Master switch |
| `LATITUDE_HERMES_NO_CONTENT` / `LATITUDE_NO_CONTENT` | `false` | Export structure/timing only |
| `LATITUDE_DEBUG` | `false` | Verbose logging |
Telemetry stays off until both `LATITUDE_API_KEY` and a project are set.
## How it works
Hermes loads pip-installed plugins via the `hermes_agent.plugins` entry point and calls the module's `register(ctx)`, which subscribes to its lifecycle hooks (`pre_api_request` / `post_api_request`, `pre_llm_call` / `post_llm_call`, `pre_tool_call` / `post_tool_call`). The plugin assembles each turn into one trace — an `interaction` root span with an `llm_request` child per model call and a `tool_execution` child per tool call — and ships it to Latitude over OTLP. It is fail-open: a telemetry error never affects your agent.
## Captured data and privacy
By default, Latitude receives the content needed to reconstruct Hermes runs, including prompts, responses, the system prompt, tool input/output, model metadata, and token usage.
* Set `LATITUDE_NO_CONTENT=true` when you only want structural telemetry.
* Telemetry runs for each turn until disabled or uninstalled.
* Disable telemetry before working with sensitive material you do not want sent to Latitude.
## Troubleshooting
**No traces appear.** Confirm the plugin is enabled (`hermes plugins list`), check that the API key and project slug in `~/.hermes/.env` are correct, and send a new message.
**Need more diagnostics.** Set `LATITUDE_DEBUG=true` in `~/.hermes/.env` and trigger another run.
**Traces show timing but no content.** Structural-only mode is enabled. Remove `LATITUDE_NO_CONTENT` from `~/.hermes/.env`.
# Braintrust
Source: https://docs.latitude.so/telemetry/imports/braintrust
Import historical project logs from Braintrust into a Latitude project.
## Overview
This guide shows you how to import your **Braintrust** history into Latitude. Latitude
reads the logs in a Braintrust project and maps each span to a Latitude span.
Importing only reads from Braintrust. Nothing is changed or deleted at the source.
***
## Requirements
* A **Braintrust project** with the logs you want to import
* A Braintrust **API key** (`sk-…`) with read access to that project
* Your Braintrust **data plane** — EU or US, found under **Settings → Data plane**
***
## Steps
In Braintrust, open **Settings → API keys** and create a key.
In Latitude, go to **Project settings → Imports**, choose **Braintrust** and your data
plane, enter the API key, then press **Test connection**.
Pick the Braintrust project, set the time range and trace limit, check the preview,
then press **Start import**.
***
## Braintrust specifics
* Session and user identity are read from `metadata.session_id` and `metadata.user_id`;
token usage and Braintrust's own cost estimate come through directly.
* Images and files are fetched from Braintrust while the import runs and stored with the
message (up to 2 MB each), so imported conversations do not depend on your Braintrust
account afterwards.
Braintrust flattens messages logged over **OpenTelemetry** and can drop reasoning blocks
doing so. Set `braintrust.otel.preserve_attributes: true` in your instrumentation and
reasoning imports intact. Logs written with Braintrust's own SDK are unaffected.
***
## Seeing your traces
Once imported, traces appear in your project's **Traces** view like any live trace, marked
with `import.*` metadata naming Braintrust and their original ids.
# Langfuse
Source: https://docs.latitude.so/telemetry/imports/langfuse
Import historical traces and observations from Langfuse Cloud.
## Overview
This guide shows you how to import your **Langfuse** history into Latitude. Latitude reads
the observations in a Langfuse project and maps them to Latitude traces and spans.
Importing only reads from Langfuse. Nothing is changed or deleted at the source.
***
## Requirements
* A **Langfuse Cloud project** with the history you want to import
* A Langfuse **public key** (`pk-lf-…`) and **secret key** (`sk-lf-…`) for that project
* Your Langfuse **region** — EU, US, Japan or HIPAA US
***
## Steps
In Langfuse, open **Settings → API keys** for the project you are importing from and
create a key pair. Copy both keys.
In Latitude, go to **Project settings → Imports**, choose **Langfuse** and your region,
enter both keys, then press **Test connection**.
Pick the Langfuse project, set the time range and trace limit, check the preview, then
press **Start import**.
***
## Langfuse specifics
* Sessions, users, tags, metadata, messages, tool calls, token usage and cost all map
directly — nothing needs configuring in Langfuse.
* Prompt references (`promptName`, `promptVersion`) land in span metadata, since Latitude
has no prompt registry.
* Langfuse Cloud regions are fully separate deployments: keys from another region will fail
the connection test. Pick the one you signed up in.
***
## Seeing your traces
Once imported, traces appear in your project's **Traces** view like any live trace, marked
with `import.*` metadata naming Langfuse and their original ids.
# LangSmith
Source: https://docs.latitude.so/telemetry/imports/langsmith
Import historical runs from a LangSmith project, including conversation grouping.
## Overview
This guide shows you how to import your **LangSmith** history into Latitude. Latitude reads
the runs in a LangSmith project and maps them to Latitude traces and spans.
Importing only reads from LangSmith. Nothing is changed or deleted at the source.
***
## Requirements
* A **LangSmith project** with the history you want to import
* A LangSmith **API key** (`lsv2_pt_…`)
* Your LangSmith **region** — GCP EU, GCP US, GCP APAC or AWS US
* Your **workspace id**, if your account has more than one workspace
***
## Steps
In LangSmith, open **Settings → API keys** and create a key. If your account has
several workspaces, also copy the id of the one you are importing from.
In Latitude, go to **Project settings → Imports**, choose **LangSmith** and your
region, enter the API key (and workspace id, if you have one), then press
**Test connection**.
Pick the LangSmith project, set the time range, trace limit and **session metadata
key** — see below — check the preview, then press **Start import**.
***
## Sessions need a metadata key
In LangSmith, `session_id` identifies the **project**, not a conversation, so Latitude
reads the conversation id from run metadata instead: it looks under `extra.metadata` for
the key you name, defaulting to `thread_id`. If no run carries the key, each trace becomes
its own session and turns will not group into conversations.
The preview tells you which key it used and warns you if no run in the sample carried
it — worth checking before a large import.
***
## LangSmith specifics
* Your region is the host you sign in on: `smith.langchain.com` is GCP US,
`eu.smith.langchain.com` is GCP EU, and so on. Keys do not cross regions.
* Runs are imported newest first, so a capped import keeps your most recent traces.
* LangSmith's rate limits are the tightest of the three platforms, so a large import takes
a while — it is paced deliberately and runs in the background.
***
## Seeing your traces
Once imported, traces appear in your project's **Traces** view like any live trace, marked
with `import.*` metadata naming LangSmith and their original ids.
# Overview
Source: https://docs.latitude.so/telemetry/imports/overview
Backfill traces from Langfuse, LangSmith, or Braintrust into a Latitude project.
## Overview
Latitude can import the traces you already have in another observability platform, so you
do not start from an empty project when you switch. Imports run from **Project settings →
Imports** and finish in the background — you can close the tab.
Importing is for **history**. Data produced from now on should arrive through
[live tracing](/telemetry/start-tracing), which is what powers evaluations, monitors and
signals in real time.
Pick your platform to get started:
Langfuse Cloud, using your public and secret keys.
Runs from a LangSmith project, with configurable session grouping.
Project logs, queried through BTQL.
***
## How an import runs
Pick the platform and the region your account is in, then enter its credentials.
Latitude tests them before continuing and discards them once the import finishes.
Choose the source project, the time range and how many traces to import. Latitude
shows a sample of what will come through before anything is written.
The import runs in the background. Progress, counts and any errors appear in the
imports table, which refreshes while it runs.
Every platform is imported from its **cloud** offering — you pick a region, never a URL.
Self-hosted deployments are not supported yet.
***
## What gets imported
| Imported | Not imported |
| ------------------------------------------ | ---------------------------------------------------- |
| Traces and spans | Scores and annotations |
| Session and user identity | Datasets and evaluations |
| Tags and metadata | Prompt registries (references land in span metadata) |
| Messages, tool calls, token usage and cost | |
Imported spans carry `import.*` metadata (platform, source project, and the original trace
and span ids), so you can always tell imported history from live telemetry. If your
organization has a **PII redaction policy**, it applies to imported traces exactly as it
does to live ones.
An imported trace goes through the same pipeline as one that arrives live: it is grouped
into sessions, embedded for semantic search, indexed, and evaluated by flaggers. Because of
that it is **billed the same way: one credit per trace**. Re-importing the same trace does
not charge twice.
***
## Limits
| Limit | How it is set |
| ----------------------- | ---------------------------------------------------------------------------- |
| Traces per import | Defaults to your plan's remaining usage for the period; hard ceiling 100,000 |
| Time range | Defaults to the last 90 days, bounded by your plan's span retention |
| Imports running at once | One per organization |
Traces are imported **newest first**, so an import that stops early — it reached your trace
limit or your plan's remaining usage — keeps the most recent history rather than an
arbitrary slice. A stopped import is marked **capped**, everything imported stays, and
**Continue** resumes it from where it stopped.
***
## Retrying
Imports are **idempotent**: every imported span gets a deterministic id derived from its
source ids, so importing the same range twice updates the same spans instead of duplicating
them. If an import fails or is cancelled, **Retry** resumes from where it stopped — you
will be asked for credentials again, because Latitude never keeps them after an import
ends.
# Memory tracing
Source: https://docs.latitude.so/telemetry/memory
Instrument your agent's long-term memory so Latitude can show how it evolves.
## Overview
This guide shows you how to send your agent's **long-term memory** operations to Latitude. Once instrumented, every read and write appears on the [Memory page](../observability/memory): each store's current contents, per-record history and diffs, and the tokens read and written per session, with every change linked to the session that caused it.
You keep using your memory store exactly as you do today. You emit one span
per memory operation, and Latitude derives the history, diffs, and token
counts from those spans. You never compute or send a diff yourself.
***
## Requirements
* Base tracing already sending traces to Latitude. If you are not tracing yet, start with [Start tracing](./start-tracing).
* An app with **long-term memory**: state your agent persists and reloads across separate interactions, such as a `memory/` directory, a database table, a vector store, a key-value store, or a provider like Mem0, Zep, or Supermemory. Within-request conversation history is not memory; base tracing already captures it.
Memory operations ride the same exporter as the rest of your telemetry, so there is no extra account or endpoint to configure.
***
## Instrument with your coding agent
Recommended. The Latitude skill inspects your codebase, finds your memory read and write call sites, and instruments them with the right operations and identifiers.
Paste this prompt into Claude Code, Cursor, Windsurf, Codex, OpenCode, or another coding agent:
```text theme={"theme":{"light":"github-light","dark":"github-dark"}}
Install the `latitude-telemetry` skill from `github.com/latitude-dev/skills`, and use it to add Latitude memory observability to this app's long-term memory, following best practices.
```
Trigger one interaction that recalls memory and one that writes it.
Open your project and go to **Memory**. The store appears within a few seconds of the trace completing.
Prefer to wire it up by hand? Continue below.
***
## The store and record model
Two identifiers carry the whole model, and getting them right is the entire job, because they are how Latitude groups, versions, and attributes everything. Think of a store as a git repository and a record as a file in it.
**Store (`gen_ai.memory.store.id`)** is one isolated pool of memory. The rule: two operations share a store if and only if a write by one should be visible to the other.
* **Per-user memory** that users cannot see across each other: use the user id, or a `user/` prefix, as the store id, so each user becomes a separate store.
* **Shared memory** that several users read and write: one store id for all of them, and the latest write wins across them.
* **Always set it.** A span without a store id lands in the `(unattributed)` store, which counts as activity but gives you nothing to browse. Keep it stable, or a store's history fragments into many stores.
**Record (`gen_ai.memory.record.id`)** is one addressable unit within a store: what a write creates or updates and what a read returns. Use the store's natural key, such as a file path relative to the memory root, a key-value key, a row id, or the provider's own record id. Latitude splits record ids on `/` to nest them into folders on the Memory page, so a path-like id gives you a browsable tree. The only operations without a record id are the whole-store ones.
**Writes carry the whole record, not a delta.** Each write span is a full
snapshot of the record as it stands after the write. If your app updates only
part of a record, read back or reconstruct the full body and send that.
Sending only the changed fragment makes Latitude read everything you omitted
as a deletion.
**Content capture is opt-in.** Record bodies and the search query can hold
sensitive data, so the SDK does not send them unless you enable
`captureContent`. With it off you still get classification and counts, but no
diffs, no token deltas, and nothing to browse. Turn it on unless the memory
holds data you cannot send to Latitude; when only some fields are sensitive,
keep it on and scrub them with the `redact` hook.
***
## Instrument manually
The memory helper ships in the same telemetry package as base tracing, so there is nothing new to add if you already installed it.
```bash npm theme={"theme":{"light":"github-light","dark":"github-dark"}}
npm install @latitude-data/telemetry
```
```bash pnpm theme={"theme":{"light":"github-light","dark":"github-dark"}}
pnpm add @latitude-data/telemetry
```
```bash yarn theme={"theme":{"light":"github-light","dark":"github-dark"}}
yarn add @latitude-data/telemetry
```
```bash bun theme={"theme":{"light":"github-light","dark":"github-dark"}}
bun add @latitude-data/telemetry
```
```bash pip theme={"theme":{"light":"github-light","dark":"github-dark"}}
pip install latitude-telemetry
```
```bash uv theme={"theme":{"light":"github-light","dark":"github-dark"}}
uv add latitude-telemetry
```
```bash poetry theme={"theme":{"light":"github-light","dark":"github-dark"}}
poetry add latitude-telemetry
```
Pass the `latitude` instance from your base tracing setup. Set `storeId` to scope the memory, using a user id for per-user memory, and enable content capture so diffs and token deltas work.
```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
import { createMemoryTelemetry } from "@latitude-data/telemetry"
// `latitude` is the instance from your base tracing setup.
const memory = createMemoryTelemetry({
latitude,
storeId: `user/${user.id}`,
captureContent: true,
})
```
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
from latitude_telemetry import create_memory_telemetry
# `latitude` is the instance from your base tracing setup.
memory = create_memory_telemetry(
latitude,
store_id=f"user/{user_id}",
capture_content=True,
)
```
Call the helper at your store's read and write boundaries, inside the `capture()` that already wraps the interaction so the spans join the trace. Each operation has a **wrap** form, where you pass `execute` and the helper runs your call inside the span and records its latency, status, and errors, and an **emit** form, with no `execute`, for when the read or write already happened elsewhere.
```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
import { capture } from "@latitude-data/telemetry"
await capture("support-agent-turn", async () => {
// Read: one span carries the query and every record it returned.
const hits = await memory.search({
query,
execute: () => store.search(query),
recordsFromResult: (rows) =>
rows.map((r) => ({ id: r.id, content: r.text, score: r.score })),
})
// Write: pass the record's full new body, not a delta.
await memory.upsert({
recordId: "preferences/tone",
records: [
{ id: "preferences/tone", content: "Prefers concise answers with code." },
],
execute: () => store.upsert("preferences/tone", nextValue),
})
// Emit form: record a completed span when the read or write already happened.
await memory.update({
recordId: "preferences/tone",
records: [
{ id: "preferences/tone", content: "Prefers concise answers with code." },
],
})
// Delete one record. Omit recordId to wipe the whole store.
await memory.delete({ recordId: "preferences/tone" })
})
```
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
from latitude_telemetry import capture
def handle_turn():
# Read: one span carries the query and every record it returned.
hits = memory.search(
query=query,
execute=lambda: store.search(query),
records_from_result=lambda rows: [
{"id": r["id"], "content": r["text"], "score": r["score"]} for r in rows
],
)
# Write: pass the record's full new body, not a delta.
memory.upsert(
record_id="preferences/tone",
records=[
{"id": "preferences/tone", "content": "Prefers concise answers with code."}
],
execute=lambda: store.upsert("preferences/tone", next_value),
)
# Emit form: record a completed span when the read or write already happened.
memory.update(
record_id="preferences/tone",
records=[
{"id": "preferences/tone", "content": "Prefers concise answers with code."}
],
)
# Delete one record. Omit record_id to wipe the whole store.
memory.delete(record_id="preferences/tone")
capture("support-agent-turn", handle_turn)
```
### Without the SDK
If your app traces to Latitude without the Latitude SDK, or runs in another language, emit the spans on your existing tracer. Set the span name and `gen_ai.operation.name` to the operation, and add the `gen_ai.memory.*` attributes.
```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
import { trace } from "@opentelemetry/api"
const tracer = trace.getTracer("memory")
await tracer.startActiveSpan("update_memory", async (span) => {
span.setAttributes({
"gen_ai.operation.name": "update_memory",
"gen_ai.memory.store.id": `user/${user.id}`,
"gen_ai.memory.record.id": "preferences/tone",
"gen_ai.memory.records": JSON.stringify([
{ id: "preferences/tone", content: "Prefers concise answers with code." },
]),
})
await store.update("preferences/tone", nextValue)
span.end()
})
```
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
import json
from opentelemetry import trace
tracer = trace.get_tracer("memory")
with tracer.start_as_current_span("search_memory") as span:
span.set_attribute("gen_ai.operation.name", "search_memory")
span.set_attribute("gen_ai.memory.store.id", f"user/{user_id}")
span.set_attribute("gen_ai.memory.query.text", query)
hits = store.search(query)
span.set_attribute("gen_ai.memory.record.count", len(hits))
span.set_attribute(
"gen_ai.memory.records",
json.dumps(
[{"id": h["id"], "content": h["text"], "score": h["score"]} for h in hits]
),
)
```
Other runtimes such as Go, Java, Ruby, and .NET set the same attributes on whatever span they already export. Only `gen_ai.operation.name` is required; add the rest to unlock more of the Memory page.
***
## Operations and attributes
Memory operations are the standard OpenTelemetry GenAI **memory operation spans**, not a Latitude-specific format. The span name equals `gen_ai.operation.name`, and both are one of:
| Operation | Meaning |
| --------------------- | --------------------------------------------------------------- |
| `create_memory` | Create new records |
| `update_memory` | Modify existing records |
| `upsert_memory` | Create or update, without choosing which |
| `delete_memory` | Delete records. Without a record id, this wipes the whole store |
| `search_memory` | Query or retrieve records (a read) |
| `create_memory_store` | Create or initialize a store |
| `delete_memory_store` | Delete a store and everything in it |
These attributes control what Latitude can show:
| Attribute | Purpose |
| ---------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
| `gen_ai.operation.name` | The operation above. Required |
| `gen_ai.memory.store.id` | The store the operation targets, e.g. `user/usr_4f2a`. Everything groups under it; absent lands in the `(unattributed)` store |
| `gen_ai.memory.record.id` | The record touched, e.g. `preferences/tone`. Splits on `/` to nest records into folders |
| `gen_ai.memory.record.count` | How many records were affected or returned |
| `gen_ai.memory.query.text` | The search query, on `search_memory` spans. Opt-in |
| `gen_ai.memory.records` | The content payload: an array of `{ id, content, score?, metadata? }` objects. Powers content browsing, diffs, and token counts. Opt-in |
***
## Verify
Instrumentation is not finished when the code compiles, only when operations show up correctly in Latitude.
* Run a real interaction that recalls memory and one that writes it, so you produce both a `search_memory` span and a mutating span. For short-lived scripts, call `await latitude.flush()` (Python: `latitude.flush()`) before exit so spans flush.
* On the **Spans tab**, the operations classify and carry the store and record attributes.
* On the **[Memory page](../observability/memory)**, the store appears under the expected id, its records show the expected bodies, and a second write to a record produces a new version and a diff.
* On the **trace or session detail**, the **Memory** row shows tokens read, added, and removed.
Common issues: an empty `store.id` (everything lands in `(unattributed)`), an unstable or missing `record.id` (history will not stitch and reads will not attribute), a write sending a partial body instead of the full snapshot, or a span emitted outside `capture()` so it never joins the trace.
## Next steps
* [Memory](../observability/memory): Explore stores, record history, diffs, and per-session footprint
* [Start tracing](./start-tracing): Base tracing setup, if you are not sending traces yet
# Openclaw
Source: https://docs.latitude.so/telemetry/openclaw
# OpenClaw telemetry
Stream OpenClaw agent runs into Latitude as traces. After setup, agent runs appear in your project's **Traces** view with model calls, tool calls, token usage, cost, timing, and nested subagent activity — as a proper `invoke_agent → chat → execute_tool` tree.
The recommended way is OpenClaw's **official OpenTelemetry exporter** (the bundled `@openclaw/diagnostics-otel` plugin), pointed at Latitude's OTLP ingest. It follows OpenTelemetry GenAI semantic conventions and is maintained by OpenClaw — see [OpenClaw's OpenTelemetry docs](https://docs.openclaw.ai/gateway/opentelemetry).
> **Note:** OpenClaw's official exporter is the preferred setup and is documented below. If you need to group a multi-turn conversation into a Latitude **session**, the Latitude-maintained [`@latitude-data/openclaw-telemetry`](https://github.com/latitude-dev/latitude-llm/tree/main/packages/telemetry/openclaw) plugin remains the only option that emits a session id today — the native exporter doesn't yet ([openclaw/openclaw#91927](https://github.com/openclaw/openclaw/issues/91927)).
## Prerequisites
* A [Latitude account](https://console.latitude.so/login) with a project
* OpenClaw 2026.6 or newer
* A Latitude **API key** and your **project slug** (project sidebar → **Settings → API Keys**)
## Setup
### 1. Install the exporter
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
openclaw plugins install clawhub:@openclaw/diagnostics-otel
```
### 2. Point it at Latitude
Add the `diagnostics.otel` block to `~/.openclaw/openclaw.json`. Latitude's ingest accepts OTLP/HTTP (protobuf) at `/v1/traces` and authenticates with your API key; the project is selected with the `X-Latitude-Project` header.
```jsonc theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
"diagnostics": {
"otel": {
"enabled": true,
"traces": true,
"protocol": "http/protobuf",
"tracesEndpoint": "https://ingest.latitude.so/v1/traces",
"headers": {
"Authorization": "Bearer lat_xxx",
"X-Latitude-Project": "your-project-slug"
},
"captureContent": {
"enabled": true,
"inputMessages": true,
"outputMessages": true,
"toolInputs": true,
"toolOutputs": true,
"systemPrompt": true
}
}
}
}
```
The same keys can be set non-interactively:
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
openclaw config set 'diagnostics.otel.enabled' true
openclaw config set 'diagnostics.otel.traces' true
openclaw config set 'diagnostics.otel.protocol' '"http/protobuf"'
openclaw config set 'diagnostics.otel.tracesEndpoint' '"https://ingest.latitude.so/v1/traces"'
openclaw config set 'diagnostics.otel.headers' \
'{"Authorization":"Bearer lat_xxx","X-Latitude-Project":"your-project-slug"}'
openclaw config set 'diagnostics.otel.captureContent' \
'{"enabled":true,"inputMessages":true,"outputMessages":true,"toolInputs":true,"toolOutputs":true,"systemPrompt":true}'
```
### 3. Restart and verify
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
openclaw gateway restart
```
Send a message to an agent, then open your Latitude project and go to **Traces** — the run should appear within a few seconds.
## Structural-only telemetry
To capture trace structure (timing, model, token usage, cost, run/tool shape) without prompt, response, or tool content, set `captureContent.enabled` to `false`:
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
openclaw config set 'diagnostics.otel.captureContent.enabled' false
openclaw gateway restart
```
## Captured data and privacy
With `captureContent.enabled = true`, Latitude receives the content needed to reconstruct runs — prompts, responses, system instructions, and tool input/output — plus model metadata, token usage, and cost. The granular `captureContent.*` flags let you capture some kinds of content and not others. Content is **not** exported unless you opt in. Disable capture (or telemetry entirely) before working with sensitive material you don't want sent to Latitude.
## Disable
Pause the exporter without uninstalling:
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
openclaw config set 'diagnostics.otel.enabled' false
openclaw gateway restart
```
## Switching to the native exporter
If you previously installed `@latitude-data/openclaw-telemetry`, remove it and switch to the native exporter above:
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
openclaw plugins uninstall @latitude-data/openclaw-telemetry --force
openclaw plugins install clawhub:@openclaw/diagnostics-otel
# add the diagnostics.otel config from step 2, then:
openclaw gateway restart
```
The native exporter produces a cleaner `invoke_agent → chat → execute_tool` structure, and no changes are needed on the Latitude side.
> **Note — session grouping.** The native exporter does not export a session id (OpenClaw redacts session keys by design — see [OpenClaw's OpenTelemetry docs](https://docs.openclaw.ai/gateway/opentelemetry)), so each agent turn arrives as its own trace and the **Sessions** view won't group a multi-turn conversation. The `@latitude-data/openclaw-telemetry` plugin emits a `session.id` and does group turns. We're tracking opt-in upstream support in [openclaw/openclaw#91927](https://github.com/openclaw/openclaw/issues/91927); once it lands, Latitude groups automatically (we already resolve `session.id`, `gen_ai.session.id`, `gen_ai.conversation.id`, and `langfuse.session.id`).
## Troubleshooting
**No traces appear.** Restart the gateway, confirm the API key and project slug are correct, and send a new agent message. A `401`/`403` from ingest means the API key isn't valid for that project's organization.
**Traces show timing but no content.** `captureContent.enabled` is `false` — set it to `true` and restart.
**Traces aren't grouped into sessions.** Expected with the native exporter — it doesn't emit a session id (see the note above). Each turn is its own trace until [openclaw/openclaw#91927](https://github.com/openclaw/openclaw/issues/91927) ships an opt-in session attribute. For session grouping today, use the `@latitude-data/openclaw-telemetry` plugin.
**Traces silently never arrive.** Check that `tracesEndpoint` uses **`https://`**. Plain `http://` gets a `301` redirect that OTLP exporters don't follow on `POST`, so batches are dropped with no error.
# OpenTelemetry Exporter (OTEL)
Source: https://docs.latitude.so/telemetry/otel-exporter
Connect any OpenTelemetry-instrumented application to Latitude, regardless of language or framework.
# Connect with Any OpenTelemetry Exporter
Latitude's ingestion endpoint speaks standard **OTLP over HTTP**.
If your language has an OpenTelemetry SDK (Go, Java, Ruby, Rust, .NET, Elixir, PHP, etc.), you can send traces to Latitude without a Latitude-specific library.
Using **TypeScript** or **Python**? The dedicated SDKs handle all of this for you with a single function call. See the [TypeScript SDK](/telemetry/typescript) or [Python SDK](/telemetry/python) instead.
## Prerequisites
1. A **Latitude API key** — generate one from your project settings in the Latitude dashboard.
2. Your **project slug** — visible in the project settings or URL.
## Endpoint and Headers
| | Value |
| ------------------------ | ---------------------------------------------- |
| **URL** | `https://ingest.latitude.so/v1/traces` |
| **Method** | `POST` |
| **`Authorization`** | `Bearer ` |
| **`X-Latitude-Project`** | `` |
| **`Content-Type`** | `application/json` or `application/x-protobuf` |
The endpoint accepts a standard OTLP `ExportTraceServiceRequest` body and returns **`202`** on success.
## Verify with curl
A minimal request to confirm connectivity (replace the placeholders):
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl -X POST https://ingest.latitude.so/v1/traces \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "X-Latitude-Project: YOUR_PROJECT_SLUG" \
-H "Content-Type: application/json" \
-d '{
"resourceSpans": [{
"resource": {
"attributes": [{
"key": "service.name",
"value": { "stringValue": "my-service" }
}]
},
"scopeSpans": [{
"scope": { "name": "manual-test" },
"spans": [{
"traceId": "00000000000000000000000000000001",
"spanId": "0000000000000001",
"name": "test-span",
"kind": 1,
"startTimeUnixNano": "1700000000000000000",
"endTimeUnixNano": "1700000001000000000",
"attributes": [{
"key": "gen_ai.system",
"value": { "stringValue": "openai" }
}]
}]
}]
}]
}'
```
A `202` response with `{}` means the endpoint accepted your payload.
## Language Examples
Every OpenTelemetry SDK lets you configure an OTLP HTTP exporter with a custom endpoint and headers. Below are the key configuration snippets — the rest of your OTel setup (tracer provider, instrumentations, etc.) stays the same as usual.
### Go
```go theme={"theme":{"light":"github-light","dark":"github-dark"}}
import (
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp"
"go.opentelemetry.io/otel/sdk/trace"
)
exporter, err := otlptracehttp.New(ctx,
otlptracehttp.WithEndpointURL("https://ingest.latitude.so/v1/traces"),
otlptracehttp.WithHeaders(map[string]string{
"Authorization": "Bearer " + apiKey,
"X-Latitude-Project": projectSlug,
}),
)
provider := trace.NewTracerProvider(trace.WithBatcher(exporter))
```
### Java
```java theme={"theme":{"light":"github-light","dark":"github-dark"}}
import io.opentelemetry.exporter.otlp.trace.OtlpHttpSpanExporter;
import io.opentelemetry.sdk.trace.SdkTracerProvider;
import io.opentelemetry.sdk.trace.export.BatchSpanProcessor;
OtlpHttpSpanExporter exporter = OtlpHttpSpanExporter.builder()
.setEndpoint("https://ingest.latitude.so/v1/traces")
.addHeader("Authorization", "Bearer " + apiKey)
.addHeader("X-Latitude-Project", projectSlug)
.build();
SdkTracerProvider provider = SdkTracerProvider.builder()
.addSpanProcessor(BatchSpanProcessor.builder(exporter).build())
.build();
```
### Ruby
```ruby theme={"theme":{"light":"github-light","dark":"github-dark"}}
require "opentelemetry-sdk"
require "opentelemetry-exporter-otlp"
ENV["OTEL_EXPORTER_OTLP_TRACES_ENDPOINT"] = "https://ingest.latitude.so/v1/traces"
ENV["OTEL_EXPORTER_OTLP_TRACES_HEADERS"] = "Authorization=Bearer #{api_key},X-Latitude-Project=#{project_slug}"
OpenTelemetry::SDK.configure do |c|
c.add_span_processor(
OpenTelemetry::SDK::Trace::Export::BatchSpanProcessor.new(
OpenTelemetry::Exporter::OTLP::Exporter.new
)
)
end
```
### .NET
```csharp theme={"theme":{"light":"github-light","dark":"github-dark"}}
using OpenTelemetry;
using OpenTelemetry.Trace;
using OpenTelemetry.Exporter;
var tracerProvider = Sdk.CreateTracerProviderBuilder()
.AddOtlpExporter(opt =>
{
opt.Endpoint = new Uri("https://ingest.latitude.so/v1/traces");
opt.Headers = $"Authorization=Bearer {apiKey},X-Latitude-Project={projectSlug}";
opt.Protocol = OtlpExportProtocol.HttpProtobuf;
})
.Build();
```
### Environment Variables (Any Language)
Most OpenTelemetry SDKs respect standard environment variables, so you can often skip code changes entirely:
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
export OTEL_EXPORTER_OTLP_TRACES_ENDPOINT="https://ingest.latitude.so/v1/traces"
export OTEL_EXPORTER_OTLP_TRACES_HEADERS="Authorization=Bearer YOUR_API_KEY,X-Latitude-Project=YOUR_PROJECT_SLUG"
```
## Latitude Span Attributes
All spans reaching the endpoint are ingested. To get the most out of the Latitude UI (filtering by user, session, tags), set these optional span attributes:
| Attribute | Type | Description |
| ----------------------- | ---------------------- | ------------------------------------------------------------- |
| `latitude.capture.name` | `string` | A name for the capture context (e.g. `"handle-user-request"`) |
| `latitude.tags` | `string` (JSON array) | Tags for filtering, e.g. `'["production","v2"]'` |
| `latitude.metadata` | `string` (JSON object) | Arbitrary key-value pairs, e.g. `'{"requestId":"abc"}'` |
| `session.id` | `string` | Group related traces into a session |
| `user.id` | `string` | Associate traces with a specific user |
Set these as standard OTel span attributes. Example in Go:
```go theme={"theme":{"light":"github-light","dark":"github-dark"}}
span.SetAttributes(
attribute.String("user.id", "user_123"),
attribute.String("session.id", "session_abc"),
attribute.String("latitude.tags", `["production","v2-agent"]`),
attribute.String("latitude.metadata", `{"requestId":"req-xyz"}`),
)
```
## GenAI Span Attributes (LLM Metadata)
Configuring the OTLP exporter (endpoint + headers) only ensures your spans **reach** Latitude. For Latitude to display LLM call details — model name, token counts, input/output messages — your spans must carry the [OpenTelemetry GenAI semantic convention](https://opentelemetry.io/docs/specs/semconv/gen-ai/) attributes described below.
Without these attributes, traces will appear in the dashboard but show no model, token usage, or message content.
Latitude follows the **[OpenTelemetry Semantic Conventions for Generative AI](https://opentelemetry.io/docs/specs/semconv/gen-ai/gen-ai-spans/)**. When your spans include `gen_ai.*` attributes, the Latitude UI displays full LLM call details: the provider, model, input/output messages, token usage, cost, and latency.
Refer to the [GenAI span semconv spec](https://opentelemetry.io/docs/specs/semconv/gen-ai/gen-ai-spans/) for the full attribute list. The message content attributes use the standard OpenTelemetry GenAI **parts-based** message format, shown below.
### Message Format
Latitude expects the standard **parts-based** GenAI message format. Each message has a `role` and an array of `parts`:
**`gen_ai.input.messages`** (JSON string):
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
[
{
"role": "user",
"parts": [{"type": "text", "content": "What is the capital of France?"}]
}
]
```
**`gen_ai.output.messages`** (JSON string):
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
[
{
"role": "assistant",
"parts": [{"type": "text", "content": "The capital of France is Paris."}]
}
]
```
**`gen_ai.system_instructions`** (JSON string):
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
[{"type": "text", "content": "You are a helpful geography assistant."}]
```
## Existing Observability Stack
Latitude works alongside your existing observability tools. In TypeScript, `new Latitude()` detects common OpenTelemetry-compatible providers (Sentry, Datadog, New Relic, Honeycomb, and custom OTel SDKs) and attaches Latitude when possible. In Python, `Latitude(...)` attaches to the registered OpenTelemetry provider when one already exists or creates one when none exists. Custom setups in either SDK can also add `LatitudeSpanProcessor` as an additional span processor so traces go to both Latitude and your current backend.
### With Datadog (TypeScript)
```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
import OpenAI from "openai"
import tracer from "dd-trace"
import { createOpenAIInstrumentation } from "@latitude-data/telemetry/instrumentations/openai"
import { Latitude } from "@latitude-data/telemetry"
tracer.init({ service: "my-app", env: "production" })
const latitude = new Latitude({
apiKey: process.env.LATITUDE_API_KEY!,
project: process.env.LATITUDE_PROJECT_SLUG!,
instrumentations: [createOpenAIInstrumentation(OpenAI)],
})
await latitude.ready
```
### With Sentry (TypeScript)
```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
import OpenAI from "openai"
import * as Sentry from "@sentry/node"
import { createOpenAIInstrumentation } from "@latitude-data/telemetry/instrumentations/openai"
import { Latitude } from "@latitude-data/telemetry"
Sentry.init({
dsn: process.env.SENTRY_DSN,
tracesSampleRate: 1.0,
})
const latitude = new Latitude({
apiKey: process.env.LATITUDE_API_KEY!,
project: process.env.LATITUDE_PROJECT_SLUG!,
instrumentations: [createOpenAIInstrumentation(OpenAI)],
})
await latitude.ready
```
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.
### With New Relic (TypeScript)
Enable New Relic's OpenTelemetry bridge first, then construct `new Latitude()`. New Relic registers an OpenTelemetry provider that Latitude can reuse.
```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
import "newrelic"
import OpenAI from "openai"
import { createOpenAIInstrumentation } from "@latitude-data/telemetry/instrumentations/openai"
import { Latitude } from "@latitude-data/telemetry"
const latitude = new Latitude({
apiKey: process.env.LATITUDE_API_KEY!,
project: process.env.LATITUDE_PROJECT_SLUG!,
instrumentations: [createOpenAIInstrumentation(OpenAI)],
})
await latitude.ready
```
### With Honeycomb (TypeScript)
Start Honeycomb's `HoneycombSDK` first, then construct `new Latitude()`. Honeycomb registers an OpenTelemetry provider that Latitude can reuse.
```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
import OpenAI from "openai"
import { HoneycombSDK } from "@honeycombio/opentelemetry-node"
import { createOpenAIInstrumentation } from "@latitude-data/telemetry/instrumentations/openai"
import { Latitude } from "@latitude-data/telemetry"
const honeycomb = new HoneycombSDK({ serviceName: "my-app" })
honeycomb.start()
const latitude = new Latitude({
apiKey: process.env.LATITUDE_API_KEY!,
project: process.env.LATITUDE_PROJECT_SLUG!,
instrumentations: [createOpenAIInstrumentation(OpenAI)],
})
await latitude.ready
```
### Other Platforms
For any observability platform that supports OpenTelemetry (Jaeger, Grafana Tempo, etc.), the pattern is the same: initialize the existing SDK first and then construct `Latitude`, or configure an additional OTLP exporter pointed at `https://ingest.latitude.so/v1/traces` with the required `Authorization` and `X-Latitude-Project` headers alongside your existing exporter.
## Troubleshooting
### 401 Unauthorized
The `Authorization` header must use the `Bearer ` prefix (with a space). Double-check your API key is valid and not expired.
### 400 Bad Request — missing project header
The `X-Latitude-Project` header is required on every request. Ensure it is present and spelled correctly, and that the value matches a project slug in the organization associated with your API key.
### 202 but no traces visible
* **Empty body:** An empty request body is accepted with `202` but produces no traces. Ensure your exporter is actually attaching span data.
* **Smart filter (SDK only):** If you are using a Latitude SDK's `LatitudeSpanProcessor`, only LLM-relevant spans are exported by default. This does not apply when sending OTLP directly — all spans are ingested.
* **Flush before exit:** Make sure your tracer provider flushes pending spans before the process exits.
### Traces appear but show no model, tokens, or messages
This is the most common issue when integrating via the generic OTLP exporter. Your exporter is configured correctly (you get `202` and traces appear), but the Latitude UI shows empty LLM metadata.
**Cause:** Your spans are missing the `gen_ai.*` semantic convention attributes. Configuring the exporter only ensures spans reach Latitude — it does not add LLM metadata. You must explicitly set attributes like `gen_ai.provider.name`, `gen_ai.request.model`, `gen_ai.usage.input_tokens`, `gen_ai.input.messages`, etc. on your spans.
**Fix:** See [GenAI Span Attributes (LLM Metadata)](#genai-span-attributes-llm-metadata) above for the full list of attributes and message format. At a minimum, set:
1. `gen_ai.provider.name` — provider name (e.g. `"openai"`, `"anthropic"`)
2. `gen_ai.request.model` — model name
3. `gen_ai.operation.name` — set to `"chat"`
4. `gen_ai.usage.input_tokens` and `gen_ai.usage.output_tokens` — token counts
5. `gen_ai.input.messages` and `gen_ai.output.messages` — the actual conversation content
If you use a Latitude SDK or an OpenTelemetry auto-instrumentation library (e.g. `opentelemetry-instrumentation-openai`), these attributes are set automatically. This section only applies when you manually create spans for LLM calls without auto-instrumentation.
### Encoding
The endpoint accepts both `application/json` (JSON-encoded OTLP) and `application/x-protobuf` (protobuf-encoded OTLP). Most OTel SDK exporters default to protobuf; both work.
# Pi coding agent
Source: https://docs.latitude.so/telemetry/pi-coding-agent
# Pi coding agent telemetry
Stream [pi coding agent](https://pi.dev) sessions into Latitude as traces with the first-party `@latitude-data/pi-telemetry` extension.
After setup, pi prompts appear in your Latitude project's **Traces** view with model calls, prompts, responses, token usage, tool calls, and tool results.
## Prerequisites
* A [Latitude account](https://console.latitude.so/login) with a project
* A Latitude API key from **Settings → API Keys**
* Your Latitude project slug from the project sidebar
* `pi` installed locally
* Node.js available on your `PATH`
## Install
Run the installer:
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
npx -y @latitude-data/pi-telemetry install
```
The installer prompts for your API key and project slug, adds the pi extension package to `~/.pi/agent/settings.json`, and writes Latitude config to `~/.pi/agent/latitude-telemetry.json`.
You can also pass values directly:
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
npx -y @latitude-data/pi-telemetry install \
--api-key=lat_xxx \
--project=your-project-slug \
--yes
```
## Restart and verify
Restart pi so it can install and load the package. Send a prompt that uses the model or a tool, then open your Latitude project and go to **Traces**. The new trace should appear within a few seconds.
A trace includes:
* an `interaction` root span for the pi prompt
* `llm_request` spans for model calls
* `tool_call:` spans for tool executions
* `gen_ai.input.messages` and `gen_ai.output.messages` for prompt/response reconstruction
* `gen_ai.tool.call.arguments` and `gen_ai.tool.call.result` for tool I/O
## Structural-only telemetry
By default, Latitude receives the content needed to reconstruct pi runs, including prompts, assistant responses, system instructions when available, tool arguments, and tool results.
If you want trace structure without conversation or tool content, install with:
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
npx -y @latitude-data/pi-telemetry install --no-content
```
Structural-only traces still include timing, token usage, provider/model names, tool names, session metadata, user/host identity, and whether content capture was enabled.
## Disable or uninstall
Disable the extension for one pi process:
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
LATITUDE_PI_TELEMETRY_ENABLED=0 pi
```
Remove the extension and config:
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
npx -y @latitude-data/pi-telemetry uninstall
```
Restart pi after uninstalling to unload the extension.
## Advanced configuration
The installer stores config in `~/.pi/agent/latitude-telemetry.json`. Environment variables override file values:
| Variable | Description |
| --------------------------------------------- | ----------------------------------------------------------------------------- |
| `LATITUDE_API_KEY` | API key used to upload traces |
| `LATITUDE_PROJECT` or `LATITUDE_PROJECT_SLUG` | Project slug for `X-Latitude-Project` |
| `LATITUDE_BASE_URL` | Ingest base URL, default `https://ingest.latitude.so` |
| `LATITUDE_DEBUG=1` | Print upload diagnostics |
| `LATITUDE_PI_NO_CONTENT=1` | Disable prompt/response/tool content capture |
| `LATITUDE_PI_USER_ID` | Override `user.id` |
| `LATITUDE_PI_USER_EMAIL` | Override user email |
| `LATITUDE_PI_USER_NAME` | Override user display name |
| `LATITUDE_REDACT_ATTRIBUTES` | JSON array or comma-separated custom attribute patterns to mask before export |
| `LATITUDE_REDACT_MASK` | Mask value for custom redaction, default `******` |
Use `--staging`, `--dev`, or `--base-url=` during install to target a non-production Latitude ingest endpoint.
## Captured data and privacy
Full-content mode sends prompts, responses, system instructions when available, tool inputs, and tool outputs. Use `--no-content` for structural-only telemetry or configure custom redaction as described below.
Disable telemetry before working with sensitive material you do not want sent to Latitude. Even `--no-content` mode still sends structural metadata such as cwd/session identifiers, model names, tool names, timing, token usage, and user/host identity.
### Custom redaction
If you want to keep content capture enabled but mask specific span attributes before they leave your machine, use `redact` in `~/.pi/agent/latitude-telemetry.json` or the `LATITUDE_REDACT_ATTRIBUTES` environment variable. Redaction happens locally, after the content gate and before the OTLP export.
`redact.attributes` (or `LATITUDE_REDACT_ATTRIBUTES`) accepts an array of patterns. Each pattern can be:
* An **exact attribute name** — `"gen_ai.tool.call.arguments"`
* A **regex source string** — `"^gen_ai\\.(input|output)\\.messages$"` (anchored match)
* A **`/pattern/flags` string** — `"/^gen_ai\\.tool\\.call\\.(arguments|result)$/i"`
`redact.mask` (or `LATITUDE_REDACT_MASK`) sets the replacement value (default: `******`). Set it to `"[]"` to replace message arrays with an empty array instead of a string.
#### Via config file
Add a `redact` block to `~/.pi/agent/latitude-telemetry.json`:
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
"redact": {
"attributes": [
"/^gen_ai\\.(input|output)\\.messages$/",
"gen_ai.tool.call.arguments",
"gen_ai.tool.call.result"
],
"mask": "[]"
}
}
```
#### Via environment variable
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
LATITUDE_REDACT_ATTRIBUTES='["/^gen_ai\\.(input|output)\\.messages$/", "gen_ai.tool.call.arguments"]' \
LATITUDE_REDACT_MASK='[]' \
pi
```
## Troubleshooting
**No traces appear.** Restart pi or run `/reload`, then send a new prompt. Confirm the API key and project slug are correct.
**HTTP 401.** The API key is missing or invalid. Re-run the installer with a valid key.
**HTTP 400 about `X-Latitude-Project`.** The project slug is missing. Re-run the installer with `--project=your-project-slug`.
**Traces show timing but no prompt/response content.** Structural-only mode is enabled. Reinstall without `--no-content`, or set `LATITUDE_PI_NO_CONTENT=0` and reload pi.
**Need diagnostics.** Run `LATITUDE_DEBUG=1 pi` and trigger another prompt.
# Prime intellect
Source: https://docs.latitude.so/telemetry/prime-intellect
# Prime Intellect (Verifiers) telemetry
Stream [Prime Intellect Verifiers](https://github.com/PrimeIntellect-ai/verifiers) eval rollouts into Latitude as traces. After setup, each rollout appears in your project's **Traces** view with prompts, model calls, tool calls, token usage, timing, and rewards — optionally as Latitude custom scores.
## Prerequisites
* A [Latitude account](https://console.latitude.so/login) with a project
* A Verifiers v1 eval setup (`verifiers.v1`, `uv run eval`, or `prime eval`)
* `pip` / `uv` in the same environment that runs your eval
## Install
1. In Latitude, copy your project slug from the project sidebar.
2. Create or copy an API key from **Settings → API Keys**.
3. Install the package into the env that runs Verifiers:
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
pip install latitude-telemetry-prime-intellect
# or: uv pip install latitude-telemetry-prime-intellect
```
4. Set credentials:
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
export LATITUDE_API_KEY=lat_xxx
export LATITUDE_PROJECT=your-project-slug
```
Traces go to Latitude Cloud ingest (`https://ingest.latitude.so`) by default. Scores use the public API (`https://api.latitude.so`). For self-hosted or local Latitude, set `LATITUDE_BASE_URL` (ingest origin, no `/v1/traces` suffix) and `LATITUDE_API_BASE_URL` (API origin) to match that instance.
## Usage
### From a Python eval script
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
from latitude_telemetry_prime_intellect import export_episodes
episodes = await run_eval(env, config)
export_episodes(episodes)
```
Or attach an `on_complete` callback to `Env.run_slot`:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
from latitude_telemetry_prime_intellect import make_on_complete
on_complete = make_on_complete(next=your_persist_callback)
await env.run_slot(slot, ctx, semaphore, on_complete)
```
### After a CLI eval (post-hoc)
Run your eval as usual, then export the results directory:
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
latitude-prime-intellect-export export ./outputs/
```
The CLI looks for `traces.jsonl`, `episodes.jsonl`, or `results.jsonl`.
## Verify
Open your Latitude project → **Traces**. New rollouts should appear within a few seconds after export. If you left score export enabled (default), rewards show up as custom scores on those traces.
If nothing arrives, set `LATITUDE_DEBUG=true` and re-run the export.
## Structural-only telemetry
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
export LATITUDE_NO_CONTENT=true
```
Structural-only traces still include timing, model, token usage, and span structure. Message and tool content are omitted.
## Disable scores
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
export LATITUDE_EXPORT_SCORES=false
# or: latitude-prime-intellect-export export ./outputs/ --no-scores
```
## Configuration
| Env | Default | Description |
| --------------------------------------------------------------------------- | ---------------------------- | ------------------------------------- |
| `LATITUDE_API_KEY` | — | API key (required) |
| `LATITUDE_PROJECT` / `LATITUDE_PROJECT_SLUG` | — | Project slug (required) |
| `LATITUDE_BASE_URL` | `https://ingest.latitude.so` | Ingest origin (appends `/v1/traces`) |
| `LATITUDE_API_BASE_URL` | `https://api.latitude.so` | Public API origin for scores |
| `LATITUDE_EXPORT_SCORES` | `true` | POST rewards/metrics as custom scores |
| `LATITUDE_PRIME_INTELLECT_TELEMETRY_ENABLED` / `LATITUDE_TELEMETRY_ENABLED` | `true` | Master switch |
| `LATITUDE_NO_CONTENT` | `false` | Structure/timing only |
| `LATITUDE_DEBUG` | `false` | Verbose logging |
Telemetry stays off until both `LATITUDE_API_KEY` and a project are set.
## How it works
Verifiers v1 records each rollout as a typed `Trace` (messages, `ModelCall`s, tools, rewards, timing). This package maps that record to OTLP:
```text theme={"theme":{"light":"github-light","dark":"github-dark"}}
interaction (one rollout; session = eval/episode id)
├── llm_request (one per model call)
└── tool_call: (tool_execution; one per tool result)
```
It POSTs traces to ingest and, when enabled, POSTs each reward as a custom score (`sourceId = verifiers.reward.`) against the same 32-hex trace id. Export is fail-open: a Latitude error never fails your eval.
## Captured data and privacy
By default Latitude receives prompts, responses, tool I/O, model metadata, token usage, and reward values.
* Set `LATITUDE_NO_CONTENT=true` for structural telemetry only.
* Disable or skip export before working with sensitive material you do not want sent to Latitude.
## Troubleshooting
**No traces appear.** Confirm `LATITUDE_API_KEY` and `LATITUDE_PROJECT` are set in the same environment that runs the export, and that the results path contains a `.jsonl` file.
**Scores missing.** Check `LATITUDE_EXPORT_SCORES` is not false, and that `LATITUDE_API_BASE_URL` points at the same Latitude instance as your API key.
**Traces show timing but no content.** Structural-only mode is on — unset `LATITUDE_NO_CONTENT`.
# Aleph Alpha
Source: https://docs.latitude.so/telemetry/providers/aleph-alpha
Connect your Aleph Alpha-powered application to Latitude for observability.
## Overview
This guide shows you how to integrate **Latitude Telemetry** into an application that uses the **Aleph Alpha** SDK (`aleph-alpha-client`).
You'll keep calling Aleph Alpha exactly as you do today. Telemetry simply
observes and enriches those calls.
Auto-instrumentation for Aleph Alpha is available in the **Python** SDK. For other
languages, send traces via the [OpenTelemetry exporter](/telemetry/otel-exporter).
***
## Requirements
* A **Latitude account** and **API key**
* A **Latitude project slug**
* A project that uses the **Aleph Alpha SDK** (`aleph-alpha-client`)
* An **Aleph Alpha API token** (set as `ALEPH_ALPHA_API_KEY`)
***
## Steps
```bash pip theme={"theme":{"light":"github-light","dark":"github-dark"}}
pip install latitude-telemetry aleph-alpha-client
```
```bash uv theme={"theme":{"light":"github-light","dark":"github-dark"}}
uv add latitude-telemetry aleph-alpha-client
```
```bash poetry theme={"theme":{"light":"github-light","dark":"github-dark"}}
poetry add latitude-telemetry aleph-alpha-client
```
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
import os
import aleph_alpha_client
from aleph_alpha_client import Client, CompletionRequest, Prompt
from latitude_telemetry import Latitude, capture
latitude = Latitude(
api_key="your-api-key",
project="your-project-slug",
instrumentations={"aleph_alpha": aleph_alpha_client},
)
def generate_reply():
client = Client(token=os.environ["ALEPH_ALPHA_API_KEY"])
request = CompletionRequest(prompt=Prompt.from_text("Hello:"), maximum_tokens=50)
response = client.complete(request, model="luminous-base")
return response.completions[0].completion
capture("generate-reply", generate_reply)
latitude.shutdown()
```
***
## Seeing Your Traces
Once connected, traces appear automatically in Latitude:
1. Open your **project** in the Latitude dashboard
2. Each execution shows input/output messages, model, token usage, latency, and errors
# Amazon Bedrock
Source: https://docs.latitude.so/telemetry/providers/amazon-bedrock
Connect your Amazon Bedrock-powered application to Latitude for observability.
## Overview
This guide shows you how to integrate **Latitude Telemetry** into an application that uses **Amazon Bedrock**.
You'll keep calling Bedrock exactly as you do today. Telemetry simply
observes and enriches those calls.
***
## Requirements
* A **Latitude account** and **API key**
* A **Latitude project slug**
* A project that uses the **AWS Bedrock SDK**
***
## Steps
```bash npm theme={"theme":{"light":"github-light","dark":"github-dark"}}
npm install @latitude-data/telemetry @aws-sdk/client-bedrock-runtime
```
```bash pnpm theme={"theme":{"light":"github-light","dark":"github-dark"}}
pnpm add @latitude-data/telemetry @aws-sdk/client-bedrock-runtime
```
```bash yarn theme={"theme":{"light":"github-light","dark":"github-dark"}}
yarn add @latitude-data/telemetry @aws-sdk/client-bedrock-runtime
```
```bash bun theme={"theme":{"light":"github-light","dark":"github-dark"}}
bun add @latitude-data/telemetry @aws-sdk/client-bedrock-runtime
```
```bash pip theme={"theme":{"light":"github-light","dark":"github-dark"}}
pip install latitude-telemetry
```
```bash uv theme={"theme":{"light":"github-light","dark":"github-dark"}}
uv add latitude-telemetry
```
```bash poetry theme={"theme":{"light":"github-light","dark":"github-dark"}}
poetry add latitude-telemetry
```
```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
import { createBedrockInstrumentation } from "@latitude-data/telemetry/instrumentations/bedrock"
import { Latitude, capture } from "@latitude-data/telemetry"
import {
BedrockRuntimeClient,
InvokeModelCommand,
} from "@aws-sdk/client-bedrock-runtime"
import * as BedrockSDK from "@aws-sdk/client-bedrock-runtime"
const latitude = new Latitude({
apiKey: process.env.LATITUDE_API_KEY!,
project: process.env.LATITUDE_PROJECT_SLUG!,
instrumentations: [createBedrockInstrumentation(BedrockSDK)],
})
await latitude.ready
const client = new BedrockRuntimeClient({ region: "us-east-1" })
await capture("generate-reply", async () => {
const command = new InvokeModelCommand({
modelId: "anthropic.claude-3-haiku-20240307-v1:0",
contentType: "application/json",
body: JSON.stringify({
anthropic_version: "bedrock-2023-05-31",
max_tokens: 1024,
messages: [{ role: "user", content: "Hello" }],
}),
})
const response = await client.send(command)
return JSON.parse(new TextDecoder().decode(response.body))
})
await latitude.shutdown()
```
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
import json
import boto3
from latitude_telemetry import Latitude, capture
latitude = Latitude(
api_key="your-api-key",
project="your-project-slug",
instrumentations={"bedrock": boto3},
)
client = boto3.client("bedrock-runtime", region_name="us-east-1")
def generate_reply():
response = client.invoke_model(
modelId="anthropic.claude-3-haiku-20240307-v1:0",
contentType="application/json",
body=json.dumps({
"anthropic_version": "bedrock-2023-05-31",
"max_tokens": 1024,
"messages": [{"role": "user", "content": "Hello"}],
}),
)
return json.loads(response["body"].read())
capture("generate-reply", generate_reply)
latitude.shutdown()
```
***
## Streaming
When streaming, consume the stream inside `capture()` so the span covers the full operation:
```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
import { InvokeModelWithResponseStreamCommand } from "@aws-sdk/client-bedrock-runtime"
await capture("stream-reply", async () => {
const command = new InvokeModelWithResponseStreamCommand({
modelId: "anthropic.claude-3-haiku-20240307-v1:0",
contentType: "application/json",
body: JSON.stringify({
anthropic_version: "bedrock-2023-05-31",
max_tokens: 1024,
messages: [{ role: "user", content: input }],
}),
})
const response = await client.send(command)
for await (const event of response.body!) {
if (event.chunk) {
const data = JSON.parse(new TextDecoder().decode(event.chunk.bytes))
if (data.type === "content_block_delta") {
res.write(data.delta.text)
}
}
}
res.end()
})
```
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
def stream_reply():
response = client.invoke_model_with_response_stream(
modelId="anthropic.claude-3-haiku-20240307-v1:0",
contentType="application/json",
body=json.dumps({
"anthropic_version": "bedrock-2023-05-31",
"max_tokens": 1024,
"messages": [{"role": "user", "content": input}],
}),
)
chunks = []
for event in response["body"]:
chunk = json.loads(event["chunk"]["bytes"])
if chunk.get("type") == "content_block_delta":
chunks.append(chunk["delta"]["text"])
return "".join(chunks)
capture("stream-reply", stream_reply)
```
***
## Seeing Your Traces
Once connected, traces appear automatically in Latitude:
1. Open your **project** in the Latitude dashboard
2. Each execution shows input/output messages, model, token usage, latency, and errors
# Anthropic
Source: https://docs.latitude.so/telemetry/providers/anthropic
Connect your Anthropic-powered application to Latitude for observability.
## Overview
This guide shows you how to integrate **Latitude Telemetry** into an application that uses the **Anthropic** SDK.
You'll keep calling Anthropic exactly as you do today. Telemetry simply
observes and enriches those calls.
***
## Requirements
* A **Latitude account** and **API key**
* A **Latitude project slug**
* A project that uses the **Anthropic SDK**
***
## Steps
```bash npm theme={"theme":{"light":"github-light","dark":"github-dark"}}
npm install @latitude-data/telemetry @anthropic-ai/sdk
```
```bash pnpm theme={"theme":{"light":"github-light","dark":"github-dark"}}
pnpm add @latitude-data/telemetry @anthropic-ai/sdk
```
```bash yarn theme={"theme":{"light":"github-light","dark":"github-dark"}}
yarn add @latitude-data/telemetry @anthropic-ai/sdk
```
```bash bun theme={"theme":{"light":"github-light","dark":"github-dark"}}
bun add @latitude-data/telemetry @anthropic-ai/sdk
```
```bash pip theme={"theme":{"light":"github-light","dark":"github-dark"}}
pip install latitude-telemetry
```
```bash uv theme={"theme":{"light":"github-light","dark":"github-dark"}}
uv add latitude-telemetry
```
```bash poetry theme={"theme":{"light":"github-light","dark":"github-dark"}}
poetry add latitude-telemetry
```
```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
import { createAnthropicInstrumentation } from "@latitude-data/telemetry/instrumentations/anthropic"
import { Latitude, capture } from "@latitude-data/telemetry"
import Anthropic, * as AnthropicSDK from "@anthropic-ai/sdk"
const latitude = new Latitude({
apiKey: process.env.LATITUDE_API_KEY!,
project: process.env.LATITUDE_PROJECT_SLUG!,
instrumentations: [createAnthropicInstrumentation(AnthropicSDK)],
})
await latitude.ready
const client = new Anthropic()
await capture("generate-reply", async () => {
const message = await client.messages.create({
model: "claude-sonnet-4-20250514",
max_tokens: 1024,
messages: [{ role: "user", content: "Hello" }],
})
return message.content[0].text
})
await latitude.shutdown()
```
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
import anthropic
from anthropic import Anthropic
from latitude_telemetry import Latitude, capture
latitude = Latitude(
api_key="your-api-key",
project="your-project-slug",
instrumentations={"anthropic": anthropic},
)
client = Anthropic()
def generate_reply():
message = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=1024,
messages=[{"role": "user", "content": "Hello"}],
)
return message.content[0].text
capture("generate-reply", generate_reply)
latitude.shutdown()
```
***
## Streaming
When streaming, consume the stream inside `capture()` so the span covers the full operation:
```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
await capture("stream-reply", async () => {
const stream = client.messages.stream({
model: "claude-sonnet-4-20250514",
max_tokens: 1024,
messages: [{ role: "user", content: input }],
})
for await (const event of stream) {
if (
event.type === "content_block_delta" &&
event.delta.type === "text_delta"
) {
res.write(event.delta.text)
}
}
res.end()
})
```
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
def stream_reply():
chunks = []
with client.messages.stream(
model="claude-sonnet-4-20250514",
max_tokens=1024,
messages=[{"role": "user", "content": input}],
) as stream:
for text in stream.text_stream:
chunks.append(text)
return "".join(chunks)
capture("stream-reply", stream_reply)
```
***
## Seeing Your Traces
Once connected, traces appear automatically in Latitude:
1. Open your **project** in the Latitude dashboard
2. Each execution shows input/output messages, model, token usage, latency, and errors
# Azure OpenAI
Source: https://docs.latitude.so/telemetry/providers/azure
Connect your Azure OpenAI-powered application to Latitude for observability.
## Overview
This guide shows you how to integrate **Latitude Telemetry** into an application that uses **Azure OpenAI**. Azure OpenAI uses the same `openai` SDK under the hood, so the `"openai"` instrumentation handles it automatically.
You'll keep calling Azure OpenAI exactly as you do today. Telemetry simply
observes and enriches those calls.
***
## Requirements
* A **Latitude account** and **API key**
* A **Latitude project slug**
* A project that uses the **Azure OpenAI SDK** (via the `openai` package)
***
## Steps
```bash npm theme={"theme":{"light":"github-light","dark":"github-dark"}}
npm install @latitude-data/telemetry openai
```
```bash pnpm theme={"theme":{"light":"github-light","dark":"github-dark"}}
pnpm add @latitude-data/telemetry openai
```
```bash yarn theme={"theme":{"light":"github-light","dark":"github-dark"}}
yarn add @latitude-data/telemetry openai
```
```bash bun theme={"theme":{"light":"github-light","dark":"github-dark"}}
bun add @latitude-data/telemetry openai
```
```bash pip theme={"theme":{"light":"github-light","dark":"github-dark"}}
pip install latitude-telemetry
```
```bash uv theme={"theme":{"light":"github-light","dark":"github-dark"}}
uv add latitude-telemetry
```
```bash poetry theme={"theme":{"light":"github-light","dark":"github-dark"}}
poetry add latitude-telemetry
```
Azure OpenAI uses the `"openai"` instrumentation: the same one used for standard OpenAI.
```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
import { createOpenAIInstrumentation } from "@latitude-data/telemetry/instrumentations/openai"
import { Latitude, capture } from "@latitude-data/telemetry"
import { AzureOpenAI, OpenAI } from "openai"
const latitude = new Latitude({
apiKey: process.env.LATITUDE_API_KEY!,
project: process.env.LATITUDE_PROJECT_SLUG!,
instrumentations: [createOpenAIInstrumentation(OpenAI)],
})
await latitude.ready
const client = new AzureOpenAI({
endpoint: process.env.AZURE_OPENAI_ENDPOINT,
apiKey: process.env.AZURE_OPENAI_API_KEY,
apiVersion: "2024-02-01",
})
await capture("generate-support-reply", async () => {
const completion = await client.chat.completions.create({
model: "gpt-4o",
messages: [{ role: "user", content: "Hello" }],
})
return completion.choices[0].message.content
})
await latitude.shutdown()
```
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
import openai
from openai import AzureOpenAI
from latitude_telemetry import Latitude, capture
latitude = Latitude(
api_key="your-api-key",
project="your-project-slug",
instrumentations={"openai": openai},
)
client = AzureOpenAI(
azure_endpoint="https://your-resource.openai.azure.com/",
api_key="your-azure-api-key",
api_version="2024-02-01",
)
def generate_support_reply():
completion = client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": "Hello"}],
)
return completion.choices[0].message.content
capture("generate-support-reply", generate_support_reply)
latitude.shutdown()
```
***
## Streaming
When streaming, consume the stream inside `capture()` so the span covers the full operation:
```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
await capture("stream-reply", async () => {
const stream = await client.chat.completions.create({
model: "gpt-4o",
messages: [{ role: "user", content: input }],
stream: true,
})
for await (const chunk of stream) {
const content = chunk.choices[0]?.delta?.content
if (content) res.write(content)
}
res.end()
})
```
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
def stream_reply():
stream = client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": input}],
stream=True,
)
chunks = []
for chunk in stream:
if chunk.choices[0].delta.content:
chunks.append(chunk.choices[0].delta.content)
return "".join(chunks)
capture("stream-reply", stream_reply)
```
***
## Seeing Your Traces
Once connected, traces appear automatically in Latitude:
1. Open your **project** in the Latitude dashboard
2. Each execution shows input/output messages, model, token usage, latency, and errors
# Cohere
Source: https://docs.latitude.so/telemetry/providers/cohere
Connect your Cohere-powered application to Latitude for observability.
## Overview
This guide shows you how to integrate **Latitude Telemetry** into an application that uses the **Cohere** SDK.
You'll keep calling Cohere exactly as you do today. Telemetry simply
observes and enriches those calls.
***
## Requirements
* A **Latitude account** and **API key**
* A **Latitude project slug**
* A project that uses the **Cohere SDK**
***
## Steps
```bash npm theme={"theme":{"light":"github-light","dark":"github-dark"}}
npm install @latitude-data/telemetry cohere-ai
```
```bash pnpm theme={"theme":{"light":"github-light","dark":"github-dark"}}
pnpm add @latitude-data/telemetry cohere-ai
```
```bash yarn theme={"theme":{"light":"github-light","dark":"github-dark"}}
yarn add @latitude-data/telemetry cohere-ai
```
```bash bun theme={"theme":{"light":"github-light","dark":"github-dark"}}
bun add @latitude-data/telemetry cohere-ai
```
```bash pip theme={"theme":{"light":"github-light","dark":"github-dark"}}
pip install latitude-telemetry
```
```bash uv theme={"theme":{"light":"github-light","dark":"github-dark"}}
uv add latitude-telemetry
```
```bash poetry theme={"theme":{"light":"github-light","dark":"github-dark"}}
poetry add latitude-telemetry
```
```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
import { createCohereInstrumentation } from "@latitude-data/telemetry/instrumentations/cohere"
import { Latitude, capture } from "@latitude-data/telemetry"
import { CohereClient } from "cohere-ai"
import * as CohereSDK from "cohere-ai"
const latitude = new Latitude({
apiKey: process.env.LATITUDE_API_KEY!,
project: process.env.LATITUDE_PROJECT_SLUG!,
instrumentations: [createCohereInstrumentation(CohereSDK)],
})
await latitude.ready
const client = new CohereClient({ token: process.env.COHERE_API_KEY })
await capture("generate-reply", async () => {
const response = await client.chat({
model: "command-a-03-2025",
message: "Hello",
})
return response.text
})
await latitude.shutdown()
```
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
import cohere
from latitude_telemetry import Latitude, capture
latitude = Latitude(
api_key="your-api-key",
project="your-project-slug",
instrumentations={"cohere": cohere},
)
client = cohere.Client()
def generate_reply():
response = client.chat(
model="command-a-03-2025",
message="Hello",
)
return response.text
capture("generate-reply", generate_reply)
latitude.shutdown()
```
***
## Streaming
When streaming, consume the stream inside `capture()` so the span covers the full operation:
```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
await capture("stream-reply", async () => {
const stream = await client.chatStream({
model: "command-a-03-2025",
message: input,
})
for await (const event of stream) {
if (event.eventType === "text-generation") {
res.write(event.text)
}
}
res.end()
})
```
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
def stream_reply():
chunks = []
for event in client.chat_stream(
model="command-a-03-2025",
message=input,
):
if event.event_type == "text-generation":
chunks.append(event.text)
return "".join(chunks)
capture("stream-reply", stream_reply)
```
***
## Seeing Your Traces
Once connected, traces appear automatically in Latitude:
1. Open your **project** in the Latitude dashboard
2. Each execution shows input/output messages, model, token usage, latency, and errors
# Google Gemini
Source: https://docs.latitude.so/telemetry/providers/gemini
Connect your Google Gemini (Developer API) application to Latitude for observability.
## Overview
This guide shows you how to integrate **Latitude Telemetry** into an application that uses the **Google Gemini Developer API** (`google-genai`).
You'll keep calling Gemini exactly as you do today. Telemetry simply
observes and enriches those calls.
Auto-instrumentation for Gemini is available in the **Python** SDK. For other
languages, send traces via the [OpenTelemetry exporter](/telemetry/otel-exporter).
Using **Vertex AI** or **Google Cloud AI Platform** instead of the Gemini
Developer API? See [Vertex AI](/telemetry/providers/vertex-ai) or
[Google AI Platform](/telemetry/providers/google-ai-platform).
***
## Requirements
* A **Latitude account** and **API key**
* A **Latitude project slug**
* A project that uses the **Gemini SDK** (`google-genai`)
* A **Gemini API key** (set as `GEMINI_API_KEY`)
***
## Steps
```bash pip theme={"theme":{"light":"github-light","dark":"github-dark"}}
pip install latitude-telemetry google-genai
```
```bash uv theme={"theme":{"light":"github-light","dark":"github-dark"}}
uv add latitude-telemetry google-genai
```
```bash poetry theme={"theme":{"light":"github-light","dark":"github-dark"}}
poetry add latitude-telemetry google-genai
```
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
import os
from google import genai
from latitude_telemetry import Latitude, capture
latitude = Latitude(
api_key="your-api-key",
project="your-project-slug",
instrumentations={"google_generativeai": genai},
)
client = genai.Client(api_key=os.environ["GEMINI_API_KEY"])
def generate_reply():
response = client.models.generate_content(
model="gemini-2.0-flash",
contents="Hello",
)
return response.text
capture("generate-reply", generate_reply)
latitude.shutdown()
```
***
## Seeing Your Traces
Once connected, traces appear automatically in Latitude:
1. Open your **project** in the Latitude dashboard
2. Each execution shows input/output messages, model, token usage, latency, and errors
# Google AI Platform
Source: https://docs.latitude.so/telemetry/providers/google-ai-platform
Connect your Google AI Platform-powered application to Latitude for observability.
## Overview
This guide shows you how to integrate **Latitude Telemetry** into an application that uses **Google Cloud AI Platform**.
You'll keep calling AI Platform exactly as you do today. Telemetry simply
observes and enriches those calls.
***
## Requirements
* A **Latitude account** and **API key**
* A **Latitude project slug**
* A project that uses the **Google Cloud AI Platform SDK**
***
## Steps
```bash npm theme={"theme":{"light":"github-light","dark":"github-dark"}}
npm install @latitude-data/telemetry @google-cloud/aiplatform
```
```bash pnpm theme={"theme":{"light":"github-light","dark":"github-dark"}}
pnpm add @latitude-data/telemetry @google-cloud/aiplatform
```
```bash yarn theme={"theme":{"light":"github-light","dark":"github-dark"}}
yarn add @latitude-data/telemetry @google-cloud/aiplatform
```
```bash bun theme={"theme":{"light":"github-light","dark":"github-dark"}}
bun add @latitude-data/telemetry @google-cloud/aiplatform
```
```bash pip theme={"theme":{"light":"github-light","dark":"github-dark"}}
pip install latitude-telemetry
```
```bash uv theme={"theme":{"light":"github-light","dark":"github-dark"}}
uv add latitude-telemetry
```
```bash poetry theme={"theme":{"light":"github-light","dark":"github-dark"}}
poetry add latitude-telemetry
```
```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
import { createAIPlatformInstrumentation } from "@latitude-data/telemetry/instrumentations/aiplatform"
import { Latitude, capture } from "@latitude-data/telemetry"
import { PredictionServiceClient } from "@google-cloud/aiplatform"
import * as AIPlatformSDK from "@google-cloud/aiplatform"
const latitude = new Latitude({
apiKey: process.env.LATITUDE_API_KEY!,
project: process.env.LATITUDE_PROJECT_SLUG!,
instrumentations: [createAIPlatformInstrumentation(AIPlatformSDK)],
})
await latitude.ready
const client = new PredictionServiceClient()
await capture("generate-prediction", async () => {
const [response] = await client.predict({
endpoint: `projects/${process.env.GCP_PROJECT_ID}/locations/us-central1/publishers/google/models/text-bison`,
instances: [{ content: "Hello" }],
parameters: { temperature: 0.2, maxOutputTokens: 256 },
})
return response.predictions
})
await latitude.shutdown()
```
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
from google.cloud import aiplatform
from latitude_telemetry import Latitude, capture
latitude = Latitude(
api_key="your-api-key",
project="your-project-slug",
instrumentations={"aiplatform": aiplatform},
)
aiplatform.init(project="your-gcp-project", location="us-central1")
def generate_prediction():
model = aiplatform.TextGenerationModel.from_pretrained("text-bison")
response = model.predict("Hello", temperature=0.2, max_output_tokens=256)
return response.text
capture("generate-prediction", generate_prediction)
latitude.shutdown()
```
***
## Seeing Your Traces
Once connected, traces appear automatically in Latitude:
1. Open your **project** in the Latitude dashboard
2. Each execution shows input/output messages, model, token usage, latency, and errors
# Groq
Source: https://docs.latitude.so/telemetry/providers/groq
Connect your Groq-powered application to Latitude for observability.
## Overview
This guide shows you how to integrate **Latitude Telemetry** into an application that uses the **Groq** SDK.
You'll keep calling Groq exactly as you do today. Telemetry simply
observes and enriches those calls.
Auto-instrumentation for Groq is available in the **Python** SDK. For other
languages, send traces via the [OpenTelemetry exporter](/telemetry/otel-exporter).
***
## Requirements
* A **Latitude account** and **API key**
* A **Latitude project slug**
* A project that uses the **Groq SDK** (`groq`)
* A **Groq API key** (set as `GROQ_API_KEY`)
***
## Steps
```bash pip theme={"theme":{"light":"github-light","dark":"github-dark"}}
pip install latitude-telemetry groq
```
```bash uv theme={"theme":{"light":"github-light","dark":"github-dark"}}
uv add latitude-telemetry groq
```
```bash poetry theme={"theme":{"light":"github-light","dark":"github-dark"}}
poetry add latitude-telemetry groq
```
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
import groq
from groq import Groq
from latitude_telemetry import Latitude, capture
latitude = Latitude(
api_key="your-api-key",
project="your-project-slug",
instrumentations={"groq": groq},
)
client = Groq()
def generate_reply():
response = client.chat.completions.create(
model="llama-3.1-8b-instant",
messages=[{"role": "user", "content": "Hello"}],
)
return response.choices[0].message.content
capture("generate-reply", generate_reply)
latitude.shutdown()
```
***
## Seeing Your Traces
Once connected, traces appear automatically in Latitude:
1. Open your **project** in the Latitude dashboard
2. Each execution shows input/output messages, model, token usage, latency, and errors
# Mistral AI
Source: https://docs.latitude.so/telemetry/providers/mistral
Connect your Mistral AI-powered application to Latitude for observability.
## Overview
This guide shows you how to integrate **Latitude Telemetry** into an application that uses the **Mistral AI** SDK.
You'll keep calling Mistral exactly as you do today. Telemetry simply
observes and enriches those calls.
Auto-instrumentation for Mistral is available in the **Python** SDK. For other
languages, send traces via the [OpenTelemetry exporter](/telemetry/otel-exporter).
***
## Requirements
* A **Latitude account** and **API key**
* A **Latitude project slug**
* A project that uses the **Mistral SDK** (`mistralai`)
* A **Mistral API key** (set as `MISTRAL_API_KEY`)
***
## Steps
```bash pip theme={"theme":{"light":"github-light","dark":"github-dark"}}
pip install latitude-telemetry mistralai
```
```bash uv theme={"theme":{"light":"github-light","dark":"github-dark"}}
uv add latitude-telemetry mistralai
```
```bash poetry theme={"theme":{"light":"github-light","dark":"github-dark"}}
poetry add latitude-telemetry mistralai
```
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
import os
import mistralai
from mistralai import Mistral
from latitude_telemetry import Latitude, capture
latitude = Latitude(
api_key="your-api-key",
project="your-project-slug",
instrumentations={"mistralai": mistralai},
)
client = Mistral(api_key=os.environ["MISTRAL_API_KEY"])
def generate_reply():
response = client.chat.complete(
model="mistral-small-latest",
messages=[{"role": "user", "content": "Hello"}],
)
return response.choices[0].message.content
capture("generate-reply", generate_reply)
latitude.shutdown()
```
***
## Seeing Your Traces
Once connected, traces appear automatically in Latitude:
1. Open your **project** in the Latitude dashboard
2. Each execution shows input/output messages, model, token usage, latency, and errors
# Ollama
Source: https://docs.latitude.so/telemetry/providers/ollama
Connect your Ollama-powered application to Latitude for observability.
## Overview
This guide shows you how to integrate **Latitude Telemetry** into an application that uses **Ollama** for local model inference.
You'll keep calling Ollama exactly as you do today. Telemetry simply
observes and enriches those calls.
Auto-instrumentation for Ollama is available in the **Python** SDK. For other
languages, send traces via the [OpenTelemetry exporter](/telemetry/otel-exporter).
***
## Requirements
* A **Latitude account** and **API key**
* A **Latitude project slug**
* A project that uses the **Ollama SDK** (`ollama`)
* A running **Ollama** server (set `OLLAMA_HOST` if it is not the default `http://localhost:11434`)
***
## Steps
```bash pip theme={"theme":{"light":"github-light","dark":"github-dark"}}
pip install latitude-telemetry ollama
```
```bash uv theme={"theme":{"light":"github-light","dark":"github-dark"}}
uv add latitude-telemetry ollama
```
```bash poetry theme={"theme":{"light":"github-light","dark":"github-dark"}}
poetry add latitude-telemetry ollama
```
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
import ollama
from latitude_telemetry import Latitude, capture
latitude = Latitude(
api_key="your-api-key",
project="your-project-slug",
instrumentations={"ollama": ollama},
)
def generate_reply():
response = ollama.chat(
model="llama3.2",
messages=[{"role": "user", "content": "Hello"}],
)
return response["message"]["content"]
capture("generate-reply", generate_reply)
latitude.shutdown()
```
***
## Seeing Your Traces
Once connected, traces appear automatically in Latitude:
1. Open your **project** in the Latitude dashboard
2. Each execution shows input/output messages, model, token usage, latency, and errors
# OpenAI
Source: https://docs.latitude.so/telemetry/providers/openai
Connect your OpenAI-powered application to Latitude for observability.
## Overview
This guide shows you how to integrate **Latitude Telemetry** into an application that uses the **OpenAI** SDK.
You'll keep calling OpenAI exactly as you do today. Telemetry simply
observes and enriches those calls.
Using the **OpenAI Agents SDK** (`@openai/agents`)? See [OpenAI Agents SDK](/telemetry/frameworks/openai-agents) — agent runs use the Responses API and need a dedicated instrumentation.
***
## Requirements
* A **Latitude account** and **API key**
* A **Latitude project slug**
* A project that uses the **OpenAI SDK**
***
## Steps
```bash npm theme={"theme":{"light":"github-light","dark":"github-dark"}}
npm install @latitude-data/telemetry openai
```
```bash pnpm theme={"theme":{"light":"github-light","dark":"github-dark"}}
pnpm add @latitude-data/telemetry openai
```
```bash yarn theme={"theme":{"light":"github-light","dark":"github-dark"}}
yarn add @latitude-data/telemetry openai
```
```bash bun theme={"theme":{"light":"github-light","dark":"github-dark"}}
bun add @latitude-data/telemetry openai
```
```bash pip theme={"theme":{"light":"github-light","dark":"github-dark"}}
pip install latitude-telemetry
```
```bash uv theme={"theme":{"light":"github-light","dark":"github-dark"}}
uv add latitude-telemetry
```
```bash poetry theme={"theme":{"light":"github-light","dark":"github-dark"}}
poetry add latitude-telemetry
```
```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
import { createOpenAIInstrumentation } from "@latitude-data/telemetry/instrumentations/openai"
import { Latitude, capture } from "@latitude-data/telemetry"
import OpenAI from "openai"
const latitude = new Latitude({
apiKey: process.env.LATITUDE_API_KEY!,
project: process.env.LATITUDE_PROJECT_SLUG!,
instrumentations: [createOpenAIInstrumentation(OpenAI)],
})
await latitude.ready
const openai = new OpenAI()
await capture("generate-support-reply", async () => {
const completion = await openai.chat.completions.create({
model: "gpt-4o",
messages: [{ role: "user", content: "Hello" }],
})
return completion.choices[0].message.content
})
await latitude.shutdown()
```
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
import openai
from openai import OpenAI
from latitude_telemetry import Latitude, capture
latitude = Latitude(
api_key="your-api-key",
project="your-project-slug",
instrumentations={"openai": openai},
)
client = OpenAI()
def generate_support_reply():
completion = client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": "Hello"}],
)
return completion.choices[0].message.content
capture("generate-support-reply", generate_support_reply)
latitude.shutdown()
```
***
## Streaming
When streaming, consume the stream inside `capture()` so the span covers the full operation:
```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
await capture("stream-reply", async () => {
const stream = await openai.chat.completions.create({
model: "gpt-4o",
messages: [{ role: "user", content: input }],
stream: true,
})
for await (const chunk of stream) {
const content = chunk.choices[0]?.delta?.content
if (content) res.write(content)
}
res.end()
})
```
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
def stream_reply():
stream = client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": input}],
stream=True,
)
chunks = []
for chunk in stream:
content = chunk.choices[0].delta.content
if content:
chunks.append(content)
return "".join(chunks)
capture("stream-reply", stream_reply)
```
***
## Seeing Your Traces
Once connected, traces appear automatically in Latitude:
1. Open your **project** in the Latitude dashboard
2. Each execution shows input/output messages, model, token usage, latency, and errors
# Replicate
Source: https://docs.latitude.so/telemetry/providers/replicate
Connect your Replicate-powered application to Latitude for observability.
## Overview
This guide shows you how to integrate **Latitude Telemetry** into an application that uses the **Replicate** SDK.
You'll keep calling Replicate exactly as you do today. Telemetry simply
observes and enriches those calls.
Auto-instrumentation for Replicate is available in the **Python** SDK. For other
languages, send traces via the [OpenTelemetry exporter](/telemetry/otel-exporter).
***
## Requirements
* A **Latitude account** and **API key**
* A **Latitude project slug**
* A project that uses the **Replicate SDK** (`replicate`)
* A **Replicate API token** (set as `REPLICATE_API_TOKEN`)
***
## Steps
```bash pip theme={"theme":{"light":"github-light","dark":"github-dark"}}
pip install latitude-telemetry replicate
```
```bash uv theme={"theme":{"light":"github-light","dark":"github-dark"}}
uv add latitude-telemetry replicate
```
```bash poetry theme={"theme":{"light":"github-light","dark":"github-dark"}}
poetry add latitude-telemetry replicate
```
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
import replicate
from latitude_telemetry import Latitude, capture
latitude = Latitude(
api_key="your-api-key",
project="your-project-slug",
instrumentations={"replicate": replicate},
)
def generate_reply():
output = replicate.run(
"meta/meta-llama-3-8b-instruct",
input={"prompt": "Hello", "max_tokens": 50},
)
return "".join(output)
capture("generate-reply", generate_reply)
latitude.shutdown()
```
***
## Seeing Your Traces
Once connected, traces appear automatically in Latitude:
1. Open your **project** in the Latitude dashboard
2. Each execution shows input/output messages, model, token usage, latency, and errors
# Amazon SageMaker
Source: https://docs.latitude.so/telemetry/providers/sagemaker
Connect your Amazon SageMaker-powered application to Latitude for observability.
## Overview
This guide shows you how to integrate **Latitude Telemetry** into an application that invokes models hosted on **Amazon SageMaker** through `boto3`.
You'll keep calling SageMaker exactly as you do today. Telemetry simply
observes and enriches those calls.
Auto-instrumentation for SageMaker is available in the **Python** SDK. For other
languages, send traces via the [OpenTelemetry exporter](/telemetry/otel-exporter).
***
## Requirements
* A **Latitude account** and **API key**
* A **Latitude project slug**
* A project that uses **boto3** with a deployed SageMaker endpoint
* AWS credentials (`AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, `AWS_REGION`) and a `SAGEMAKER_ENDPOINT_NAME`
***
## Steps
```bash pip theme={"theme":{"light":"github-light","dark":"github-dark"}}
pip install latitude-telemetry boto3
```
```bash uv theme={"theme":{"light":"github-light","dark":"github-dark"}}
uv add latitude-telemetry boto3
```
```bash poetry theme={"theme":{"light":"github-light","dark":"github-dark"}}
poetry add latitude-telemetry boto3
```
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
import json
import os
import boto3
from latitude_telemetry import Latitude, capture
latitude = Latitude(
api_key="your-api-key",
project="your-project-slug",
instrumentations={"sagemaker": boto3},
)
client = boto3.client("sagemaker-runtime", region_name="eu-central-1")
def generate_reply():
response = client.invoke_endpoint(
EndpointName=os.environ["SAGEMAKER_ENDPOINT_NAME"],
ContentType="application/json",
Body=json.dumps({"inputs": "Hello", "parameters": {"max_new_tokens": 50}}),
)
return json.loads(response["Body"].read().decode())
capture("generate-reply", generate_reply)
latitude.shutdown()
```
***
## Seeing Your Traces
Once connected, traces appear automatically in Latitude:
1. Open your **project** in the Latitude dashboard
2. Each execution shows input/output messages, model, token usage, latency, and errors
# Together AI
Source: https://docs.latitude.so/telemetry/providers/together-ai
Connect your Together AI-powered application to Latitude for observability.
## Overview
This guide shows you how to integrate **Latitude Telemetry** into an application that uses the **Together AI** SDK.
You'll keep calling Together AI exactly as you do today. Telemetry simply
observes and enriches those calls.
***
## Requirements
* A **Latitude account** and **API key**
* A **Latitude project slug**
* A project that uses the **Together AI SDK**
***
## Steps
```bash npm theme={"theme":{"light":"github-light","dark":"github-dark"}}
npm install @latitude-data/telemetry together-ai
```
```bash pnpm theme={"theme":{"light":"github-light","dark":"github-dark"}}
pnpm add @latitude-data/telemetry together-ai
```
```bash yarn theme={"theme":{"light":"github-light","dark":"github-dark"}}
yarn add @latitude-data/telemetry together-ai
```
```bash bun theme={"theme":{"light":"github-light","dark":"github-dark"}}
bun add @latitude-data/telemetry together-ai
```
```bash pip theme={"theme":{"light":"github-light","dark":"github-dark"}}
pip install latitude-telemetry
```
```bash uv theme={"theme":{"light":"github-light","dark":"github-dark"}}
uv add latitude-telemetry
```
```bash poetry theme={"theme":{"light":"github-light","dark":"github-dark"}}
poetry add latitude-telemetry
```
```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
import { createTogetherAIInstrumentation } from "@latitude-data/telemetry/instrumentations/togetherai"
import { Latitude, capture } from "@latitude-data/telemetry"
import Together, * as TogetherSDK from "together-ai"
const latitude = new Latitude({
apiKey: process.env.LATITUDE_API_KEY!,
project: process.env.LATITUDE_PROJECT_SLUG!,
instrumentations: [createTogetherAIInstrumentation(TogetherSDK)],
})
await latitude.ready
const client = new Together()
await capture("generate-reply", async () => {
const response = await client.chat.completions.create({
model: "meta-llama/Llama-3-70b-chat-hf",
messages: [{ role: "user", content: "Hello" }],
})
return response.choices[0].message.content
})
await latitude.shutdown()
```
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
import together
from together import Together
from latitude_telemetry import Latitude, capture
latitude = Latitude(
api_key="your-api-key",
project="your-project-slug",
instrumentations={"togetherai": together},
)
client = Together()
def generate_reply():
response = client.chat.completions.create(
model="meta-llama/Llama-3-70b-chat-hf",
messages=[{"role": "user", "content": "Hello"}],
)
return response.choices[0].message.content
capture("generate-reply", generate_reply)
latitude.shutdown()
```
***
## Streaming
When streaming, consume the stream inside `capture()` so the span covers the full operation:
```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
await capture("stream-reply", async () => {
const stream = await client.chat.completions.create({
model: "meta-llama/Llama-3-70b-chat-hf",
messages: [{ role: "user", content: input }],
stream: true,
})
for await (const chunk of stream) {
const content = chunk.choices[0]?.delta?.content
if (content) res.write(content)
}
res.end()
})
```
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
def stream_reply():
stream = client.chat.completions.create(
model="meta-llama/Llama-3-70b-chat-hf",
messages=[{"role": "user", "content": input}],
stream=True,
)
chunks = []
for chunk in stream:
if chunk.choices[0].delta.content:
chunks.append(chunk.choices[0].delta.content)
return "".join(chunks)
capture("stream-reply", stream_reply)
```
***
## Seeing Your Traces
Once connected, traces appear automatically in Latitude:
1. Open your **project** in the Latitude dashboard
2. Each execution shows input/output messages, model, token usage, latency, and errors
# Hugging Face Transformers
Source: https://docs.latitude.so/telemetry/providers/transformers
Connect your Hugging Face Transformers application to Latitude for observability.
## Overview
This guide shows you how to integrate **Latitude Telemetry** into an application that runs models locally with **Hugging Face Transformers**.
You'll keep calling Transformers exactly as you do today. Telemetry simply
observes and enriches those calls.
Auto-instrumentation for Transformers is available in the **Python** SDK. For other
languages, send traces via the [OpenTelemetry exporter](/telemetry/otel-exporter).
***
## Requirements
* A **Latitude account** and **API key**
* A **Latitude project slug**
* A project that uses the **Transformers** library (`transformers`)
* A **Hugging Face token** for gated models (set as `HF_TOKEN`)
***
## Steps
```bash pip theme={"theme":{"light":"github-light","dark":"github-dark"}}
pip install latitude-telemetry transformers torch
```
```bash uv theme={"theme":{"light":"github-light","dark":"github-dark"}}
uv add latitude-telemetry transformers torch
```
```bash poetry theme={"theme":{"light":"github-light","dark":"github-dark"}}
poetry add latitude-telemetry transformers torch
```
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
import transformers
from transformers import pipeline
from latitude_telemetry import Latitude, capture
latitude = Latitude(
api_key="your-api-key",
project="your-project-slug",
instrumentations={"transformers": transformers},
)
def generate_reply():
generator = pipeline("text-generation", model="gpt2", max_new_tokens=50)
result = generator("Hello:")
return result[0]["generated_text"]
capture("generate-reply", generate_reply)
latitude.shutdown()
```
***
## Seeing Your Traces
Once connected, traces appear automatically in Latitude:
1. Open your **project** in the Latitude dashboard
2. Each execution shows input/output messages, model, token usage, latency, and errors
# Vertex AI
Source: https://docs.latitude.so/telemetry/providers/vertex-ai
Connect your Vertex AI-powered application to Latitude for observability.
## Overview
This guide shows you how to integrate **Latitude Telemetry** into an application that uses **Google Cloud Vertex AI**.
You'll keep calling Vertex AI exactly as you do today. Telemetry simply
observes and enriches those calls.
***
## Requirements
* A **Latitude account** and **API key**
* A **Latitude project slug**
* A project that uses the **Vertex AI SDK**
***
## Steps
```bash npm theme={"theme":{"light":"github-light","dark":"github-dark"}}
npm install @latitude-data/telemetry @google-cloud/vertexai
```
```bash pnpm theme={"theme":{"light":"github-light","dark":"github-dark"}}
pnpm add @latitude-data/telemetry @google-cloud/vertexai
```
```bash yarn theme={"theme":{"light":"github-light","dark":"github-dark"}}
yarn add @latitude-data/telemetry @google-cloud/vertexai
```
```bash bun theme={"theme":{"light":"github-light","dark":"github-dark"}}
bun add @latitude-data/telemetry @google-cloud/vertexai
```
```bash pip theme={"theme":{"light":"github-light","dark":"github-dark"}}
pip install latitude-telemetry
```
```bash uv theme={"theme":{"light":"github-light","dark":"github-dark"}}
uv add latitude-telemetry
```
```bash poetry theme={"theme":{"light":"github-light","dark":"github-dark"}}
poetry add latitude-telemetry
```
```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
import { createVertexAIInstrumentation } from "@latitude-data/telemetry/instrumentations/vertexai"
import { Latitude, capture } from "@latitude-data/telemetry"
import { VertexAI } from "@google-cloud/vertexai"
import * as VertexAISDK from "@google-cloud/vertexai"
const latitude = new Latitude({
apiKey: process.env.LATITUDE_API_KEY!,
project: process.env.LATITUDE_PROJECT_SLUG!,
instrumentations: [createVertexAIInstrumentation(VertexAISDK)],
})
await latitude.ready
const vertexAI = new VertexAI({
project: process.env.GCP_PROJECT_ID!,
location: "us-central1",
})
const model = vertexAI.getGenerativeModel({ model: "gemini-1.5-flash" })
await capture("generate-reply", async () => {
const result = await model.generateContent("Hello")
return result.response.candidates?.[0].content.parts[0].text
})
await latitude.shutdown()
```
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
import vertexai
from vertexai.generative_models import GenerativeModel
from latitude_telemetry import Latitude, capture
latitude = Latitude(
api_key="your-api-key",
project="your-project-slug",
instrumentations={"vertexai": vertexai},
)
vertexai.init(project="your-gcp-project", location="us-central1")
model = GenerativeModel("gemini-1.5-flash")
def generate_reply():
response = model.generate_content("Hello")
return response.text
capture("generate-reply", generate_reply)
latitude.shutdown()
```
***
## Seeing Your Traces
Once connected, traces appear automatically in Latitude:
1. Open your **project** in the Latitude dashboard
2. Each execution shows input/output messages, model, token usage, latency, and errors
# IBM watsonx.ai
Source: https://docs.latitude.so/telemetry/providers/watsonx
Connect your IBM watsonx.ai-powered application to Latitude for observability.
## Overview
This guide shows you how to integrate **Latitude Telemetry** into an application that uses **IBM watsonx.ai** (`ibm-watsonx-ai`).
You'll keep calling watsonx.ai exactly as you do today. Telemetry simply
observes and enriches those calls.
Auto-instrumentation for watsonx.ai is available in the **Python** SDK. For other
languages, send traces via the [OpenTelemetry exporter](/telemetry/otel-exporter).
***
## Requirements
* A **Latitude account** and **API key**
* A **Latitude project slug**
* A project that uses the **watsonx.ai SDK** (`ibm-watsonx-ai`)
* watsonx credentials: `WATSONX_API_KEY`, `WATSONX_PROJECT_ID`, and `WATSONX_URL`
***
## Steps
```bash pip theme={"theme":{"light":"github-light","dark":"github-dark"}}
pip install latitude-telemetry ibm-watsonx-ai
```
```bash uv theme={"theme":{"light":"github-light","dark":"github-dark"}}
uv add latitude-telemetry ibm-watsonx-ai
```
```bash poetry theme={"theme":{"light":"github-light","dark":"github-dark"}}
poetry add latitude-telemetry ibm-watsonx-ai
```
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
import os
import ibm_watsonx_ai
from ibm_watsonx_ai.foundation_models import Model
from ibm_watsonx_ai.metanames import GenTextParamsMetaNames as GenParams
from latitude_telemetry import Latitude, capture
latitude = Latitude(
api_key="your-api-key",
project="your-project-slug",
instrumentations={"watsonx": ibm_watsonx_ai},
)
def generate_reply():
model = Model(
model_id="ibm/granite-13b-chat-v2",
credentials={
"url": os.environ.get("WATSONX_URL", "https://us-south.ml.cloud.ibm.com"),
"apikey": os.environ["WATSONX_API_KEY"],
},
project_id=os.environ["WATSONX_PROJECT_ID"],
)
return model.generate_text(prompt="Hello", params={GenParams.MAX_NEW_TOKENS: 50})
capture("generate-reply", generate_reply)
latitude.shutdown()
```
***
## Seeing Your Traces
Once connected, traces appear automatically in Latitude:
1. Open your **project** in the Latitude dashboard
2. Each execution shows input/output messages, model, token usage, latency, and errors
# Python SDK
Source: https://docs.latitude.so/telemetry/python
Instrument Python apps with Latitude Telemetry.
# Python SDK
Use `latitude-telemetry` to send LLM traces from Python applications to Latitude. The SDK is built on OpenTelemetry and can attach to an existing tracing setup when your app already uses one.
## Installation
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
pip install latitude-telemetry
```
Requires Python 3.11+.
## Bootstrap
Initialize Latitude once, before your LLM calls run. Pass the LLM SDK modules your app uses through `instrumentations` so Latitude can auto-instrument them.
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
import openai
from openai import OpenAI
from latitude_telemetry import Latitude
latitude = Latitude(
api_key="your-api-key",
project="your-project-slug",
instrumentations={"openai": openai},
)
client = OpenAI()
response = client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": "Hello"}],
)
latitude.shutdown()
```
`instrumentations` should use the same package module your application imports for the actual LLM call.
## 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
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
import openai
from openai import OpenAI
from latitude_telemetry import Latitude, capture
latitude = Latitude(
api_key="your-api-key",
project="your-project-slug",
instrumentations={"openai": openai},
)
client = OpenAI()
capture(
"handle-user-request",
lambda: client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": user_message}],
),
{
"user_id": "user_123",
"session_id": "session_abc",
"project": "support-agent",
"tags": ["production", "v2-agent"],
"metadata": {"request_id": "req-xyz"},
},
)
latitude.shutdown()
```
`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:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
scope = capture.start(
"handle-user-request",
{
"user_id": "user_123",
"session_id": "session_abc",
"project": "support-agent",
},
)
try:
run_agent()
except Exception as error:
capture.end(scope, error)
raise
capture.end(scope)
```
Nested `capture()` calls inherit parent context and can override local values. Metadata is shallow-merged, and tags are appended and deduplicated.
## Existing OpenTelemetry setup
If your app already has an OpenTelemetry provider, add Latitude to the existing setup and register the LLM instrumentations against that provider.
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
import openai
from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from latitude_telemetry import LatitudeSpanProcessor, register_latitude_instrumentations
provider = TracerProvider()
provider.add_span_processor(LatitudeSpanProcessor("api-key", "project-slug"))
trace.set_tracer_provider(provider)
register_latitude_instrumentations(
instrumentations={"openai": openai},
tracer_provider=provider,
)
```
`LatitudeSpanProcessor` exports spans to Latitude. You still need LLM instrumentations to create those spans.
If you need lower-level OpenTelemetry wiring or a non-Python runtime, see the [OpenTelemetry Exporter](/telemetry/otel-exporter) guide.
## Supported integrations
Set the integration key on `instrumentations` to the SDK module your app imports.
| Integration | Package | Example |
| ------------------ | ------------------------- | ------------------------------------- |
| OpenAI | `openai` | `{"openai": openai}` |
| OpenAI Agents SDK | `openai-agents` | `{"openai-agents": agents}` |
| Anthropic | `anthropic` | `{"anthropic": anthropic}` |
| Amazon Bedrock | `boto3` | `{"bedrock": boto3}` |
| Amazon SageMaker | `boto3` | `{"sagemaker": boto3}` |
| Cohere | `cohere` | `{"cohere": cohere}` |
| LangChain | `langchain-core` | `{"langchain": langchain_core}` |
| LlamaIndex | `llama-index` | `{"llamaindex": llama_index}` |
| CrewAI | `crewai` | `{"crewai": crewai}` |
| Haystack | `haystack-ai` | `{"haystack": haystack}` |
| Together AI | `together` | `{"togetherai": together}` |
| Vertex AI | `google-cloud-aiplatform` | `{"vertexai": vertexai}` |
| Google AI Platform | `google-cloud-aiplatform` | `{"aiplatform": aiplatform}` |
| Google ADK | `google-adk` | `{"google_adk": google.adk}` |
| Google Gemini | `google-genai` | `{"google_generativeai": genai}` |
| Groq | `groq` | `{"groq": groq}` |
| LiteLLM | `litellm` | `{"litellm": litellm}` |
| Mistral AI | `mistralai` | `{"mistralai": mistralai}` |
| Ollama | `ollama` | `{"ollama": ollama}` |
| Replicate | `replicate` | `{"replicate": replicate}` |
| IBM watsonx.ai | `ibm-watsonx-ai` | `{"watsonx": ibm_watsonx_ai}` |
| Aleph Alpha | `aleph-alpha-client` | `{"aleph_alpha": aleph_alpha_client}` |
| Transformers | `transformers` | `{"transformers": transformers}` |
| DSPy | `dspy` | via litellm → `{"litellm": litellm}` |
For provider-specific setup notes, use the provider and framework pages in the Observability sidebar.
## 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:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
latitude = Latitude(
api_key="your-api-key",
project="your-project-slug",
instrumentations={"openai": openai},
)
```
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.
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
import openai
from openai import OpenAI
from latitude_telemetry import Latitude
latitude = Latitude(
api_key="your-api-key",
project="your-project-slug",
instrumentations={"openai": openai},
)
client = OpenAI()
client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": "Hello"}],
)
```
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:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
try:
client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": "Hello"}],
)
latitude.flush()
finally:
latitude.shutdown()
```
#### Wrap the actual LLM call with `capture()`
If you use `capture()`, the instrumented operation must happen inside the callback:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
capture(
"support-agent-turn",
lambda: client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": user_message}],
),
{
"user_id": user.id,
"session_id": conversation.id,
"project": "support-agent",
},
)
```
This will not attach context to the LLM call, because the call happens before `capture()` starts:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
response = client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": user_message}],
)
capture(
"support-agent-turn",
lambda: response,
{
"user_id": user.id,
"session_id": conversation.id,
},
)
```
#### 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.
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
def stream_support_agent_turn():
stream = client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": user_message}],
stream=True,
)
for chunk in stream:
content = chunk.choices[0].delta.content
if content:
print(content, end="")
capture(
"stream-support-agent-turn",
stream_support_agent_turn,
{
"user_id": user.id,
"session_id": conversation.id,
"project": "support-agent",
},
)
```
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
`Latitude(...)` registers OpenTelemetry context propagation when it owns the provider. If you provide your own OpenTelemetry setup, make sure it has working context propagation before Latitude attaches to it.
# Start tracing
Source: https://docs.latitude.so/telemetry/start-tracing
Connect your agent to Latitude and send your first traces.
Use your coding agent to add Latitude tracing to your app. The Latitude skill inspects your codebase, detects your LLM providers and existing OpenTelemetry setup, installs the right SDK, and verifies that traces arrive in your project.
Paste this prompt into Claude Code, Cursor, Windsurf, Codex, OpenCode, or another coding agent:
```text theme={"theme":{"light":"github-light","dark":"github-dark"}}
Install the `latitude-setup` skill from `github.com/latitude-dev/skills`, and use it to add Latitude tracing to this app following best practices.
```
Trigger one agent request or LLM call after the install finishes.
Open your Latitude project and go to **Traces**. The new trace should appear within a few seconds.
Install the [Latitude MCP server](../getting-started/mcp) so your agent can access your Latitude workspace, find the right project, and verify the setup.
## Manual setup
If you are not using a coding agent, start with the SDK for your runtime:
Install `@latitude-data/telemetry` and instrument TypeScript or JavaScript
apps.
Install `latitude-telemetry` and instrument Python apps.
For other languages, use the [OpenTelemetry exporter](../telemetry/otel-exporter).
## Next steps
* Explore traces in [Observability](../observability/overview)
* Add provider-specific instrumentation from the Observability sidebar
* Trace your agent's long-term memory with [Memory tracing](./memory)
* Learn how traces become [scores](../scores/overview), [signals](../signals/overview), and [evaluations](../evaluations/overview)
# TypeScript SDK
Source: https://docs.latitude.so/telemetry/typescript
Instrument TypeScript and JavaScript apps with Latitude Telemetry.
# 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.
## Installation
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
npm install @latitude-data/telemetry openai
```
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`.
```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
import { createOpenAIInstrumentation } from "@latitude-data/telemetry/instrumentations/openai"
import { Latitude } from "@latitude-data/telemetry"
import OpenAI from "openai"
const latitude = new Latitude({
apiKey: process.env.LATITUDE_API_KEY!,
project: process.env.LATITUDE_PROJECT_SLUG!,
instrumentations: [createOpenAIInstrumentation(OpenAI)],
})
await latitude.ready
const client = new OpenAI()
const response = await client.chat.completions.create({
model: "gpt-4o",
messages: [{ role: "user", content: "Hello" }],
})
await latitude.shutdown()
```
`new Latitude()` returns immediately. Await `latitude.ready` before creating LLM clients or making calls.
## 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
```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
import { createOpenAIInstrumentation } from "@latitude-data/telemetry/instrumentations/openai"
import { Latitude, capture } from "@latitude-data/telemetry"
import OpenAI from "openai"
const latitude = new Latitude({
apiKey: process.env.LATITUDE_API_KEY!,
project: process.env.LATITUDE_PROJECT_SLUG!,
instrumentations: [createOpenAIInstrumentation(OpenAI)],
})
await latitude.ready
const client = new OpenAI()
await capture(
"handle-user-request",
async () => {
return client.chat.completions.create({
model: "gpt-4o",
messages: [{ role: "user", content: userMessage }],
})
},
{
userId: "user_123",
sessionId: "session_abc",
project: "support-agent",
tags: ["production", "v2-agent"],
metadata: { requestId: "req-xyz" },
},
)
await latitude.shutdown()
```
`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:
```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
const scope = capture.start("handle-user-request", {
userId: "user_123",
sessionId: "session_abc",
project: "support-agent",
})
try {
await runAgent()
} catch (error) {
capture.end(scope, error)
throw error
}
capture.end(scope)
```
Nested `capture()` calls inherit parent context and can override local values. Metadata is shallow-merged, and tags are appended and deduplicated.
## 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.
```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
import { createOpenAIInstrumentation } from "@latitude-data/telemetry/instrumentations/openai"
import { Latitude } from "@latitude-data/telemetry"
import * as Sentry from "@sentry/node"
import OpenAI from "openai"
Sentry.init({
dsn: process.env.SENTRY_DSN!,
tracesSampleRate: 1.0,
})
const latitude = new Latitude({
apiKey: process.env.LATITUDE_API_KEY!,
project: process.env.LATITUDE_PROJECT_SLUG!,
instrumentations: [createOpenAIInstrumentation(OpenAI)],
})
```
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](/telemetry/otel-exporter) guide.
## Supported integrations
Set the integration key on `instrumentations` to the SDK module your app imports.
| Integration | Package | Example |
| ------------------ | --------------------------------- | -------------------------------------- |
| OpenAI | `openai` | `{ openai: OpenAI }` |
| OpenAI Agents SDK | `@openai/agents` | `{ "openai-agents": OpenAIAgentsSDK }` |
| Anthropic | `@anthropic-ai/sdk` | `{ anthropic: AnthropicSDK }` |
| Amazon Bedrock | `@aws-sdk/client-bedrock-runtime` | `{ bedrock: BedrockSDK }` |
| Cohere | `cohere-ai` | `{ cohere: CohereSDK }` |
| LangChain | `langchain` | `{ langchain: LangChain }` |
| LlamaIndex | `llamaindex` | `{ llamaindex: LlamaIndex }` |
| Together AI | `together-ai` | `{ togetherai: TogetherSDK }` |
| Vertex AI | `@google-cloud/vertexai` | `{ vertexai: VertexAISDK }` |
| Google AI Platform | `@google-cloud/aiplatform` | `{ aiplatform: AIPlatformSDK }` |
For provider-specific setup notes, use the provider and framework pages in the Observability sidebar.
## 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:
```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
const latitude = new Latitude({
apiKey: process.env.LATITUDE_API_KEY!,
project: process.env.LATITUDE_PROJECT_SLUG!,
instrumentations: [createOpenAIInstrumentation(OpenAI)],
})
```
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.
```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
import { createOpenAIInstrumentation } from "@latitude-data/telemetry/instrumentations/openai"
import { Latitude } from "@latitude-data/telemetry"
import OpenAI from "openai"
const latitude = new Latitude({
apiKey: process.env.LATITUDE_API_KEY!,
project: process.env.LATITUDE_PROJECT_SLUG!,
instrumentations: [createOpenAIInstrumentation(OpenAI)],
})
await latitude.ready
const client = new OpenAI()
await client.chat.completions.create({
model: "gpt-4o",
messages: [{ role: "user", content: "Hello" }],
})
```
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:
```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
try {
await client.chat.completions.create({
model: "gpt-4o",
messages: [{ role: "user", content: "Hello" }],
})
await latitude.flush()
} finally {
await latitude.shutdown()
}
```
#### Wrap the actual LLM call with `capture()`
If you use `capture()`, the instrumented operation must happen inside the callback:
```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
await capture(
"support-agent-turn",
async () => {
return client.chat.completions.create({
model: "gpt-4o",
messages: [{ role: "user", content: userMessage }],
})
},
{
userId: user.id,
sessionId: conversation.id,
project: "support-agent",
},
)
```
This will not attach context to the LLM call, because the call happens before `capture()` starts:
```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
const response = await client.chat.completions.create({
model: "gpt-4o",
messages: [{ role: "user", content: userMessage }],
})
await capture("support-agent-turn", async () => response, {
userId: user.id,
sessionId: conversation.id,
})
```
#### 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.
```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
await capture(
"stream-support-agent-turn",
async () => {
const stream = await client.chat.completions.create({
model: "gpt-4o",
messages: [{ role: "user", content: userMessage }],
stream: true,
})
for await (const chunk of stream) {
const content = chunk.choices[0]?.delta?.content
if (content) {
process.stdout.write(content)
}
}
},
{
userId: user.id,
sessionId: conversation.id,
project: "support-agent",
},
)
```
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.
# Regression testing
Source: https://docs.latitude.so/test-and-fix/regression-testing
Replay a dataset of real traces against your agent and check the results with the same evaluations that monitor production, so fixed failures cannot quietly return.
**Where this fits:** This is the verification step of **Refine**. It takes a [dataset](../datasets/overview) built from a fixed [signal](../signals/overview) and proves the fix holds before and after you ship.
A **regression test** replays a set of known inputs against your agent and checks the results, so a failure you already fixed cannot return unnoticed. In Latitude, the inputs come from a [dataset](../datasets/overview) of real traces, and the checks reuse the same [evaluations](../evaluations/overview) that monitor production, so your test quality bar matches your production quality bar.
## Fix and verify with your coding agent
The fastest path today pairs the [MCP server](../getting-started/mcp) with a dataset, so your coding agent does the work:
Connect your coding agent (Claude, Cursor, and others) to Latitude through the [MCP server](../getting-started/mcp). It can read the failing [signal](../signals/overview), inspect the example traces, and propose a fix in the same session.
Turn the traces behind the signal into a [dataset](../datasets/overview), the seed of your regression test. Your agent can do this through the MCP, or you can [add the traces](../datasets/add-traces) from the UI.
Record what the agent should have done by [adding expected output](../datasets/expected-output) to the rows you want to check precisely.
Run your agent against each row's input to produce fresh outputs, then run the signal's [evaluations](../evaluations/overview) against them. The same check that found the failure in production now verifies the fix.
Pass when the results meet your quality bar; fail to block a regression. Re-run whenever the agent, prompts, tools, or models change.
## Run it in CI
You can drive a regression test from a dataset in your own pipeline:
* **Export** a [dataset](../datasets/overview) as CSV and replay its inputs in your own test harness.
* Submit the results back as [scores](../scores/overview) through the Scores API, so regression results live alongside your production data, and gate the build on the outcome.
## Next step
* [Datasets](../datasets/overview): turn the traces behind a signal into a reusable test set.