Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 5 additions & 0 deletions .changeset/quiet-sandboxes-trace.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"braintrust": minor
---

Add automatic tracing for E2B sandbox creation, command execution, and Code Interpreter execution, including execution errors and an `e2b` integration opt-out.
8 changes: 8 additions & 0 deletions js/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,14 @@ If you use TypeScript or other transpilation plugins, place the Braintrust plugi

For deeper details, see the [auto-instrumentation architecture docs](src/auto-instrumentations/README.md).

### E2B

Auto-instrumentation supports `e2b` 2.9+ and `@e2b/code-interpreter` 2.2+ within their 2.x release lines. With the runtime hook or a bundler plugin enabled, `Sandbox.create()`, `sandbox.commands.run()`, and `sandbox.runCode()` produce tool spans. Initialize a Braintrust logger as usual before making these calls.

Spans capture command/code inputs, execution outputs and errors, and selected operation metadata. Connection credentials, environment variables, and callbacks are not logged. Background command spans finish when the SDK returns the command handle; they do not track the background process to completion.

Set `BRAINTRUST_DISABLE_INSTRUMENTATION=e2b` to disable both E2B packages, or use `configureInstrumentation({ integrations: { e2b: false } })` before initializing the SDK.

## Migration Guides

### Upgrading from 2.x to 3.x
Expand Down
2 changes: 2 additions & 0 deletions js/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,7 @@
"license": "MIT",
"devDependencies": {
"@anthropic-ai/sdk": "^0.60.0",
"@e2b/code-interpreter": "2.2.0",
"@nodelib/fs.walk": "^1.2.8",
"@types/argparse": "^2.0.14",
"@types/async": "^3.2.24",
Expand All @@ -207,6 +208,7 @@
"ai": "^6.0.0",
"async": "^3.2.5",
"cross-env": "^7.0.3",
"e2b": "2.9.0",
"eslint": "^9.39.2",
"eslint-plugin-es-x": "9.7.0",
"eslint-plugin-n": "^18.2.1",
Expand Down
2 changes: 2 additions & 0 deletions js/src/auto-instrumentations/configs/all.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import { strandsAgentSDKConfigs } from "./strands-agent-sdk";
import { elevenLabsConfigs } from "./elevenlabs";
import { voyageAIConfigs } from "./voyageai";
import { typeSafeConfigs } from "./typesafe";
import { e2bConfigs } from "./e2b";

interface InstrumentationConfigGroup {
integrations: readonly (keyof InstrumentationIntegrationsConfig)[];
Expand All @@ -46,6 +47,7 @@ interface InstrumentationConfigGroup {
const defaultInstrumentationConfigGroups: readonly InstrumentationConfigGroup[] =
[
{ integrations: ["openai"], configs: openaiConfigs },
{ integrations: ["e2b"], configs: e2bConfigs },
{
integrations: ["openaiCodexSDK"],
configs: openAICodexConfigs,
Expand Down
39 changes: 39 additions & 0 deletions js/src/auto-instrumentations/configs/e2b.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import type { InstrumentationConfig } from "../orchestrion-js";
import {
e2bChannels,
e2bCodeInterpreterChannels,
} from "../../instrumentation/plugins/e2b-channels";

export const e2bConfigs: InstrumentationConfig[] = [
"dist/index.js",
"dist/index.mjs",
].flatMap((filePath) => [
{
channelName: e2bChannels.create.channelName,
module: { name: "e2b", versionRange: ">=2.9.0 <3.0.0", filePath },
functionQuery: {
className: "Sandbox",
methodName: "create",
kind: "Async",
},
},
{
channelName: e2bChannels.run.channelName,
module: { name: "e2b", versionRange: ">=2.9.0 <3.0.0", filePath },
functionQuery: { className: "Commands", methodName: "run", kind: "Async" },
},
{
// Code Interpreter inherits create() and commands from e2b.
channelName: e2bCodeInterpreterChannels.runCode.channelName,
module: {
name: "@e2b/code-interpreter",
versionRange: ">=2.2.0 <3.0.0",
filePath,
},
functionQuery: {
className: "Sandbox",
methodName: "runCode",
kind: "Async",
},
},
]);
22 changes: 22 additions & 0 deletions js/src/instrumentation/braintrust-plugin.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { PiCodingAgentPlugin } from "./plugins/pi-coding-agent-plugin";
import { StrandsAgentSDKPlugin } from "./plugins/strands-agent-sdk-plugin";
import { VoyageAIPlugin } from "./plugins/voyageai-plugin";
import { TypeSafePlugin } from "./plugins/typesafe-plugin";
import { E2BPlugin } from "./plugins/e2b-plugin";
import { CloudflareAIChatPlugin } from "./plugins/cloudflare-ai-chat-plugin";
import { CloudflareAgentsPlugin } from "./plugins/cloudflare-agents-plugin";

Expand Down Expand Up @@ -143,6 +144,10 @@ vi.mock("./plugins/typesafe-plugin", () => ({
TypeSafePlugin: createPluginClassMock(),
}));

vi.mock("./plugins/e2b-plugin", () => ({
E2BPlugin: createPluginClassMock(),
}));

vi.mock("./plugins/cloudflare-ai-chat-plugin", () => ({
CloudflareAIChatPlugin: createPluginClassMock(),
}));
Expand All @@ -157,6 +162,23 @@ describe("BraintrustPlugin", () => {
});

describe("sub-plugin creation (default behavior)", () => {
it("enables and disables E2B with the parent plugin", () => {
const plugin = new BraintrustPlugin();
plugin.enable();
expect(E2BPlugin).toHaveBeenCalledTimes(1);
const instance = vi.mocked(E2BPlugin).mock.results[0].value;
expect(instance.enable).toHaveBeenCalledTimes(1);
plugin.disable();
expect(instance.disable).toHaveBeenCalledTimes(1);
});

it("respects the E2B integration opt-out", () => {
const plugin = new BraintrustPlugin({ integrations: { e2b: false } });
plugin.enable();
expect(E2BPlugin).not.toHaveBeenCalled();
plugin.disable();
});

it("should create and enable OpenAI plugin by default", () => {
const plugin = new BraintrustPlugin();
plugin.enable();
Expand Down
10 changes: 10 additions & 0 deletions js/src/instrumentation/braintrust-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import { StrandsAgentSDKPlugin } from "./plugins/strands-agent-sdk-plugin";
import { ElevenLabsPlugin } from "./plugins/elevenlabs-plugin";
import { VoyageAIPlugin } from "./plugins/voyageai-plugin";
import { TypeSafePlugin } from "./plugins/typesafe-plugin";
import { E2BPlugin } from "./plugins/e2b-plugin";
import { CloudflareAIChatPlugin } from "./plugins/cloudflare-ai-chat-plugin";
import { CloudflareAgentsPlugin } from "./plugins/cloudflare-agents-plugin";
import type { InstrumentationIntegrationsConfig } from "./config";
Expand Down Expand Up @@ -91,6 +92,7 @@ export class BraintrustPlugin extends BasePlugin {
private elevenLabsPlugin: ElevenLabsPlugin | null = null;
private voyageAIPlugin: VoyageAIPlugin | null = null;
private typeSafePlugin: TypeSafePlugin | null = null;
private e2bPlugin: E2BPlugin | null = null;
private cloudflareAIChatPlugin: CloudflareAIChatPlugin | null = null;
private cloudflareAgentsPlugin: CloudflareAgentsPlugin | null = null;

Expand Down Expand Up @@ -217,6 +219,10 @@ export class BraintrustPlugin extends BasePlugin {
this.typeSafePlugin = new TypeSafePlugin();
this.typeSafePlugin.enable();
}
if (integrations.e2b !== false) {
this.e2bPlugin = new E2BPlugin();
this.e2bPlugin.enable();
}

if (integrations.groq !== false) {
this.groqPlugin = new GroqPlugin();
Expand Down Expand Up @@ -395,6 +401,10 @@ export class BraintrustPlugin extends BasePlugin {
this.typeSafePlugin.disable();
this.typeSafePlugin = null;
}
if (this.e2bPlugin) {
this.e2bPlugin.disable();
this.e2bPlugin = null;
}

if (this.groqPlugin) {
this.groqPlugin.disable();
Expand Down
4 changes: 4 additions & 0 deletions js/src/instrumentation/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ export interface InstrumentationIntegrationsConfig {
langsmith?: boolean;
voyageai?: boolean;
typesafe?: boolean;
e2b?: boolean;
elevenlabs?: boolean;
}

Expand Down Expand Up @@ -161,6 +162,8 @@ const envIntegrationAliases: Record<
"voyage-ai": "voyageai",
voyageai: "voyageai",
typesafe: "typesafe",
e2b: "e2b",
"@e2b/code-interpreter": "e2b",
"typesafe-ai": "typesafe",
"@typesafe-ai/sdk": "typesafe",
elevenlabs: "elevenlabs",
Expand Down Expand Up @@ -207,6 +210,7 @@ export function getDefaultInstrumentationIntegrations(): Record<
langsmith: true,
voyageai: true,
typesafe: true,
e2b: true,
elevenlabs: true,
piCodingAgent: true,
strandsAgentSDK: true,
Expand Down
28 changes: 28 additions & 0 deletions js/src/instrumentation/plugins/e2b-channels.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { INSTRUMENTATION_NAMES } from "../../span-origin";
import { channel, defineChannels } from "../core/channel-definitions";

export const e2bChannels = defineChannels(
"e2b",
{
create: channel<[templateOrOptions?: unknown, options?: unknown], unknown>({
channelName: "Sandbox.create",
kind: "async",
}),
run: channel<[command: string, options?: unknown], unknown>({
channelName: "Commands.run",
kind: "async",
}),
},
{ instrumentationName: INSTRUMENTATION_NAMES.E2B },
);

export const e2bCodeInterpreterChannels = defineChannels(
"@e2b/code-interpreter",
{
runCode: channel<[code: string, options?: unknown], unknown>({
channelName: "Sandbox.runCode",
kind: "async",
}),
},
{ instrumentationName: INSTRUMENTATION_NAMES.E2B },
);
94 changes: 94 additions & 0 deletions js/src/instrumentation/plugins/e2b-plugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { isObject, SpanTypeAttribute } from "../../../util/index";
import { BasePlugin } from "../core";
import {
traceAsyncChannel,
traceStreamingChannel,
unsubscribeAll,
} from "../core/channel-tracing";
import { e2bChannels, e2bCodeInterpreterChannels } from "./e2b-channels";

// Select public operation data instead of serializing connection options,
// callbacks, environment variables, or credential-bearing SDK instances.
function fields(value: unknown, keys: string[]): Record<string, unknown> {
if (!isObject(value)) return {};
return Object.fromEntries(
keys
.filter((key) => value[key] !== undefined)
.map((key) => [key, value[key]]),
);
}

export class E2BPlugin extends BasePlugin {
protected onEnable(): void {
this.unsubscribers.push(
traceAsyncChannel(e2bChannels.create, {
name: "e2b.Sandbox.create",
type: SpanTypeAttribute.TOOL,
extractInput: ([templateOrOptions, options]) => ({
input: {
...(typeof templateOrOptions === "string"
? { template: templateOrOptions }
: {}),
...fields(
typeof templateOrOptions === "string"
? options
: templateOrOptions,
["timeoutMs"],
),
},
metadata: { provider: "e2b" },
}),
extractOutput: (result) => fields(result, ["sandboxId"]),
extractMetrics: () => ({}),
}),
traceAsyncChannel(e2bChannels.run, {
name: "e2b.commands.run",
type: SpanTypeAttribute.TOOL,
extractInput: ([command, options]) => ({
input: { command },
metadata: {
provider: "e2b",
...fields(options, ["cwd", "user", "background", "timeoutMs"]),
},
}),
// Background calls return a CommandHandle, not a finished command.
extractOutput: (result) =>
fields(result, ["pid", "stdout", "stderr", "exitCode"]),
extractMetrics: () => ({}),
}),
traceStreamingChannel(e2bCodeInterpreterChannels.runCode, {
name: "e2b.runCode",
type: SpanTypeAttribute.TOOL,
extractInput: ([code, options], event) => ({
input: { code },
metadata: {
provider: "e2b",
...fields(event.self, ["sandboxId"]),
...fields(options, ["language", "timeoutMs"]),
...(isObject(options) && isObject(options.context)
? { context: fields(options.context, ["id", "language"]) }
: {}),
},
}),
extractOutput: (result) => fields(result, ["logs", "results", "error"]),
extractMetrics: () => ({}),
onComplete: ({ result, span }) => {
// Execution errors resolve successfully at the HTTP layer.
if (isObject(result) && isObject(result.error)) {
const { name, value, traceback } = result.error;
span.log({
error:
typeof traceback === "string" && traceback
? traceback
: `${String(name ?? "ExecutionError")}: ${String(value ?? "")}`,
});
}
},
}),
);
}

protected onDisable(): void {
this.unsubscribers = unsubscribeAll(this.unsubscribers);
}
}
1 change: 1 addition & 0 deletions js/src/span-origin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export const INSTRUMENTATION_NAMES = {
CURSOR_SDK: "cursor-sdk",
DEEPSEEK_HARNESS: "deepseek-harness",
EVE: "eve",
E2B: "e2b",
FLUE: "flue",
GENKIT: "genkit",
GITHUB_COPILOT: "github-copilot",
Expand Down
Loading