Skip to content
Draft
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
13 changes: 13 additions & 0 deletions .changeset/mastra-tags-export.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
"braintrust": patch
---

fix(mastra): Emit Mastra tags as first-class Braintrust tags

Root-span tags from the Mastra observability exporter are now logged to the
top-level `tags` row field (which Braintrust surfaces as first-class tags and
filters on) instead of being nested under `metadata.tags`, where they were
invisible to the tag UI. Matching `@mastra/braintrust`, tags are attached only
to the Mastra root span.

Ports mastra-ai/mastra#12057 (re-fixes #9849).
100 changes: 100 additions & 0 deletions js/src/wrappers/mastra.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import {
afterEach,
beforeAll,
beforeEach,
describe,
expect,
test,
} from "vitest";
import { configureNode } from "../node/config";
import { _exportsForTestingOnly, initLogger } from "../logger";
import { BraintrustObservabilityExporter } from "./mastra";

try {
configureNode();
} catch {
// Best-effort initialization for test environments.
}

type MastraExportedSpan = Parameters<
BraintrustObservabilityExporter["exportTracingEvent"]
>[0]["exportedSpan"];

function makeSpan(overrides: Partial<MastraExportedSpan>): MastraExportedSpan {
return {
id: "span-1",
traceId: "trace-1",
name: "agent run",
type: "agent_run",
startTime: 1_000_000,
...overrides,
};
}

describe("BraintrustObservabilityExporter tags", () => {
let backgroundLogger: ReturnType<
typeof _exportsForTestingOnly.useTestBackgroundLogger
>;

beforeAll(async () => {
await _exportsForTestingOnly.simulateLoginForTests();
});

beforeEach(() => {
backgroundLogger = _exportsForTestingOnly.useTestBackgroundLogger();
initLogger({ projectId: "test-project-id", projectName: "mastra.test.ts" });
});

afterEach(() => {
_exportsForTestingOnly.clearTestBackgroundLogger();
});

// A span is only logged if it was started, so drive both lifecycle events.
const runSpan = async (
exporter: BraintrustObservabilityExporter,
span: MastraExportedSpan,
) => {
await exporter.exportTracingEvent({
type: "span_started",
exportedSpan: span,
});
await exporter.exportTracingEvent({
type: "span_ended",
exportedSpan: { ...span, endTime: 1_000_001 },
});
};

test("tags surface as a top-level field on the root span only", async () => {
const exporter = new BraintrustObservabilityExporter();
await runSpan(
exporter,
makeSpan({ id: "root", isRootSpan: true, tags: ["production", "beta"] }),
);
await runSpan(
exporter,
makeSpan({
id: "child",
name: "tool call",
type: "tool_call",
isRootSpan: false,
tags: ["should-not-appear"],
}),
);

const rows = (await backgroundLogger.drain()) as any[];
const byName = (name: string) =>
rows.find((r) => r.span_attributes?.name === name);

const root = byName("agent run");
expect(root).toBeDefined();
// Braintrust surfaces top-level `tags` as first-class tags; not metadata.
expect(root.tags).toEqual(["production", "beta"]);
expect(root.metadata?.tags).toBeUndefined();

// Braintrust tags are trace-level, so non-root spans carry none.
const child = byName("tool call");
expect(child).toBeDefined();
expect(child.tags).toBeUndefined();
expect(child.metadata?.tags).toBeUndefined();
});
});
18 changes: 15 additions & 3 deletions js/src/wrappers/mastra.ts
Original file line number Diff line number Diff line change
Expand Up @@ -186,9 +186,9 @@ function buildMetadata(exported: MastraExportedSpan): Record<string, unknown> {
if (value !== undefined) out[key] = value;
}
}
if (exported.tags && exported.tags.length > 0) {
out.tags = exported.tags;
}
// Note: `exported.tags` is intentionally NOT placed in metadata. Braintrust
// surfaces tags from the top-level `tags` row field (see `logPayload`), and
// nesting them under `metadata.tags` would hide them from the tag UI/filters.
if (exported.requestContext && isObject(exported.requestContext)) {
out.request_context = exported.requestContext;
}
Expand Down Expand Up @@ -351,6 +351,18 @@ export class BraintrustObservabilityExporter implements MastraObservabilityExpor
event.metrics = metrics;
}

// Emit tags as the top-level `tags` row field (via ExperimentLogPartialArgs)
// so Braintrust surfaces them as first-class tags rather than metadata.
// Braintrust tags are a trace-level concept, so — matching
// `@mastra/braintrust` — we only attach them to the Mastra root span.
if (
exported.isRootSpan === true &&
exported.tags &&
exported.tags.length > 0
) {
event.tags = exported.tags;
}

if (Object.keys(event).length > 0) {
record.span.log(event);
}
Expand Down
Loading