Skip to content
Merged
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/heavy-horses-tickle.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"braintrust": minor
---

feat: Track Eve turns as individual traces and record Eve session IDs on all spans
2,072 changes: 1,028 additions & 1,044 deletions e2e/scenarios/eve-instrumentation/__snapshots__/eve-instrumentation.span-tree.json

Large diffs are not rendered by default.

2,155 changes: 1,077 additions & 1,078 deletions e2e/scenarios/eve-instrumentation/__snapshots__/eve-instrumentation.span-tree.txt

Large diffs are not rendered by default.

136 changes: 73 additions & 63 deletions e2e/scenarios/eve-instrumentation/scenario.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,24 +46,30 @@ describe("eve instrumentation", () => {
);
}, TIMEOUT_MS);

test("captures multiple nested turns in one Eve session", async () => {
const session = findAllSpans(events, "eve.session").find(
(span) => span.span.parentIds.length === 0,
test("captures user turns as traces with subagent turns attached", async () => {
const turns = findAllSpans(events, "eve.turn");
const root = turns.find(
(turn) =>
Array.isArray(turn.input) &&
turn.input[0]?.content ===
"Run the Braintrust Eve instrumentation e2e scenario",
);
const secondRoot = turns.find(
(turn) =>
Array.isArray(turn.input) &&
turn.input[0]?.content ===
"Run the Braintrust Eve instrumentation e2e scenario again",
);
const turns = findChildSpans(events, "eve.turn", session?.span.id);
const [root, secondRoot] = turns;
const steps = findChildSpans(events, "eve.step", root?.span.id);
const researcher = findChildSpans(events, "researcher", root?.span.id)[0];
const childSession = findChildSpans(
events,
"eve.session",
researcher?.span.id,
)[0];
const childTurn = findChildSpans(
events,
"eve.turn",
childSession?.span.id,
)[0];
const childTurn = turns.find(
(turn) =>
Array.isArray(turn.input) &&
String(turn.input[0]?.content).includes(
"Caller message:\nRun the Braintrust Eve instrumentation e2e scenario",
) &&
!String(turn.input[0]?.content).includes("scenario again"),
);
const childSteps = findChildSpans(events, "eve.step", childTurn?.span.id);
const childSearch = findLatestChildSpan(
events,
Expand All @@ -77,51 +83,27 @@ describe("eve instrumentation", () => {
"researcher",
secondRoot?.span.id,
);
const secondChildSession = findLatestChildSpan(
events,
"eve.session",
secondResearcher?.span.id,
);
const secondChildTurn = findLatestChildSpan(
events,
"eve.turn",
secondChildSession?.span.id,
const secondChildTurn = turns.find(
(turn) =>
Array.isArray(turn.input) &&
String(turn.input[0]?.content).includes(
"Caller message:\nRun the Braintrust Eve instrumentation e2e scenario again",
),
);
const secondRead = findLatestChildSpan(events, "read", secondRoot?.span.id);

expect(session).toBeDefined();
expect(session?.span.type).toBe("task");
expect(session?.span.parentIds).toEqual([]);
expect(session?.metadata).toMatchObject({
model: "gpt-5.4-mini",
provider: "openai",
scenario: "eve-instrumentation",
testRunId: expect.any(String),
});
expect(turns).toHaveLength(2);
expect(turns.map((turn) => turn.span.parentIds)).toEqual([
[session?.span.id],
[session?.span.id],
]);
expect(turns.map((turn) => turn.input)).toEqual([
[
{
content: "Run the Braintrust Eve instrumentation e2e scenario",
role: "user",
},
],
[
{
content: "Run the Braintrust Eve instrumentation e2e scenario again",
role: "user",
},
],
]);
expect(findAllSpans(events, "eve.session")).toEqual([]);
expect(turns).toHaveLength(4);
expect(
turns.filter((turn) => turn.span.parentIds.length === 0),
).toHaveLength(2);
expect(new Set(turns.map((turn) => turn.span.rootId)).size).toBe(2);

expect(root).toBeDefined();
expect(root?.span.type).toBe("task");
expect(root?.span.parentIds).toEqual([session?.span.id]);
expect(root?.span.parentIds).toEqual([]);
expect(root?.metadata).toMatchObject({
"eve.session_id": expect.any(String),
scenario: "eve-instrumentation",
testRunId: expect.any(String),
});
Expand All @@ -140,6 +122,7 @@ describe("eve instrumentation", () => {
}
expect(step.input[0]).toMatchObject({ role: "system" });
expect(step.metadata).toMatchObject({
"eve.session_id": root?.metadata?.["eve.session_id"],
model: "gpt-5.4-mini",
provider: "openai",
scenario: "eve-instrumentation",
Expand All @@ -155,22 +138,23 @@ describe("eve instrumentation", () => {
message: expect.stringContaining("Braintrust Eve instrumentation"),
});
expect(researcher?.metadata).toMatchObject({
"eve.session_id": root?.metadata?.["eve.session_id"],
scenario: "eve-instrumentation",
testRunId: expect.any(String),
});
expect(researcher?.output).toContain("Researcher result");

expect(childSession).toBeDefined();
expect(childSession?.span.parentIds).toEqual([researcher?.span.id]);
expect(childSession?.span.rootId).toEqual(session?.span.rootId);

expect(childTurn).toBeDefined();
expect(childTurn?.span.parentIds).toEqual([childSession?.span.id]);
expect(childTurn?.span.parentIds).toEqual([researcher?.span.id]);
expect(childTurn?.span.rootId).toEqual(root?.span.rootId);
expect(childTurn?.metadata).toMatchObject({
"eve.session_id": expect.any(String),
scenario: "eve-instrumentation",
testRunId: expect.any(String),
});
expect(childTurn?.metadata?.["eve.session_id"]).not.toEqual(
root?.metadata?.["eve.session_id"],
);

expect(childSteps).toHaveLength(2);
for (const step of childSteps) {
Expand All @@ -182,6 +166,7 @@ describe("eve instrumentation", () => {
}
expect(step.input[0]).toMatchObject({ role: "system" });
expect(step.metadata).toMatchObject({
"eve.session_id": childTurn?.metadata?.["eve.session_id"],
model: "gpt-5.4-mini",
provider: "openai",
scenario: "eve-instrumentation",
Expand All @@ -193,6 +178,9 @@ describe("eve instrumentation", () => {
expect(childSearch?.span.type).toBe("tool");
expect(childSearch?.span.ended).toBe(true);
expect(childSearch?.span.parentIds).toEqual([childTurn?.span.id]);
expect(childSearch?.metadata).toMatchObject({
"eve.session_id": childTurn?.metadata?.["eve.session_id"],
});
expect(childSearch?.input).toMatchObject({
query: expect.stringContaining("Braintrust Eve instrumentation"),
});
Expand All @@ -204,6 +192,9 @@ describe("eve instrumentation", () => {
expect(read?.span.type).toBe("tool");
expect(read?.span.ended).toBe(true);
expect(read?.span.parentIds).toEqual([root?.span.id]);
expect(read?.metadata).toMatchObject({
"eve.session_id": root?.metadata?.["eve.session_id"],
});
expect(read?.input).toMatchObject({
url: "https://eve.dev/docs/guides/instrumentation",
});
Expand All @@ -214,6 +205,11 @@ describe("eve instrumentation", () => {

expect(secondRoot).toBeDefined();
expect(secondRoot?.span.type).toBe("task");
expect(secondRoot?.span.parentIds).toEqual([]);
expect(secondRoot?.span.rootId).not.toEqual(root?.span.rootId);
expect(secondRoot?.metadata).toMatchObject({
"eve.session_id": root?.metadata?.["eve.session_id"],
});
expect(secondRoot?.output).toContain("Final answer from read");
expect(secondSteps).toHaveLength(3);
expect(secondSteps.map((step) => step.span.type)).toEqual([
Expand All @@ -224,16 +220,30 @@ describe("eve instrumentation", () => {
expect(secondResearcher?.span.type).toBe("tool");
expect(secondResearcher?.span.ended).toBe(true);
expect(secondResearcher?.span.parentIds).toEqual([secondRoot?.span.id]);
expect(secondChildSession?.span.parentIds).toEqual([
secondResearcher?.span.id,
]);
expect(secondResearcher?.metadata).toMatchObject({
"eve.session_id": secondRoot?.metadata?.["eve.session_id"],
});
expect(secondChildTurn?.span.parentIds).toEqual([
secondChildSession?.span.id,
secondResearcher?.span.id,
]);
expect(secondChildTurn?.span.rootId).toEqual(secondRoot?.span.rootId);
expect(secondChildTurn?.metadata).toMatchObject({
"eve.session_id": expect.any(String),
});
expect(secondChildTurn?.metadata?.["eve.session_id"]).not.toEqual(
secondRoot?.metadata?.["eve.session_id"],
);
expect(secondRead?.span.type).toBe("tool");
expect(secondRead?.span.ended).toBe(true);
expect(secondRead?.span.parentIds).toEqual([secondRoot?.span.id]);
expect(secondRead?.metadata).toMatchObject({
"eve.session_id": secondRoot?.metadata?.["eve.session_id"],
});

await matchSpanTreeSnapshot(events, spanTreeSnapshotPath);
await matchSpanTreeSnapshot(events, spanTreeSnapshotPath, {
normalize: {
additionalProviderIdKeys: ["eve.session_id"],
},
});
});
});
Loading
Loading