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/mastra-logfeedback-row-id.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"braintrust": patch
---

fix(mastra): Use the Mastra span id as the Braintrust row id so `logFeedback` attaches to the right row instead of landing as a stray. Matches the upstream `@mastra/braintrust` fix (mastra-ai/mastra#11927).
81 changes: 81 additions & 0 deletions js/src/wrappers/mastra.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
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 ExportedSpan = Parameters<
BraintrustObservabilityExporter["exportTracingEvent"]
>[0]["exportedSpan"];

const span = (overrides: Partial<ExportedSpan>): ExportedSpan => ({
id: "span-1",
traceId: "trace-1",
name: "agent run",
type: "agent_run",
startTime: 1_000_000,
...overrides,
});

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

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

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

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

test("logFeedback keyed on the Mastra span id merges into the span row", async () => {
const exporter = new BraintrustObservabilityExporter();
const id = "mastra-span-123";

await exporter.exportTracingEvent({
type: "span_started",
exportedSpan: span({ id }),
});
await exporter.exportTracingEvent({
type: "span_ended",
exportedSpan: span({ id, endTime: 1_000_001 }),
});
// A Mastra user (or Mastra's score-event bus) knows only the Mastra span id.
logger.logFeedback({ id, scores: { quality: 0.9 }, source: "external" });

await backgroundLogger.flush();
const rows = (await backgroundLogger.drain()) as any[];

// The exporter aliases the row id to the Mastra span id, so the feedback
// merge row keys onto the span row and `mergeRowBatch` collapses them into a
// single row carrying both the span and the score. Without the aliasing the
// feedback would land as a separate orphan row.
const merged = rows.filter((r) => r.id === id);
expect(merged).toHaveLength(1);
expect(merged[0].span_attributes?.name).toBe("agent run");
expect(merged[0].scores?.quality).toBe(0.9);
});
});
6 changes: 6 additions & 0 deletions js/src/wrappers/mastra.ts
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,12 @@ export class BraintrustObservabilityExporter implements MastraObservabilityExpor
name: exported.name,
spanAttributes: { type: spanTypeFor(exported.type) },
startTime: epochSeconds(exported.startTime),
// Use the Mastra span id as the Braintrust row id so that
// `logFeedback({ id: <mastra span id> })` (and Mastra's score events)
// attach to the right row. Without this, `SpanImpl` auto-generates a
// row id (`this._id = eventId ?? idGenerator.getSpanId()`) that no
// external caller could know.
event: { id: exported.id },
};

const parentRecord = exported.parentSpanId
Expand Down
Loading