Skip to content
Open
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
cfa6ceb
feat: add Braintrust tracer implementation
dpguthrie Feb 9, 2026
06963f5
docs: add Braintrust monitoring integration page
dpguthrie Feb 9, 2026
fe49c18
feat: register Braintrust tracer in TracingService
dpguthrie Feb 9, 2026
4f1f80a
docs: add Braintrust to monitoring sidebar
dpguthrie Feb 9, 2026
17cb447
refactor: remove braintrust-langchain dep, inline LangChain callback …
dpguthrie Feb 9, 2026
5c3ed29
[autofix.ci] apply automated fixes
autofix-ci[bot] Feb 9, 2026
5b05217
refactor: simplify tracer — return None from get_langchain_callback, …
dpguthrie Feb 9, 2026
aadc3a2
feat: add optional braintrust-langchain support for deep LangChain tr…
dpguthrie Feb 9, 2026
c8bd542
test: add braintrust tracer to mock_tracers fixture and assertions
dpguthrie Feb 9, 2026
cfb8677
docs: add docstrings to all BraintrustTracer methods for 100% coverage
dpguthrie Feb 9, 2026
d2a9ae3
test: add comprehensive unit tests for BraintrustTracer
dpguthrie Feb 9, 2026
10058a5
[autofix.ci] apply automated fixes
autofix-ci[bot] Feb 9, 2026
e948e91
style: fix ruff SIM117, RUF059, FBT003 lint errors in test file
dpguthrie Feb 9, 2026
f63db8c
[autofix.ci] apply automated fixes
autofix-ci[bot] Feb 9, 2026
ce5f5ca
style: fix ruff I001 import sorting and RET504 unnecessary assignment
dpguthrie Feb 9, 2026
1e8783c
[autofix.ci] apply automated fixes
autofix-ci[bot] Feb 9, 2026
6e02eca
fix: use sys.modules mocking so tests pass without braintrust installed
dpguthrie Feb 9, 2026
b40b5dd
[autofix.ci] apply automated fixes
autofix-ci[bot] Feb 9, 2026
921aedc
Merge branch 'main' into feat/braintrust-tracing-integration
dpguthrie Feb 10, 2026
1736e4f
Merge branch 'main' into feat/braintrust-tracing-integration
dpguthrie Feb 11, 2026
3b8a496
[autofix.ci] apply automated fixes
autofix-ci[bot] Feb 11, 2026
a49d9c5
fix: forward user_id and session_id to BraintrustTracer
dpguthrie Feb 11, 2026
ca56ac3
fix: add frontmatter description/sidebar_position and code block titles
dpguthrie Feb 11, 2026
aa718f3
fix(test): Skip Tavily API key fill when global variable is loaded …
Cristhianzl Feb 11, 2026
7f2e2d1
feat: add smart column ordering and clean output toggle to Split Text…
viktoravelino Feb 11, 2026
1dd0a4d
fix: add iter method to noopresult (#11517)
HzaRashid Feb 13, 2026
8e62c45
fix-le-354: Fixing 404 error response (#11760)
dkaushik94 Feb 13, 2026
09bcabb
fix: add public config endpoint to fix shareable playground grey scre…
RamGopalSrikar Feb 17, 2026
b05ebad
fix: improve condition check for streaming token callback in handle_o…
ricofurtado Feb 17, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 92 additions & 0 deletions docs/docs/Develop/integrations-braintrust.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
---
title: Braintrust
slug: /integrations-braintrust
---
Comment on lines +1 to +6

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Frontmatter is missing description and sidebar_position.

As per coding guidelines, YAML frontmatter must include title, description, and sidebar_position.

Proposed fix
 ---
 title: Braintrust
 slug: /integrations-braintrust
+description: Configure Langflow to send tracing data to Braintrust for debugging and analyzing flow executions.
+sidebar_position: 1
 ---

As per coding guidelines: "Markdown files must include YAML frontmatter with title, description, and sidebar_position".

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
---
title: Braintrust
slug: /integrations-braintrust
---
---
title: Braintrust
slug: /integrations-braintrust
description: Configure Langflow to send tracing data to Braintrust for debugging and analyzing flow executions.
sidebar_position: 1
---
🤖 Prompt for AI Agents
In `@docs/docs/Develop/integrations-braintrust.mdx` around lines 1 - 4, The MDX
frontmatter is missing required fields; update the YAML frontmatter for the
Braintrust doc by adding the missing `description` and `sidebar_position` keys
alongside the existing `title` (e.g., set a short descriptive string for
`description` and an integer for `sidebar_position`) so the frontmatter contains
`title`, `description`, and `sidebar_position`.


import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';

[Braintrust](https://www.braintrust.com) is an end-to-end platform for building AI applications, providing logging, tracing, evaluation, and prompt management. Braintrust helps developers debug, analyze, and optimize their AI systems with detailed span trees, token metrics, and scoring.

This guide explains how to configure Langflow to collect tracing data about your flow executions and automatically send the data to Braintrust.

## Prerequisites

- A [Braintrust account](https://www.braintrust.com) and [API key](https://www.braintrust.com/docs/reference/api-keys)
- A [running Langflow server](/get-started-installation) with a [flow](/concepts-flows) that you want to trace

:::tip
If you need a flow to test the Braintrust integration, see the [Langflow quickstart](/get-started-quickstart).
:::

## Set Braintrust credentials as environment variables {#braintrust-credentials}

1. Get your API key from your [Braintrust settings](https://www.braintrust.com/app/settings/api-keys).

2. Set your Braintrust API key as an environment variable in the same environment where you run Langflow.

<Tabs>
<TabItem value="linux" label="Linux/macOS" default>

```bash
export BRAINTRUST_API_KEY=YOUR_API_KEY
```

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Code blocks are missing the title attribute.

Per coding guidelines, code blocks must include a title attribute and specify the language (e.g., ```bash title="Terminal"). All bash code blocks in this file are missing title.

As per coding guidelines: "Code blocks must include a title attribute and specify the language".

Also applies to: 38-40, 52-58, 63-65, 75-77

🤖 Prompt for AI Agents
In `@docs/docs/Develop/integrations-braintrust.mdx` around lines 31 - 33, Several
bash code blocks (instances of the triple-backtick fences that start with
"```bash") are missing the required title attribute; update each code fence
(e.g., the ones currently written as "```bash") to include a title like
title="Terminal" (so they read e.g. ```bash title="Terminal") or another
descriptive title per block; ensure you change every bash block in the file
(including the blocks noted around the earlier examples) so each specifies both
the language and a title attribute.


</TabItem>
<TabItem value="windows" label="Windows">

```bash
set BRAINTRUST_API_KEY=YOUR_API_KEY
```

</TabItem>
</Tabs>

Replace `YOUR_API_KEY` with your Braintrust API key.

3. Optionally, set additional configuration:

<Tabs>
<TabItem value="linux" label="Linux/macOS" default>

```bash
# Override the default Braintrust API URL (defaults to https://api.braintrust.dev)
export BRAINTRUST_API_URL=https://api.braintrust.dev

# Set a project name (defaults to "Langflow")
export BRAINTRUST_PROJECT=my-langflow-project
```

</TabItem>
<TabItem value="windows" label="Windows">

```bash
set BRAINTRUST_API_URL=https://api.braintrust.dev
set BRAINTRUST_PROJECT=my-langflow-project
```

</TabItem>
</Tabs>

## Start Langflow and view traces in Braintrust

1. Start Langflow in the same environment where you set the Braintrust environment variables:

```bash
uv run langflow run
```

2. Run a flow.

Langflow automatically collects and sends tracing data about the flow execution to Braintrust. Each flow run creates a trace with nested spans for every component execution and LangChain operation, including LLM calls with token usage metrics.

3. View the collected data in your [Braintrust dashboard](https://www.braintrust.com/app).

## Disable Braintrust tracing

To disable the Braintrust integration, remove the [Braintrust environment variables](#braintrust-credentials), and then restart Langflow.

## See also

* [Braintrust documentation](https://www.braintrust.com/docs)
* [Braintrust GitHub repository](https://github.com/braintrustdata/braintrust-sdk)
1 change: 1 addition & 0 deletions docs/sidebars.js
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ module.exports = {
label: "Monitoring",
items: [
"Develop/integrations-arize",
"Develop/integrations-braintrust",
"Develop/integrations-langfuse",
"Develop/integrations-langsmith",
"Develop/integrations-langwatch",
Expand Down
240 changes: 240 additions & 0 deletions src/backend/base/langflow/services/tracing/braintrust.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,240 @@
"""Braintrust tracer for Langflow.

Implements Langflow's BaseTracer interface to send component-level
traces to Braintrust.

Only depends on the ``braintrust`` package.

Activation: set the BRAINTRUST_API_KEY environment variable.
Optional: BRAINTRUST_API_URL, BRAINTRUST_PROJECT.
"""

from __future__ import annotations

import os
import types
from typing import TYPE_CHECKING, Any

from langchain_core.documents import Document
from langchain_core.messages import BaseMessage, HumanMessage, SystemMessage
from lfx.log.logger import logger
from typing_extensions import override

from langflow.schema.data import Data
from langflow.schema.message import Message
from langflow.services.tracing.base import BaseTracer

if TYPE_CHECKING:
from collections.abc import Sequence
from uuid import UUID

from langchain.callbacks.base import BaseCallbackHandler
from lfx.graph.vertex.base import Vertex

from langflow.services.tracing.schema import Log


class BraintrustTracer(BaseTracer):
"""Traces Langflow flow executions to Braintrust.

This tracer creates a root span for each flow run and child spans for
each component execution. Each Langflow component (prompt, model,
tool, retriever, etc.) is captured as a span with its inputs, outputs,
metadata, and any errors.

Only depends on the ``braintrust`` package.
"""

flow_id: str

def __init__(
self,
trace_name: str,
trace_type: str,
project_name: str,
trace_id: UUID,
user_id: str | None = None,
session_id: str | None = None,
) -> None:
self.trace_name = trace_name
self.trace_type = trace_type
self.trace_id = trace_id
self.user_id = user_id
self.session_id = session_id
self.flow_id = trace_name.split(" - ")[-1]
self.spans: dict[str, Any] = {}

config = self._get_config()
self._ready: bool = self._setup_braintrust(config, project_name) if config else False
Comment on lines +57 to +91

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Constructor does not accept user_id / session_id from the service layer.

The __init__ signature accepts user_id and session_id, and stores them in metadata, but _initialize_braintrust_tracer in service.py (line 233) does not pass them. Compare with langfuse and opik tracers which do. This means user_id and session_id will always be None in the root span metadata.

🤖 Prompt for AI Agents
In `@src/backend/base/langflow/services/tracing/braintrust.py` around lines 50 -
68, The Braintrust tracer constructor (__init__ in braintrust.py) accepts
user_id and session_id but _initialize_braintrust_tracer in service.py does not
forward them, so root span metadata always has None; update
_initialize_braintrust_tracer to accept (or extract) the service-layer user_id
and session_id and pass them into the Braintrust tracer constructor when
instantiating the tracer (same pattern used by langfuse/opik tracers) so the
root span metadata includes the correct user_id and session_id.


@property
def ready(self) -> bool:
return self._ready

# ------------------------------------------------------------------
# Setup
# ------------------------------------------------------------------

def _setup_braintrust(self, config: dict[str, Any], project_name: str) -> bool:
try:
from braintrust import init_logger

project = config.pop("project", None) or project_name or "Langflow"
self._logger = init_logger(
project=project,
api_key=config.get("api_key"),
app_url=config.get("api_url"),
)

# Create a root span for this flow execution
self._root_span = self._logger.start_span(
name=self.flow_id,
input={
"trace_name": self.trace_name,
"trace_type": self.trace_type,
},
metadata={
"langflow_trace_id": str(self.trace_id),
"langflow_trace_name": self.trace_name,
"user_id": self.user_id,
"session_id": self.session_id,
"created_from": "langflow",
},
)
except ImportError:
logger.exception("Could not import braintrust. Please install it with `pip install braintrust`.")
return False
except Exception as e: # noqa: BLE001
logger.debug(f"Error setting up Braintrust tracer: {e}")
return False

return True

# ------------------------------------------------------------------
# BaseTracer interface
# ------------------------------------------------------------------

@override
def add_trace(
self,
trace_id: str,
trace_name: str,
trace_type: str,
inputs: dict[str, Any],
metadata: dict[str, Any] | None = None,
vertex: Vertex | None = None,
) -> None:
if not self._ready:
return

name = trace_name.removesuffix(f" ({trace_id})")
processed_inputs = self._convert_to_loggable(inputs) if inputs else {}
processed_metadata = self._convert_to_loggable(metadata) if metadata else {}

processed_metadata["from_langflow_component"] = True
processed_metadata["component_id"] = trace_id
if trace_type:
processed_metadata["trace_type"] = trace_type

span = self._root_span.start_span(
name=name,
input=processed_inputs,
metadata=processed_metadata,
)

self.spans[trace_id] = span

@override
def end_trace(
self,
trace_id: str,
trace_name: str,
outputs: dict[str, Any] | None = None,
error: Exception | None = None,
logs: Sequence[Log | dict] = (),
) -> None:
if not self._ready:
return

span = self.spans.pop(trace_id, None)
if span is None:
logger.warning(f"Braintrust: no span found for trace_id={trace_id}")
return

output: dict[str, Any] = {}
output |= self._convert_to_loggable(outputs) if outputs else {}
if logs:
output["logs"] = [self._convert_to_loggable(log) if isinstance(log, dict) else str(log) for log in logs]

span.log(
output=output,
error=str(error) if error else None,
)
span.end()

@override
def end(
self,
inputs: dict[str, Any],
outputs: dict[str, Any],
error: Exception | None = None,
metadata: dict[str, Any] | None = None,
) -> None:
if not self._ready:
return

self._root_span.log(
input=self._convert_to_loggable(inputs) if inputs else {},
output=self._convert_to_loggable(outputs) if outputs else {},
error=str(error) if error else None,
metadata=self._convert_to_loggable(metadata) if metadata else {},
)
self._root_span.end()

@override
def get_langchain_callback(self) -> BaseCallbackHandler | None:
return None

# ------------------------------------------------------------------
# Helpers
# ------------------------------------------------------------------

def _convert_to_loggable(self, value: Any) -> Any:
"""Recursively convert Langflow/LangChain types to JSON-serializable values."""
if isinstance(value, dict):
return {str(k): self._convert_to_loggable(v) for k, v in value.items() if k is not None}
if isinstance(value, list):
return [self._convert_to_loggable(v) for v in value]
if isinstance(value, Message):
return value.text
if isinstance(value, Data):
return value.get_text()
if isinstance(value, (BaseMessage, HumanMessage, SystemMessage)):
return value.content
if isinstance(value, Document):
return value.page_content
if isinstance(value, (types.GeneratorType, types.NoneType)):
return str(value)
return value

@staticmethod
def _get_config() -> dict[str, Any]:
"""Read Braintrust configuration from environment variables.

Returns an empty dict if the required BRAINTRUST_API_KEY is not set.
"""
api_key = os.getenv("BRAINTRUST_API_KEY")
if not api_key:
return {}

config: dict[str, Any] = {"api_key": api_key}

api_url = os.getenv("BRAINTRUST_API_URL")
if api_url:
config["api_url"] = api_url

project = os.getenv("BRAINTRUST_PROJECT")
if project:
config["project"] = project

return config
18 changes: 18 additions & 0 deletions src/backend/base/langflow/services/tracing/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,12 @@ def _get_traceloop_tracer():
return TraceloopTracer


def _get_braintrust_tracer():
from langflow.services.tracing.braintrust import BraintrustTracer

return BraintrustTracer


trace_context_var: ContextVar[TraceContext | None] = ContextVar("trace_context", default=None)
component_context_var: ContextVar[ComponentTraceContext | None] = ContextVar("component_trace_context", default=None)

Expand Down Expand Up @@ -220,6 +226,17 @@ def _initialize_traceloop_tracer(self, trace_context: TraceContext) -> None:
session_id=trace_context.session_id,
)

def _initialize_braintrust_tracer(self, trace_context: TraceContext) -> None:
if self.deactivated:
return
braintrust_tracer = _get_braintrust_tracer()
trace_context.tracers["braintrust"] = braintrust_tracer(
trace_name=trace_context.run_name,
trace_type="chain",
project_name=trace_context.project_name,
trace_id=trace_context.run_id,
)
Comment on lines +229 to +240

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

user_id and session_id are not forwarded to the Braintrust tracer.

Other tracers that support user/session context (e.g., langfuse at line 188–189, opik at line 212–213, traceloop at line 225–226) pass user_id and session_id from trace_context. The Braintrust tracer's __init__ accepts these parameters and includes them in root span metadata, but they will always be None because they aren't passed here.

Proposed fix
         braintrust_tracer = _get_braintrust_tracer()
         trace_context.tracers["braintrust"] = braintrust_tracer(
             trace_name=trace_context.run_name,
             trace_type="chain",
             project_name=trace_context.project_name,
             trace_id=trace_context.run_id,
+            user_id=trace_context.user_id,
+            session_id=trace_context.session_id,
         )
🤖 Prompt for AI Agents
In `@src/backend/base/langflow/services/tracing/service.py` around lines 229 -
238, The Braintrust tracer is not receiving user_id and session_id from the
TraceContext, so root span metadata will always be None; update
_initialize_braintrust_tracer to pass trace_context.user_id and
trace_context.session_id into the braintrust_tracer call (alongside trace_name,
trace_type, project_name, trace_id) so the tracer's constructor receives those
values (TraceContext and braintrust_tracer are the relevant symbols to update).


async def start_tracers(
self,
run_id: UUID,
Expand Down Expand Up @@ -247,6 +264,7 @@ async def start_tracers(
self._initialize_arize_phoenix_tracer(trace_context)
self._initialize_opik_tracer(trace_context)
self._initialize_traceloop_tracer(trace_context)
self._initialize_braintrust_tracer(trace_context)
except Exception as e: # noqa: BLE001
await logger.adebug(f"Error initializing tracers: {e}")

Expand Down
Loading